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
Showing only changes of commit 8e889adeb9 - Show all commits
-256
View File
@@ -354,262 +354,6 @@ section[id] {
color: var(--fitme-accent);
}
/* ── 3D "pushable" neon button (seen at an angle) ──────────────────────────────
The element itself is the face. On hover it lifts toward the viewer — up by
--depth and left by --depth-x — and two pseudo-elements grow to fill the gap:
::before is the bottom wall, ::after the right wall. Each is a parallelogram
(clip-path) joining the face's edge to where the face used to be; they share
the diagonal from the face's corner, so the join has no gap and no overlap.
Light model (key light top-left, the walls are the neon tubes):
- Glow lives on the walls and spills onto the page under/around them. The
halo is an outer box-shadow offset toward the walls (never painted inside
the face, no drop-shadow, so no ghost copy of the text), blooming in with
the lift and tightening on press.
- Walls: hot near-white core in the middle of their length, saturated brand
colour on the falloff, darkest at the ends; across their thickness bright
where they meet the face, darker at the far edge. The right wall is one
step darker than the bottom; both reach the darkest value (ambient
occlusion) where they meet in the corner.
- Contact shadow: a tight dark line just past the walls' far edges.
- Face: only a faint rim light near the edges, a thin specular line on the
top-left edge, slightly darker toward the bottom/right. Centre untouched.
Colours derive from --fitme-accent with color-mix(), so any gym colour works. */
:where(.site-btn, .gym-switch-btn) { position: relative; }
.site-btn,
.gym-switch-btn,
.trainer-item .trainer-pic {
--wall-hi: color-mix(in srgb, var(--fitme-accent) 40%, #fff); /* hot core of the tube */
--wall: var(--fitme-accent);
--wall-lo: color-mix(in srgb, var(--fitme-accent) 60%, #000);
--wall-ao: color-mix(in srgb, var(--fitme-accent) 30%, #000); /* corner / far ends */
/* bottom wall: across thickness (bright at the face, dark far edge) over along length */
--wall-bottom:
linear-gradient(to bottom, rgba(255, 255, 255, .28) 0%, rgba(255, 255, 255, 0) 40%, rgba(0, 0, 0, .4) 100%),
linear-gradient(to right, var(--wall-lo) 0%, var(--wall) 18%, var(--wall-hi) 48%, var(--wall) 78%, var(--wall-ao) 100%);
/* right wall: one step darker, ends in the same --wall-ao at the corner */
--wall-right:
linear-gradient(to right, rgba(255, 255, 255, .14) 0%, rgba(0, 0, 0, .12) 45%, rgba(0, 0, 0, .5) 100%),
linear-gradient(to bottom, var(--wall-lo) 0%, var(--wall) 22%, color-mix(in srgb, var(--wall-hi) 60%, var(--wall)) 50%, var(--wall-lo) 82%, var(--wall-ao) 100%);
/* halo stacks (outer only): contact shadow, tight bloom, wide ambient spill */
--neon-on:
calc(var(--depth-x) + 1px) calc(var(--depth) + 1px) 2px rgba(0, 0, 0, .45),
calc(var(--depth-x) * .75) var(--depth) 7px 0 rgba(var(--fitme-accent-rgb), .5),
var(--depth-x) calc(var(--depth) * 1.6) 24px 2px rgba(var(--fitme-accent-rgb), .24);
--neon-dim:
calc(var(--depth-x) + 1px) calc(var(--depth) + 1px) 2px rgba(0, 0, 0, .45),
calc(var(--depth-x) * .75) var(--depth) 6px 0 rgba(var(--fitme-accent-rgb), .25),
var(--depth-x) calc(var(--depth) * 1.6) 20px 0 rgba(var(--fitme-accent-rgb), .1);
--neon-off:
calc(var(--depth-x) + 1px) calc(var(--depth) + 1px) 2px rgba(0, 0, 0, .45),
calc(var(--depth-x) * .75) var(--depth) 4px 0 rgba(var(--fitme-accent-rgb), .08),
var(--depth-x) calc(var(--depth) * 1.6) 16px 0 rgba(var(--fitme-accent-rgb), 0);
/* pressed: wall compressed to ~1px, glow pulled in close */
--neon-press:
1px 1px 1px rgba(0, 0, 0, .5),
1px 1px 4px 0 rgba(var(--fitme-accent-rgb), .55),
1px 2px 12px 0 rgba(var(--fitme-accent-rgb), .2);
/* face shading (inset): specular top-left, lit seam bottom-right, shade, faint rim */
--face-light:
inset 1px 1px 0 color-mix(in srgb, var(--wall-hi) 55%, transparent),
inset -1px -1px 0 rgba(var(--fitme-accent-rgb), .5),
inset -3px -4px 8px -2px rgba(0, 0, 0, .55),
inset 0 0 7px -3px rgba(var(--fitme-accent-rgb), .45);
}
.site-btn,
.gym-switch-btn {
--depth: 5px; /* lift (and bottom wall height) */
--depth-x: 4px; /* shift to the left (and right wall width) */
--bw: 0px; /* the element's own border, so the walls start outside it */
--neon-face-on: var(--face-light), var(--neon-on);
--neon-face-dim: var(--face-light), var(--neon-dim);
--neon-face-off: var(--face-light), var(--neon-off);
}
.site-btn.sb-line,
.site-btn.sb-ghost { --bw: 2px; }
.site-btn::before,
.site-btn::after,
.gym-switch-btn::before,
.gym-switch-btn::after {
content: "";
position: absolute;
pointer-events: none;
transform-origin: top left;
}
/* bottom wall */
.site-btn::before,
.gym-switch-btn::before {
left: calc(var(--bw) * -1);
top: calc(100% + var(--bw));
width: calc(100% + var(--bw) * 2 + var(--depth-x));
height: var(--depth);
background: var(--wall-bottom);
clip-path: polygon(0 0, calc(100% - var(--depth-x)) 0, 100% 100%, var(--depth-x) 100%);
transform: scaleY(0);
}
/* right wall */
.site-btn::after,
.gym-switch-btn::after {
left: calc(100% + var(--bw));
top: calc(var(--bw) * -1);
width: var(--depth-x);
height: calc(100% + var(--bw) * 2 + var(--depth));
background: var(--wall-right);
clip-path: polygon(0 0, 100% var(--depth), 100% 100%, 0 calc(100% - var(--depth)));
transform: scaleX(0);
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
.site-btn,
.gym-switch-btn {
will-change: transform;
/* the face colour fades slowly so the change to near-black never "pops" */
transition: transform 600ms cubic-bezier(.3, .7, .4, 1), box-shadow 600ms cubic-bezier(.3, .7, .4, 1),
background-color 450ms ease, border-color 450ms ease, color 450ms ease;
}
.site-btn::before,
.site-btn::after,
.gym-switch-btn::before,
.gym-switch-btn::after {
transition: transform 600ms cubic-bezier(.3, .7, .4, 1);
}
.site-btn:hover,
.site-btn:focus-visible,
.gym-switch-btn:hover,
.gym-switch-btn:focus-visible {
transform: translate(calc(var(--depth-x) * -1), calc(var(--depth) * -1));
box-shadow: var(--neon-face-on);
/* lift springs fast; the bloom eases in a little slower so it grows with the lift */
transition-duration: 250ms, 380ms, 450ms, 450ms, 450ms;
transition-timing-function: cubic-bezier(.3, .7, .4, 1.5), ease-out, ease, ease, ease;
}
.site-btn:hover::before,
.site-btn:focus-visible::before,
.gym-switch-btn:hover::before,
.gym-switch-btn:focus-visible::before { transform: scaleY(1); transition: transform 250ms cubic-bezier(.3, .7, .4, 1.5); }
.site-btn:hover::after,
.site-btn:focus-visible::after,
.gym-switch-btn:hover::after,
.gym-switch-btn:focus-visible::after { transform: scaleX(1); transition: transform 250ms cubic-bezier(.3, .7, .4, 1.5); }
/* Neon blink: after a moment of hovering, one short stutter — like a tube
that isn't quite seated — then steady. Walls (opacity) and halo
(box-shadow) dim on the same keyframe times. */
.site-btn:hover,
.site-btn:focus-visible,
.gym-switch-btn:hover,
.gym-switch-btn:focus-visible { animation: neon-flicker-glow 600ms linear 1.4s 1; }
.site-btn:hover::before,
.site-btn:hover::after,
.site-btn:focus-visible::before,
.site-btn:focus-visible::after,
.gym-switch-btn:hover::before,
.gym-switch-btn:hover::after,
.gym-switch-btn:focus-visible::before,
.gym-switch-btn:focus-visible::after { animation: neon-flicker-tube 600ms linear 1.4s 1; }
.site-btn:active,
.gym-switch-btn:active {
transform: translate(-1px, -1px);
box-shadow: var(--face-light), var(--neon-press);
animation: none;
transition-duration: 34ms;
}
.site-btn:active::before,
.gym-switch-btn:active::before { transform: scaleY(.2); transition-duration: 34ms; }
.site-btn:active::after,
.gym-switch-btn:active::after { transform: scaleX(.25); transition-duration: 34ms; }
}
@keyframes neon-flicker-tube {
0%, 30%, 65%, 100% { opacity: 1; }
15% { opacity: .3; }
50% { opacity: .55; }
}
@keyframes neon-flicker-glow {
0%, 30%, 65%, 100% { box-shadow: var(--neon-face-on); }
15% { box-shadow: var(--neon-face-off); }
50% { box-shadow: var(--neon-face-dim); }
}
/* ── Team photo frames: the same neon extrusion ────────────────────────────────
.trainer-pic is the face. Its ::before draws the L-shaped wall (bottom + right,
one clipped polygon, the same wall gradients); ::after stays the existing
gloss overlay and also carries the face shading. The halo is the frame's
outer box-shadow only (no inset there, the photo covers it). The frame no
longer clips its children (overflow:hidden would cut the wall). */
.trainer-item .trainer-pic {
--depth: 5px;
--depth-x: 4px;
--bw: 4px; /* the frame's own border */
--neon-face-on: var(--neon-on);
--neon-face-dim: var(--neon-dim);
--neon-face-off: var(--neon-off);
overflow: visible;
}
.trainer-item .trainer-pic::before {
content: "";
position: absolute;
left: calc(var(--bw) * -1);
top: calc(var(--bw) * -1);
width: calc(100% + var(--bw) * 2 + var(--depth-x));
height: calc(100% + var(--bw) * 2 + var(--depth));
z-index: -1;
pointer-events: none;
opacity: 0;
/* the two gradient pairs, each sized to its wall's box (bottom strip, right strip) */
background:
linear-gradient(to bottom, rgba(255, 255, 255, .28) 0%, rgba(255, 255, 255, 0) 40%, rgba(0, 0, 0, .4) 100%)
0 100% / calc(100% - var(--depth-x)) var(--depth) no-repeat,
linear-gradient(to right, var(--wall-lo) 0%, var(--wall) 18%, var(--wall-hi) 48%, var(--wall) 78%, var(--wall-ao) 100%)
0 100% / calc(100% - var(--depth-x)) var(--depth) no-repeat,
linear-gradient(to right, rgba(255, 255, 255, .14) 0%, rgba(0, 0, 0, .12) 45%, rgba(0, 0, 0, .5) 100%)
100% 0 / var(--depth-x) 100% no-repeat,
linear-gradient(to bottom, var(--wall-lo) 0%, var(--wall) 22%, color-mix(in srgb, var(--wall-hi) 60%, var(--wall)) 50%, var(--wall-lo) 82%, var(--wall-ao) 100%)
100% 0 / var(--depth-x) 100% no-repeat;
clip-path: polygon(0 calc(100% - var(--depth)), calc(100% - var(--depth-x)) calc(100% - var(--depth)),
calc(100% - var(--depth-x)) 0, 100% var(--depth), 100% 100%, var(--depth-x) 100%);
transition: opacity .25s ease;
}
.trainer-item .trainer-pic:hover,
#powerRotation>div:nth-child(even) .trainer-pic:hover {
transform: rotate(0deg) translate(calc(var(--depth-x) * -1), calc(var(--depth) * -1)) scale(1.03);
box-shadow: var(--neon-face-on);
}
.trainer-item .trainer-pic:hover::before { opacity: 1; }
.trainer-item .trainer-pic:hover::after { box-shadow: var(--face-light); }
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
.trainer-item .trainer-pic { transition: transform .25s cubic-bezier(.3, .7, .4, 1.5), box-shadow .38s ease-out; }
.trainer-item .trainer-pic:hover { animation: neon-flicker-glow 600ms linear 1.4s 1; }
.trainer-item .trainer-pic:hover::before { animation: neon-flicker-tube 600ms linear 1.4s 1; }
}
/* Hover: white text on an almost-black face keeping ~10% of the brand colour
(white on it is >= 15:1 for any brand colour, even pure white or black).
The lettering gets a dark backing under the neon text-shadow so very light
brand colours never wash the letter edges out. The dark and ghost variants
keep their own backgrounds. */
.site-btn:not(.sb-dark):not(.sb-line):not(.sb-ghost):hover,
.site-btn:not(.sb-dark):not(.sb-line):not(.sb-ghost):focus-visible,
.gym-switch-btn:hover,
.gym-switch-btn:focus-visible {
background: color-mix(in srgb, var(--fitme-accent) 10%, #000);
border-color: color-mix(in srgb, var(--fitme-accent) 10%, #000);
color: #fff;
text-shadow: 0 0 1px rgba(0, 0, 0, .8), 0 0 7px rgba(var(--fitme-accent-rgb), .75); /* neon lettering */
}
.site-btn.sb-dark:hover,
.site-btn.sb-line:hover { color: #fff; }
/* ── Bootstrap Nav Overrides ───────────────────────────────────────────────── */
.nav-link {
font-weight: 700;