diff --git a/.htaccess b/.htaccess
index d2f4cfb..0ed500c 100644
--- a/.htaccess
+++ b/.htaccess
@@ -40,6 +40,8 @@ RewriteRule ^home/inscricao/?$ site/inscricao.php [L,QSA]
RewriteRule ^home/crossfit/?$ site/crossfit.php [L,QSA]
RewriteRule ^home/privacidade/?$ site/privacidade.php [L,QSA]
RewriteRule ^evento/([a-zA-Z0-9-]+)/?$ site/evento.php?slug=$1 [L,QSA]
+RewriteRule ^robots\.txt$ site/seo_files.php?f=robots [L]
+RewriteRule ^sitemap\.xml$ site/seo_files.php?f=sitemap [L]
# Pass the Authorization header through to PHP
RewriteCond %{HTTP:Authorization} .
diff --git a/config/functions_site.php b/config/functions_site.php
index b32b669..49867f0 100644
--- a/config/functions_site.php
+++ b/config/functions_site.php
@@ -101,3 +101,26 @@ function site_horario(PDO $pdo, string $local = ''): array {
try { $st = $pdo->prepare($sql); $st->execute($args); return $st->fetchAll(PDO::FETCH_ASSOC); }
catch (Throwable $e) { return []; }
}
+
+// ─── Indexing ─────────────────────────────────────────────────────────────────
+// SEO_NOINDEX=true in .env keeps an install out of search results (meta robots
+// + robots.txt "Disallow: /"). For staging copies such as fitme.andmore.pt,
+// which serve the same content as the real domain and would otherwise compete
+// with it as duplicate content.
+function seo_noindex(): bool {
+ return filter_var(env('SEO_NOINDEX', 'false'), FILTER_VALIDATE_BOOLEAN);
+}
+
+// Public, indexable URLs for sitemap.xml: the fixed pages plus any live event
+// landing page.
+function site_sitemap_urls(PDO $pdo): array {
+ $urls = [
+ ['loc' => base_url(), 'priority' => '1.0'],
+ ['loc' => base_url('home/crossfit'), 'priority' => '0.8'],
+ ['loc' => base_url('home/privacidade'), 'priority' => '0.2'],
+ ];
+ foreach (site_active_events($pdo) as $ev) {
+ $urls[] = ['loc' => base_url('evento/' . rawurlencode($ev['slug'])), 'priority' => '0.6'];
+ }
+ return $urls;
+}
diff --git a/site/_open.php b/site/_open.php
index 08a1c38..b067a0c 100644
--- a/site/_open.php
+++ b/site/_open.php
@@ -13,3 +13,6 @@ $GLOBALS['_view_shared'] = compact('info', 'navbar', 'current_uri_2');
echo view('include'); // renders view('seo') inside
echo view('navbar');
echo view('messenger_chat');
+// Page content landmark — closed at the top of site/includes/footer.php, so
+// every public page gets one (and the skip link a real target).
+echo '' . "\n";
diff --git a/site/assets/css/style.css b/site/assets/css/style.css
index 4c3a9e9..2c88619 100644
--- a/site/assets/css/style.css
+++ b/site/assets/css/style.css
@@ -581,9 +581,19 @@ section[id] {
background: var(--fitme-white);
}
+/* Team backdrop: the warm grey of the gym's block wall in the header photo
+ (sampled ≈ rgb(70,66,62)), a touch darker so white names still pop. On top,
+ very faint mortar joints in running bond (120×60 blocks, alternate rows
+ offset by half a block) plus a fine concrete grain, both as inline SVG
+ tiles, and a soft shade at the top and bottom edges. */
.trainers-section {
- background: var(--fitme-dark);
position: relative;
+ background-color: #403d3a;
+ background-image:
+ linear-gradient(180deg, rgba(0, 0, 0, .28) 0%, transparent 18%, transparent 82%, rgba(0, 0, 0, .28) 100%),
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .07 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"),
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='120'%3E%3Cpath d='M0 1H240M0 61H240M1 0V60M121 0V60M61 60V120M181 60V120' fill='none' stroke='rgba(0,0,0,0.09)' stroke-width='1.5'/%3E%3Cpath d='M0 3.5H240M0 63.5H240' fill='none' stroke='rgba(255,255,255,0.022)' stroke-width='1'/%3E%3C/svg%3E");
+ background-size: 100% 100%, 200px 200px, 240px 120px;
}
.trainers-section:before {
@@ -623,6 +633,59 @@ section[id] {
.workhours-section {
background: var(--fitme-white);
+ padding: 48px 0 44px;
+}
+
+.workhours-section .section-title {
+ margin-bottom: 1.4rem;
+}
+
+/* Compact opening-hours cards: dark day label on top, hours underneath,
+ side by side and close together (wraps to a column on phones). */
+.wh-grid {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 12px;
+ margin: 0;
+}
+
+.wh-day {
+ flex: 0 1 210px;
+ border-radius: 6px;
+ overflow: hidden;
+ background: var(--fitme-surface);
+ border: 1px solid var(--fitme-surface-2);
+ text-align: center;
+}
+
+.wh-day dt {
+ background: var(--fitme-dark-3);
+ color: #fff;
+ font-size: .9rem;
+ font-weight: 700;
+ padding: 8px 10px;
+}
+
+.wh-day dd {
+ margin: 0;
+ padding: 7px 10px;
+ font-weight: 700;
+ font-size: 1rem;
+ color: var(--fitme-text);
+}
+
+.wh-day dd + dd {
+ border-top: 1px solid var(--fitme-surface-2);
+}
+
+.wh-day dd.wh-closed {
+ font-weight: 600;
+ color: var(--fitme-text-muted);
+}
+
+@media (max-width: 575px) {
+ .wh-day { flex-basis: 100%; }
}
.map-section {
@@ -698,6 +761,7 @@ section[id] {
mix-blend-mode: multiply;
}
+.icon-box-item h3,
.icon-box-item h4 {
margin-bottom: .75rem;
font-size: 1.15rem;
@@ -819,6 +883,7 @@ section[id] {
visibility: hidden;
}
+.trainer-item h3,
.trainer-item h4 {
margin-bottom: .3rem;
}
@@ -835,6 +900,7 @@ section[id] {
}
.trainers-section .section-title h2,
+.trainers-section .trainer-item h3,
.trainers-section .trainer-item h4,
.trainers-section .trainer-item>*:last-child {
color: #fff;
@@ -1136,10 +1202,16 @@ section[id] {
margin-bottom: 2.5rem;
}
+/* The form heading is an h3 under the section's h2 — keep the h2 look. */
+h3.contact-title {
+ font-size: 2.25rem;
+}
+
.contact-info-warp {
margin-bottom: 2rem;
}
+.contact-info-warp h3,
.contact-info-warp h4 {
color: var(--fitme-accent);
margin-bottom: .5rem;
@@ -2152,6 +2224,7 @@ section[id] {
max-height: 34px;
}
+ .icon-box-item h3,
.icon-box-item h4 {
font-size: .95rem;
}
@@ -2551,6 +2624,96 @@ section[id] {
display: none !important;
}
+/* ── Horário: compact cards (on screen, every width) ───────────────────────────
+ Smaller, narrower cards and type so a day reads at a glance and three days
+ fit side by side on desktop. The JPG export keeps its own sizing. */
+.schedule:not(.schedule-export) .schedule_cell {
+ gap: 6px;
+ margin-bottom: .6rem;
+}
+
+.schedule:not(.schedule-export) .schedule_cell > .schedule_box {
+ flex: 0 1 112px;
+ padding: 6px 3px;
+}
+
+.schedule:not(.schedule-export) .schedule_box img {
+ height: 22px;
+ margin-bottom: 2px;
+}
+
+.schedule:not(.schedule-export) .schedule_box a {
+ font-size: 10px;
+ min-height: 2.3em;
+}
+
+.schedule:not(.schedule-export) .schedule_info {
+ margin: auto -3px -6px;
+ padding: 4px 6px;
+ font-size: 11.5px;
+ min-height: 2.4em;
+}
+
+.schedule:not(.schedule-export) .schedule_box:hover {
+ transform: scale(1.05);
+}
+
+.schedule:not(.schedule-export) .schedule_dayhead {
+ margin-bottom: .7rem;
+}
+
+.schedule:not(.schedule-export) .rounded_frame {
+ width: 104px;
+ height: 32px;
+ font-size: .95rem;
+}
+
+.schedule:not(.schedule-export) .schedule_section_label {
+ margin: .5rem 0 .8rem;
+}
+
+.schedule:not(.schedule-export) .schedule_section_label span {
+ font-size: .72rem;
+}
+
+/* ── Desktop: yesterday · today · tomorrow ─────────────────────────────────────
+ ≥992px, main.js shows three days — the chosen one (today by default) centred
+ between its neighbours, wrapping round the week — and tags each visible
+ header/cell with .sched-pos-1/2/3 for its column. Rows stay time-aligned, so
+ classes at the same hour line up across the three days. The side days are
+ toned down so the centre one leads. */
+@media (min-width: 992px) {
+ .schedule.is-3day:not(.schedule-export) .schedule_panel {
+ grid-template-columns: repeat(3, minmax(0, 420px));
+ column-gap: 28px;
+ }
+ .schedule.is-3day:not(.schedule-export) .schedule_section_label {
+ grid-column: 1 / -1;
+ }
+ .schedule.is-3day:not(.schedule-export) .sched-pos-1 { grid-column: 1 !important; }
+ .schedule.is-3day:not(.schedule-export) .sched-pos-2 { grid-column: 2 !important; }
+ .schedule.is-3day:not(.schedule-export) .sched-pos-3 { grid-column: 3 !important; }
+
+ .schedule.is-3day:not(.schedule-export) .schedule_cell.sched-side {
+ opacity: .5;
+ transition: opacity var(--fitme-transition);
+ }
+ .schedule.is-3day:not(.schedule-export) .schedule_cell.sched-side:hover {
+ opacity: 1;
+ }
+ .schedule.is-3day:not(.schedule-export) .schedule_dayhead.sched-side .rounded_frame {
+ background: var(--fitme-surface-2);
+ color: var(--fitme-text-muted);
+ }
+ .schedule.is-3day:not(.schedule-export) .schedule_dayhead.sched-pos-2 .rounded_frame {
+ background: var(--fitme-accent);
+ color: var(--fitme-dark);
+ }
+ .schedule.is-3day:not(.schedule-export) .schedule_dayhead.sched-side {
+ cursor: pointer;
+ }
+}
+
/* ── Aula experimental modal ───────────────────────────────────────────────── */
.modal-content.trial-modal {
background: #fff !important;
@@ -2963,6 +3126,7 @@ section[id] {
/* The name used to be the tile's last child and got its size from the
`.trainer-item>*:last-child` rule; pin it so it stays the same whether or
not a title follows it. */
+.trainers-section .trainer-item h3,
.trainers-section .trainer-item h4 {
font-size: .9rem;
}
diff --git a/site/assets/js/main.js b/site/assets/js/main.js
index e7a3e6a..c6c8b33 100644
--- a/site/assets/js/main.js
+++ b/site/assets/js/main.js
@@ -152,10 +152,11 @@ document.addEventListener('DOMContentLoaded', function () {
});
/* ── Schedule day filter ─────────────────────────────────────────────────────
- The on-screen schedule shows one day at a time; a day-picker bar switches days
- (each header + class cell is tagged with data-day). Defaults to today when the
- gym is open that day, otherwise the first day. The JPG export is unaffected —
- its clone removes every .sched-hide and re-renders the full week. */
+ The on-screen schedule shows the chosen day (phones) or that day centred
+ between the day before and after (desktop ≥992px); a day-picker bar switches
+ days (each header + class cell is tagged with data-day). Defaults to today
+ when the gym is open that day, otherwise the first day. The JPG export is
+ unaffected — its clone removes every .sched-hide and re-renders the full week. */
document.addEventListener('DOMContentLoaded', function () {
// Works on the home page (#horario) and the CrossFit page (bare .schedule).
var scope = document.querySelector('.schedule');
@@ -166,19 +167,40 @@ document.addEventListener('DOMContentLoaded', function () {
var buttons = filter.querySelectorAll('.sdf-btn');
var labels = scope.querySelectorAll('.schedule_section_label');
- // Toggle a class (not inline display) — .sched-hide carries !important so it
- // reliably wins, and the export clone simply strips it to show every day.
+ // Desktop shows three days — the chosen one centred between the day before
+ // and the day after (wrapping round the week); phones show just the one.
+ var wide = window.matchMedia('(min-width: 992px)');
+ var week = Array.prototype.map.call(buttons, function (b) { return b.getAttribute('data-day'); });
+ var current = null;
+
+ // Toggle classes (not inline display) — .sched-hide carries !important so it
+ // reliably wins, and the export clone simply strips it (and ignores the
+ // .sched-pos-* columns) to show every day.
function showDay(day) {
+ current = day;
+ var i = week.indexOf(day), n = week.length;
+ var three = wide.matches && n >= 3;
+ var visible = three ? [week[(i - 1 + n) % n], day, week[(i + 1) % n]] : [day];
+ scope.classList.toggle('is-3day', three);
cells.forEach(function (c) {
- c.classList.toggle('sched-hide', c.getAttribute('data-day') !== day);
+ var pos = visible.indexOf(c.getAttribute('data-day'));
+ c.classList.toggle('sched-hide', pos === -1);
+ c.classList.remove('sched-pos-1', 'sched-pos-2', 'sched-pos-3', 'sched-side');
+ if (pos !== -1) {
+ var col = three ? pos + 1 : 1;
+ c.classList.add('sched-pos-' + col);
+ if (three && col !== 2) c.classList.add('sched-side');
+ }
});
buttons.forEach(function (b) {
b.classList.toggle('active', b.getAttribute('data-day') === day);
});
- // Show a Manhã / Tarde divider only when the day has a class in that block.
+ // Show a Manhã / Tarde divider only when a visible day has a class in that block.
labels.forEach(function (lbl) {
var sec = lbl.getAttribute('data-section');
- var has = !!scope.querySelector('.schedule_cell[data-day="' + day + '"][data-section="' + sec + '"]');
+ var has = visible.some(function (d) {
+ return !!scope.querySelector('.schedule_cell[data-day="' + d + '"][data-section="' + sec + '"]');
+ });
lbl.classList.toggle('sched-hide', !has);
});
}
@@ -187,6 +209,13 @@ document.addEventListener('DOMContentLoaded', function () {
var btn = e.target.closest('.sdf-btn');
if (btn) showDay(btn.getAttribute('data-day'));
});
+ // Clicking a side day's header centres it.
+ scope.addEventListener('click', function (e) {
+ var head = e.target.closest('.schedule_dayhead.sched-side');
+ if (head) showDay(head.getAttribute('data-day'));
+ });
+ var reflow = function () { if (current) showDay(current); };
+ if (wide.addEventListener) wide.addEventListener('change', reflow); else if (wide.addListener) wide.addListener(reflow);
// Pre-select today when the schedule has a button for it (PT day names),
// otherwise the first day.
diff --git a/site/includes/footer.php b/site/includes/footer.php
index 438be4a..f129bf4 100644
--- a/site/includes/footer.php
+++ b/site/includes/footer.php
@@ -1,3 +1,5 @@
+
+