release: promote beta to main (gyms, brand colours, JPEG XL, SEO per gym) #3

Merged
andre merged 74 commits from beta into main 2026-10-26 19:47:48 +00:00
3 changed files with 131 additions and 35 deletions
Showing only changes of commit 4b54a777b7 - Show all commits

No files matched your search

+1 -1
View File
@@ -79,7 +79,7 @@ Since 2026-09-25, one install serves several physical gyms: the `gyms` table, on
folder per gym (`/3070/`, `/<postal-code>/`), content rows carry `gym_id`, and the
backoffice has an "a editar" gym switcher (`admin_gym_id()` /
`admin_gym()` in `config/functions_gyms.php`). `/` and `/home` 301 to the default gym (`is_default`); the other published gyms appear as highlighted buttons (`.gym-switch-btn`) at the right end of the public navbar (`site/includes/navbar.php`). `site/gym_chooser.php` is no longer used.
Each gym has a brand colour (`gyms.brand_color`, `#RRGGBB`, migration `migration_20261013_gym_brand_color.sql`; gym 1 = green `#C2D500`, gym 2 = red): set in Gestão → Ginásios, emitted as `:root` overrides by `gym_brand_css()` in `site/includes/include.php`. New components must use `--fitme-accent*` / `--fitme-on-accent`, never a hard-coded colour; the CrossFit nav pill follows the brand colour too. The public navbar's bottom edge is an always-on, slow (14s) sweep in the brand colour (`.header-section::before/::after`, `nav-sweep`, static with reduced motion). On the home page (`$nav_center_logo` set in `site/index.php`, shared by `_open.php`) the header starts taller with the logo big (240px) and centred (`.has-center-logo.is-top`, desktop >= 992px only) and glides back (0.5s on padding-top/left/top/transform/width) to the 150px left logo once `scrollY >= 40` (script in `navbar.php`). On desktop the brand and the other-gym buttons (`.gym-switch`) are absolutely positioned (brand left, switch right and vertically centred on the whole header, even when tall), so the links are centred on the full bar width; nav padding and logo width live in CSS, not inline. Logos (`.navbar-brand img`, `.gym-switch-logo`) get the same `#pt-noise` grain filter as h1/h2, and `.hero-event-go`, `.cta-btn`, `.btn-primary` joined the `--noise` button list. Section-title underlines (`.section-title.deco h2:after`) use the same slashed 4px bar as the nav underline. The hero event button text uses `--fitme-on-accent` (white on red). The nav underline (`.nav-link:after`) is a 4px bar 3px under the text with slashed ends (`clip-path`, like the "in" box of the FitMe in logo). The hero event card (`.hero-event`) is square (no radius on card, badge or button), taller (88px min) and has a defocus gradient: `::after` is a `backdrop-filter: blur()` layer masked left->right, sharp on the left and most blurred on the right; the card itself must not get filter/backdrop-filter/opacity/mask or the blur stops reaching the hero photo. Each gym can have a `logo` (same migration, plus `migration_20261026_gym_logo.sql`: the column was added to the 2026-10-13 file after it had already run on dev, so dev had no `logo` column until 2026-10-26 — never edit a migration that has run, add a new one); the header image and the logo are uploaded in Gestão → Ginásios (`gyms_save_image()` → `uploads/gyms/`, validated by content, SVG through `svg_is_safe()`), not typed as paths; `db_gyms_create/update` skip `brand_color`/`logo` while the migration isn't applied (`_gyms_cols()`); the navbar button for another gym is drawn in that gym's brand colour (`gym_brand_vars()` inline); with a logo (`gym_logo()`: the gym's own, or for the default gym the bundled `logo_black.svg`) the button shows the logo in full colour straight on the frosted navbar, with no face and no darkening on hover (`.gym-switch-btn--logo`: thin brand outline, hover = spinning ring only, no glow, a thin dark `drop-shadow` outline on the image for contrast (a white halo washed out the lime FitMe 3070 logo); name as alt), without one it shows the name. The navbar brand on each gym's page uses that gym's logo too. Uploads (header + logo) also accept JPEG XL (`.jxl`, detected by signature in `upload_image_ext()` when the caller allows `'jxl'`; the gyms form and gallery photos (`db_gallery_image_upload()`) do; `AddType image/jxl` in `.htaccess`; GD can't resize it, so it is stored as uploaded; Chrome/Firefox may not display JXL, Safari does). The public gallery lists its folder by extension (`site/includes/gallery.php`, `site/index.php`: jpg/jpeg/png/gif/webp/jxl; before 2026-10-26 it only listed `*.jpg`, so PNG/WebP uploads never showed). Gallery photos are not in the scroll "hover" (`.is-centered`, main.js) any more; only a real hover lights them. Mensalidades (`.pricing-section::before`) has a slow (26s) drifting brand-colour glow over the metal texture (`pricing-drift`, static with reduced motion).
Each gym has a brand colour (`gyms.brand_color`, `#RRGGBB`, migration `migration_20261013_gym_brand_color.sql`; gym 1 = green `#C2D500`, gym 2 = red): set in Gestão → Ginásios, emitted as `:root` overrides by `gym_brand_css()` in `site/includes/include.php`. New components must use `--fitme-accent*` / `--fitme-on-accent`, never a hard-coded colour; the CrossFit nav pill follows the brand colour too. The public navbar's bottom edge is an always-on, slow (14s) sweep in the brand colour (`.header-section::before/::after`, `nav-sweep`, static with reduced motion). On the home page (`$nav_center_logo` set in `site/index.php`, shared by `_open.php`) the header starts taller (desktop >= 992px) with a second, big (180px) copy of the logo centred (`.navbar-brand-hero`, `aria-hidden`); once `scrollY >= 40` (script in `navbar.php` toggles `.is-top`) the bar shrinks, the big logo fades out and the 150px left logo fades in (opacity crossfade, no movement). The hero gets `padding-top: 120px` on desktop so its event card clears the tall bar. On desktop the brand is absolute at the left and the links are centred on the full bar width; nav padding and logo width live in CSS, not inline. The other-gym buttons (`.gym-switch`) sit in the **match head**: an absolute dark block at the right end of the bar (full bar height, width follows its buttons), its left side cut on the brand slash (22px) with a 5px accent strip and an ember glow (`.gym-switch::before/::after`); logos inside it are lightness-flipped (`invert(1) hue-rotate(180deg)`) so dark lettering reads on the dark head. 992-1199px: links shrink (`.8125rem`, `--bs-navbar-nav-link-padding-x: .35rem`) so the 3070 menu clears the logo and head. Menu links are `.875rem` bold. Below 992px the open menu fills the screen under the bar (absolute, `100dvh - 60px`, links/buttons centred, opacity fade; not `position:fixed` because the header's backdrop-filter is its containing block). The bar is liquid glass in Chromium: `backdrop-filter: url(#nav-glass) blur(6px) saturate(180%)` (`#nav-glass` in `include.php`: blurred low-frequency `feTurbulence` -> `feDisplacementMap`, bends the page behind it), gated by `@supports (-webkit-app-region: drag)` (Chromium-only property); Safari/Firefox keep the plain blur. **Brand slash template:** the cut corners of the "in" box are one rule in `style.css` ("Brand slash (template)"): `class="fx-slash"` (both ends) or `fx-slash-start` (left only), size via `--slash` (default 4px); pseudo-elements are added to that rule's selector list. Used by the nav and section-title underlines and the CrossFit pill (`--slash: 8px`, no radius); never paste the polygon again. The hero subtitle (`.hero-text h1 span`, gym `hero_subtitle`) is a block strapline at `.42em` of the h1, tracked out. Logos (`.navbar-brand img`, `.gym-switch-logo`) get the same `#pt-noise` grain filter as h1/h2, and `.hero-event-go`, `.cta-btn`, `.btn-primary` joined the `--noise` button list. Section-title underlines (`.section-title.deco h2:after`) use the same slashed 4px bar as the nav underline. The hero event button text uses `--fitme-on-accent` (white on red). The nav underline (`.nav-link:after`) is a 4px bar 3px under the text with slashed ends (`clip-path`, like the "in" box of the FitMe in logo). The hero event card (`.hero-event`) is square (no radius on card, badge or button), taller (88px min) and has a defocus gradient: `::after` is a `backdrop-filter: blur()` layer masked left->right, sharp on the left and most blurred on the right; the card itself must not get filter/backdrop-filter/opacity/mask or the blur stops reaching the hero photo. Each gym can have a `logo` (same migration, plus `migration_20261026_gym_logo.sql`: the column was added to the 2026-10-13 file after it had already run on dev, so dev had no `logo` column until 2026-10-26 — never edit a migration that has run, add a new one); the header image and the logo are uploaded in Gestão → Ginásios (`gyms_save_image()` → `uploads/gyms/`, validated by content, SVG through `svg_is_safe()`), not typed as paths; `db_gyms_create/update` skip `brand_color`/`logo` while the migration isn't applied (`_gyms_cols()`); the navbar button for another gym is drawn in that gym's brand colour (`gym_brand_vars()` inline); with a logo (`gym_logo()`: the gym's own, or for the default gym the bundled `logo_black.svg`) the button shows the logo in full colour straight on the frosted navbar, with no face and no darkening on hover (`.gym-switch-btn--logo`: thin brand outline, hover = spinning ring only, no glow, a thin dark `drop-shadow` outline on the image for contrast (a white halo washed out the lime FitMe 3070 logo); name as alt), without one it shows the name. The navbar brand on each gym's page uses that gym's logo too. Uploads (header + logo) also accept JPEG XL (`.jxl`, detected by signature in `upload_image_ext()` when the caller allows `'jxl'`; the gyms form and gallery photos (`db_gallery_image_upload()`) do; `AddTyLine truncated
An unpublished gym 404s publicly but previews for staff.
## Imagens / JPEG XL (2026-10-26)
+118 -24
View File
@@ -359,14 +359,30 @@ section[id] {
/* ── Bootstrap Nav Overrides ───────────────────────────────────────────────── */
.nav-link {
font-weight: 700;
font-size: .875rem;
letter-spacing: .02em;
color: var(--fitme-text);
margin: 0 .6rem !important;
transition: color var(--fitme-transition);
position: relative;
}
/* Underline: a short bar with slashed ends (top-left and bottom-right cut, like
the "in" box of the FitMe in logo), tucked right under the text. */
/* ── Brand slash (template) ──────────────────────────────────────────────────
The cut of the "in" box in the FitMe in logo: top-left and bottom-right
corners sliced off. Put class="fx-slash" on any element (or add a pseudo
element to this list) instead of writing the polygon again; --slash sets the
cut size. .fx-slash-start cuts the left side only (open to the right edge). */
:root { --slash: 4px; }
.fx-slash,
.nav-link::after,
.section-title.deco h2::after {
clip-path: polygon(var(--slash) 0, 100% 0, calc(100% - var(--slash)) 100%, 0 100%);
}
.fx-slash-start {
clip-path: polygon(var(--slash) 0, 100% 0, 100% 100%, 0 100%);
}
/* Underline: a short bar with the brand slash, tucked right under the text. */
.nav-link:after {
content: '';
position: absolute;
@@ -375,7 +391,6 @@ section[id] {
right: var(--bs-navbar-nav-link-padding-x, .5rem);
height: 4px;
background: var(--fitme-accent);
clip-path: polygon(4px 0, 100% 0, calc(100% - 4px) 100%, 0 100%);
transform: scaleX(0);
transform-origin: left;
transition: transform var(--fitme-transition);
@@ -402,8 +417,8 @@ section[id] {
text-transform: uppercase;
letter-spacing: .5px;
border: 2px solid var(--fitme-accent);
border-radius: var(--fitme-radius);
padding: 6px 14px !important;
--slash: 8px; /* shape: .fx-slash on the link (navbar.php) */
padding: 6px 16px !important;
transition: background-color .4s ease, color .4s ease, border-color .4s ease;
}
@@ -610,14 +625,15 @@ section[id] {
/* Bar layout. Desktop: the links are centred on the full bar width, the logo
sits out of flow at the left and the other-gym buttons out of flow at the
right, vertically centred on the whole header. On the home page
(.has-center-logo) the header starts taller with the logo bigger and centred
(.is-top); on scroll everything glides back (navbar.php toggles .is-top). */
(.has-center-logo) the header starts taller with a big centred copy of the
logo (.navbar-brand-hero, .is-top); on scroll the bar shrinks, the big logo
fades out and the left one fades in (navbar.php toggles .is-top). */
.header-section .navbar { padding: 0 0 0 1rem; }
.navbar-brand img {
width: 150px;
height: auto;
transition: width .5s cubic-bezier(.4, 0, .2, 1);
}
.navbar-brand-hero { display: none; }
@media (min-width: 992px) {
.header-section .navbar {
padding: 0;
@@ -630,31 +646,99 @@ section[id] {
top: 50%;
margin: 0;
transform: translate(0, -50%);
transition: left .5s cubic-bezier(.4, 0, .2, 1), top .5s cubic-bezier(.4, 0, .2, 1), transform .5s cubic-bezier(.4, 0, .2, 1);
}
.header-section .navbar-brand,
.navbar-brand-hero {
transition: opacity .45s ease, visibility .45s;
}
.has-center-logo .navbar-brand-hero {
display: block;
position: absolute;
z-index: 1;
left: 50%;
top: 12px;
transform: translateX(-50%);
opacity: 0;
visibility: hidden;
}
.navbar-brand-hero img { width: 180px; height: auto; }
.header-section .navbar-collapse {
min-height: 58px;
align-items: center;
}
.header-section .gym-switch {
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
padding: 0 !important;
}
.header-section .gym-switch { position: absolute; } /* the match head, below */
.header-section.is-top .navbar { padding-top: 62px; } /* room for the 180px logo */
.header-section.is-top .navbar-brand {
left: 50%;
top: 12px;
transform: translate(-50%, 0);
/* Match head: the right end of the bar is a dark block behind the other-gym
button, its left side cut on the same slash as the underlines, with an
accent strip on the cut and an ember glow just inside it (the bar is the
matchstick, this is the head). ::before = accent strip, ::after = head. */
.header-section .gym-switch {
--slash: 22px;
top: 0;
bottom: 0;
right: 0;
transform: none;
z-index: 1;
align-items: center;
padding: 0 .85rem 0 calc(var(--slash) + .85rem) !important;
}
.header-section.is-top .navbar-brand img { width: 180px; }
.header-section .gym-switch::before,
.header-section .gym-switch::after {
content: '';
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
clip-path: polygon(var(--slash) 0, 100% 0, 100% 100%, 0 100%); /* .fx-slash-start */
}
.header-section .gym-switch::before { left: -5px; background: var(--fitme-accent); }
.header-section .gym-switch::after {
background:
radial-gradient(70% 90% at 10% 50%, rgba(var(--fitme-accent-rgb), .38), transparent 70%),
linear-gradient(180deg, #1b1b1f, #0b0b0d);
}
/* other-gym logos are drawn for a light bar: flip lightness, keep the hues */
.header-section .gym-switch .gym-switch-btn--logo .gym-switch-logo {
filter: url(#pt-noise) invert(1) hue-rotate(180deg) drop-shadow(0 1px 1px rgba(0, 0, 0, .4));
}
.header-section.is-top .navbar-brand { opacity: 0; visibility: hidden; }
.header-section.is-top .navbar-brand-hero { opacity: 1; visibility: visible; }
/* keep the centred hero content (event card) clear of the taller top bar */
.hero-section { padding-top: 120px; }
}
@media (prefers-reduced-motion: reduce) {
.header-section .navbar, .header-section .navbar-brand, .navbar-brand img { transition: none; }
.header-section .navbar, .header-section .navbar-brand, .navbar-brand-hero { transition: none; }
}
/* Mobile/tablet menu: the open collapse fills the screen below the bar, links
and gym buttons centred both ways, fading in and out. It is position:absolute
(top:100% of the .navbar), not fixed: the header's backdrop-filter makes the
header the containing block for fixed children anyway. Links shrink to their
text so the underline is only as wide as the word. */
@media (max-width: 991.98px) {
.header-section .navbar-collapse {
position: absolute;
top: 100%;
left: 0;
right: 0;
height: calc(100dvh - var(--nav-h, 60px)) !important;
overflow-y: auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1.5rem;
transition: opacity .3s ease;
}
.header-section .navbar-collapse:not(.show):not(.collapsing) { display: none; }
.header-section .navbar-collapse.collapsing { opacity: 0; transition: opacity .3s ease; }
.header-section .navbar-nav { align-items: center; gap: .25rem; }
.header-section .navbar-nav .nav-link { font-size: 1.25rem; margin: 0 !important; }
.header-section .navbar-nav .nav-link::after { left: 0; right: 0; } /* no side padding here */
.header-section .gym-switch { justify-content: center; }
}
@media (max-width: 991.98px) and (prefers-reduced-motion: reduce) {
.header-section .navbar-collapse, .header-section .navbar-collapse.collapsing { transition: none; }
}
@media (prefers-reduced-motion: reduce) {
@@ -801,7 +885,15 @@ section[id] {
letter-spacing: -.5px;
}
/* Subtitle (gym hero_subtitle): same h1 but a much smaller, tracked-out line
so the title leads and this reads as its strapline. */
.hero-text h1 span {
display: block;
margin-top: .55em;
font-size: .42em;
font-weight: 800;
line-height: 1.3;
letter-spacing: .12em;
color: var(--fitme-accent);
}
@@ -2204,6 +2296,9 @@ h3.contact-title {
/* ── 992px–1199px ──────────────────────────────────────────────────────────── */
@media only screen and (min-width: 992px) and (max-width: 1199px) {
/* tighter link spacing so a long menu (3070) clears the logo and match head */
.header-section .navbar-nav { --bs-navbar-nav-link-padding-x: .35rem; }
.header-section .nav-link { margin: 0 .2rem !important; font-size: .8125rem; }
.header-right .hr-box {
display: none !important;
}
@@ -2588,8 +2683,7 @@ h3.contact-title {
transform: translateX(-50%);
width: 64px;
height: 4px;
background: var(--fitme-accent);
clip-path: polygon(4px 0, 100% 0, calc(100% - 4px) 100%, 0 100%); /* slashed ends, as .nav-link:after */
background: var(--fitme-accent); /* slashed ends: "Brand slash" template */
}
.section-subtitle {
+12 -10
View File
@@ -9,19 +9,21 @@ else
}
$logo = base_url($logo_rel);
$logo_dims = svg_img_dims_attr(dirname(__DIR__, 2) . '/' . $logo_rel);
// Home page: the logo starts big and centred over the hero (.is-top) and docks
// back to the left of the bar once the page scrolls (script below).
$logo_alt = 'Logo ' . (current_gym()['name'] ?? 'FitMe 3070');
$logo_html = img_fallback_rel($logo_rel) !== ''
? img_tag($logo_rel, ['loading' => 'eager', 'decoding' => 'async', 'alt' => $logo_alt])
: '<img loading="eager" decoding="async" alt="' . htmlspecialchars($logo_alt) . '" src="' . htmlspecialchars($logo) . '" ' . $logo_dims . '>';
// Home page: a second, big copy of the logo shows centred over the hero
// (.is-top); on scroll it fades out and the normal left logo fades in
// (script below toggles .is-top, style.css "Bar layout").
$center_logo = !empty($nav_center_logo);
?>
<header class="header-section lazy<?= $center_logo ? ' has-center-logo is-top' : '' ?>">
<nav class="navbar navbar-expand-lg navbar-light">
<a class="navbar-brand" href="<?= htmlspecialchars(gym_url(current_gym())) ?>">
<?php if (img_fallback_rel($logo_rel) !== ''): ?>
<?= img_tag($logo_rel, ['loading' => 'eager', 'decoding' => 'async', 'alt' => 'Logo ' . (current_gym()['name'] ?? 'FitMe 3070')]) ?>
<?php else: ?>
<img loading="eager" decoding="async" alt="Logo <?= htmlspecialchars(current_gym()['name'] ?? 'FitMe 3070') ?>" src="<?= htmlspecialchars($logo) ?>" <?= $logo_dims ?>>
<?php endif; ?>
</a>
<a class="navbar-brand" href="<?= htmlspecialchars(gym_url(current_gym())) ?>"><?= $logo_html ?></a>
<?php if ($center_logo): ?>
<a class="navbar-brand-hero" href="<?= htmlspecialchars(gym_url(current_gym())) ?>" aria-hidden="true" tabindex="-1"><?= $logo_html ?></a>
<?php endif; ?>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mx-auto ">
@@ -40,7 +42,7 @@ $center_logo = !empty($nav_center_logo);
?>
<li class="nav-item <?= $is_active ? 'active' : '' ?> main-menu-li" >
<?php if ($is_crossfit): ?>
<a class="nav-link crossfit-nav-link" href="<?= htmlspecialchars($n) ?>"<?= $is_active ? ' aria-current="page"' : '' ?>>
<a class="nav-link crossfit-nav-link fx-slash" href="<?= htmlspecialchars($n) ?>"<?= $is_active ? ' aria-current="page"' : '' ?>>
<?= $key ?>
</a>
<?php elseif ($is_anchor): ?>