diff --git a/config/functions_site.php b/config/functions_site.php index 49867f0..4476ad3 100644 --- a/config/functions_site.php +++ b/config/functions_site.php @@ -16,6 +16,8 @@ function site_info(PDO $pdo): array { foreach ($cols as $c) { if (!array_key_exists($c, $row) || $row[$c] === null) $row[$c] = ''; } + // wa.me only accepts digits — a stray "+" or space would break the link. + $row['whatsapp'] = preg_replace('/\D+/', '', (string) $row['whatsapp']); return $row; } diff --git a/site/assets/css/style.css b/site/assets/css/style.css index 9f4427d..15eae51 100644 --- a/site/assets/css/style.css +++ b/site/assets/css/style.css @@ -1380,11 +1380,15 @@ h3.contact-title { position: absolute; inset: 0; z-index: 0; - background: + /* Same matte-metal texture as the reviews section, over the (now barely + visible) gym photo. */ + background-image: + var(--metal-layers), linear-gradient(180deg, - rgba(10, 10, 10, 0.94) 0%, - rgba(10, 10, 10, 0.88) 50%, - rgba(10, 10, 10, 0.94) 100%); + rgba(20, 21, 23, 0.96) 0%, + rgba(20, 21, 23, 0.92) 50%, + rgba(20, 21, 23, 0.96) 100%); + background-size: var(--metal-sizes), 100% 100%; } .footer-section .container { @@ -3584,34 +3588,116 @@ h3.contact-title { gap: 24px; } +/* Spinning green edge (hover). --pc-a is a registered , so the conic + gradient's start angle can be animated — an unregistered custom property + would just jump. The gradient is centred on the mouse (--pc-x/--pc-y from + main.js), so the sweep pivots around the pointer; the card also tilts and + drifts toward it (--pc-rx/ry/mx/my), like the team cards. + Layers behind the content: ::after = the blurred gradient (the glow), + ::before = the card face with a 2px border whose border-box shows the crisp + gradient (padding-box/border-box backgrounds keep the rounded corners). */ +@property --pc-a { + syntax: ''; + inherits: false; + initial-value: 0deg; +} + +@keyframes pc-spin { + to { --pc-a: 360deg; } +} + .price-card { + --pc-face: #fff; + --pc-edge: var(--fitme-surface-2); + --pc-bw: 1px; + --pc-scale: 1; position: relative; + z-index: 0; flex: 0 1 270px; display: flex; flex-direction: column; - background: #fff; - border: 1px solid var(--fitme-surface-2); + background: var(--pc-face); + border: 1px solid transparent; border-radius: 6px; padding: 30px 26px 26px; box-shadow: var(--fitme-shadow); - transition: transform var(--fitme-transition), box-shadow var(--fitme-transition); + transform: perspective(900px) rotateX(var(--pc-rx, 0deg)) rotateY(var(--pc-ry, 0deg)) + translate(var(--pc-mx, 0px), calc(var(--pc-my, 0px) + var(--pc-lift, 0px))) scale(var(--pc-scale)); + transition: transform .45s cubic-bezier(.34, 1.35, .64, 1), box-shadow var(--fitme-transition); +} + +/* While the pointer is on it, follow quickly instead of springing. */ +.price-card.pc-tracking { + transition: transform .15s ease-out, box-shadow var(--fitme-transition); +} + +.price-card::before, +.price-card::after { + content: ''; + position: absolute; + pointer-events: none; +} + +.price-card::before { + inset: calc(-1 * var(--pc-bw)); + z-index: -1; + border-radius: calc(6px + var(--pc-bw)); + border: var(--pc-bw) solid transparent; + background: + linear-gradient(var(--pc-face), var(--pc-face)) padding-box, + linear-gradient(var(--pc-edge), var(--pc-edge)) border-box; +} + +.price-card::after { + inset: -3px; + z-index: -2; + border-radius: 10px; + background: conic-gradient(from var(--pc-a) at var(--pc-x, 50%) var(--pc-y, 50%), + rgba(var(--fitme-accent-rgb), 0) 0deg, rgba(var(--fitme-accent-rgb), 0) 150deg, + var(--fitme-accent) 250deg, #f4ff9c 300deg, var(--fitme-accent) 330deg, + rgba(var(--fitme-accent-rgb), 0) 360deg); + filter: blur(12px); + opacity: 0; + transition: opacity .35s ease; } .price-card:hover { - transform: translateY(-4px); + --pc-lift: -4px; + --pc-bw: 2px; box-shadow: var(--fitme-shadow-lg); } +.price-card:hover::before { + background: + linear-gradient(var(--pc-face), var(--pc-face)) padding-box, + conic-gradient(from var(--pc-a) at var(--pc-x, 50%) var(--pc-y, 50%), + rgba(var(--fitme-accent-rgb), 0) 0deg, rgba(var(--fitme-accent-rgb), 0) 150deg, + var(--fitme-accent) 250deg, #f4ff9c 300deg, var(--fitme-accent) 330deg, + rgba(var(--fitme-accent-rgb), 0) 360deg) border-box; + animation: pc-spin 3.2s linear infinite; +} + +.price-card:hover::after { + opacity: .75; + animation: pc-spin 3.2s linear infinite; +} + .price-card.is-featured { - background: var(--fitme-dark-2); - border: 2px solid var(--fitme-accent); + --pc-face: var(--fitme-dark-2); + --pc-edge: var(--fitme-accent); + --pc-bw: 2px; + border-width: 2px; color: var(--fitme-ash); box-shadow: 0 18px 50px rgba(var(--fitme-dark-rgb), .28); } @media (min-width: 992px) { - .price-card.is-featured { transform: scale(1.04); } - .price-card.is-featured:hover { transform: scale(1.04) translateY(-4px); } + .price-card.is-featured { --pc-scale: 1.04; } +} + +@media (prefers-reduced-motion: reduce) { + .price-card:hover::before, + .price-card:hover::after { animation: none; } } .pc-badge { @@ -3731,9 +3817,25 @@ h3.contact-title { /* ══════════════════════════════════════════════════════════════════════════ Reviews Google ══════════════════════════════════════════════════════════════════════════ */ +/* Matte black "gym machine" metal, shared by the reviews section and the + footer: fine powder-coat grain (light + dark speckle from SVG turbulence), + a soft diffuse sheen like light on a frame tube, and barely-there brushed + streaks. All inline, no image requests. */ +:root { + --metal-layers: + radial-gradient(120% 70% at 22% 0%, rgba(255, 255, 255, .055), transparent 60%), + linear-gradient(112deg, transparent 38%, rgba(255, 255, 255, .03) 50%, transparent 62%), + repeating-linear-gradient(0deg, rgba(255, 255, 255, .012) 0 1px, transparent 1px 3px), + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' 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 .09 -.03'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E"), + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='d'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='1' seed='7' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .22 -.06'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23d)'/%3E%3C/svg%3E"); + --metal-sizes: 100% 100%, 100% 100%, 100% 3px, 180px 180px, 140px 140px; +} + .reviews-section { padding: 90px 0 60px; - background: var(--fitme-dark); + background-color: #141517; + background-image: var(--metal-layers); + background-size: var(--metal-sizes); color: var(--fitme-ash); } diff --git a/site/assets/js/main.js b/site/assets/js/main.js index 44f120d..4c90057 100644 --- a/site/assets/js/main.js +++ b/site/assets/js/main.js @@ -640,6 +640,47 @@ document.addEventListener('DOMContentLoaded', function () { window.addEventListener('resize', function () { closeAll(null); }, { passive: true }); }); +/* ── Mensalidades: cards follow the mouse ─────────────────────────────────── + While hovered, a price card tilts and drifts a few px toward the pointer + (like the team cards) and the spinning conic edge is centred on it + (--pc-x/--pc-y feed the gradient's `at` position). Pointer devices only; + off with reduced motion. */ +document.addEventListener('DOMContentLoaded', function () { + var cards = document.querySelectorAll('.price-card'); + if (!cards.length) return; + var fine = window.matchMedia('(hover: hover) and (pointer: fine)'); + var reduced = window.matchMedia('(prefers-reduced-motion: reduce)'); + var VARS = ['--pc-rx', '--pc-ry', '--pc-mx', '--pc-my', '--pc-x', '--pc-y']; + + cards.forEach(function (card) { + var t; + card.addEventListener('mouseenter', function () { + if (!fine.matches || reduced.matches) return; + clearTimeout(t); + t = setTimeout(function () { card.classList.add('pc-tracking'); }, 350); + }); + card.addEventListener('mousemove', function (e) { + if (!fine.matches || reduced.matches) return; + var r = card.getBoundingClientRect(); + var x = e.clientX - r.left, y = e.clientY - r.top; + var dx = Math.max(-1, Math.min(1, (x - r.width / 2) / (r.width / 2))); + var dy = Math.max(-1, Math.min(1, (y - r.height / 2) / (r.height / 2))); + card.style.setProperty('--pc-ry', (dx * 6).toFixed(2) + 'deg'); + card.style.setProperty('--pc-rx', (-dy * 6).toFixed(2) + 'deg'); + card.style.setProperty('--pc-mx', (dx * 6).toFixed(1) + 'px'); + card.style.setProperty('--pc-my', (dy * 6).toFixed(1) + 'px'); + // The pseudo-elements sit a few px outside the card; close enough. + card.style.setProperty('--pc-x', x.toFixed(0) + 'px'); + card.style.setProperty('--pc-y', y.toFixed(0) + 'px'); + }); + card.addEventListener('mouseleave', function () { + clearTimeout(t); + card.classList.remove('pc-tracking'); // spring back + VARS.forEach(function (v) { card.style.removeProperty(v); }); + }); + }); +}); + /* ── Mensalidades: pre-select the clicked card in the sign-up modal ───────── */ document.addEventListener('DOMContentLoaded', function () { var select = document.querySelector('#joinModal select[name="membership_id"]'); diff --git a/views/sysadmin/system_settings.php b/views/sysadmin/system_settings.php index c97a7d4..da35365 100644 --- a/views/sysadmin/system_settings.php +++ b/views/sysadmin/system_settings.php @@ -17,14 +17,10 @@ $fields = [ 'email_from_name' => ['label' => 'Nome do remetente', 'type' => 'text', 'required' => true], 'email_subject_prefix' => ['label' => 'Prefixo do assunto', 'type' => 'text', 'required' => false], 'reminder_hours' => ['label' => 'Horas de reminder', 'type' => 'number','required' => true], - 'contact_email' => ['label' => 'Email de contacto público', 'type' => 'email', 'required' => false], - 'contact_phone_display'=> ['label' => 'Telefone (texto exibido)', 'type' => 'text', 'required' => false], - 'contact_phone_dial' => ['label' => 'Telefone (formato dial)', 'type' => 'text', 'required' => false], - 'contact_phone_intl' => ['label' => 'Telefone (formato internacional, sem +)', 'type' => 'text', 'required' => false], - 'contact_address_line1'=> ['label' => 'Morada — linha 1', 'type' => 'text', 'required' => false], - 'contact_address_line2'=> ['label' => 'Morada — linha 2', 'type' => 'text', 'required' => false], - 'social_facebook_url' => ['label' => 'Facebook URL', 'type' => 'url', 'required' => false], - 'social_instagram_url' => ['label' => 'Instagram URL', 'type' => 'url', 'required' => false], + // Public contacts are NOT here: the site reads them from fitme3070_info, + // edited in Gestão → Contactos (views/admin/info/edit.php). The fleet + // template's contact_* / social_* settings were unused and got edited by + // mistake, so they were removed from this form. // SEO defaults — used by site/includes/head.php when a page doesn't // set its own $page_title / $page_description / $page_image override. @@ -226,63 +222,10 @@ include __DIR__ . '/../../includes/navbar.php'; -
-
Contactos públicos
-
-
-
- - -
Mostrado no site e recebe os pedidos de orçamento e contacto.
-
-
- - -
-
- - -
Formato E.164 (com +).
-
-
- - -
Sem + nem espaços. Usado em https://wa.me/<numero>.
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
+
+ +
Telefone, WhatsApp, e-mail, morada e redes sociais do site editam-se em + Gestão → Contactos.