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 f28b2b8482 - Show all commits
+99 -46
View File
@@ -358,22 +358,74 @@ section[id] {
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, so the
solid reads as an extruded block viewed from the right. Walls are lit in the
brand colour with a neon glow; the right one is a shade darker (light comes
from the top-left). Only transform animates (compositor-only): lift springs
(overshoot curve), release is slow, press is instant. Colours come from
--fitme-accent, so each gym glows in its own colour. */
(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 */
--wall-hi: color-mix(in srgb, var(--fitme-accent) 45%, #fff); /* hot core of the tube */
--wall: var(--fitme-accent);
--wall-lo: color-mix(in srgb, var(--fitme-accent) 65%, #000);
--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; }
@@ -395,7 +447,7 @@ section[id] {
top: calc(100% + var(--bw));
width: calc(100% + var(--bw) * 2 + var(--depth-x));
height: var(--depth);
background: linear-gradient(to right, var(--wall-lo) 0%, var(--wall) 1%, var(--wall-hi) 50%, var(--wall) 99%, var(--wall-lo) 100%);
background: var(--wall-bottom);
clip-path: polygon(0 0, calc(100% - var(--depth-x)) 0, 100% 100%, var(--depth-x) 100%);
transform: scaleY(0);
}
@@ -407,7 +459,7 @@ section[id] {
top: calc(var(--bw) * -1);
width: var(--depth-x);
height: calc(100% + var(--bw) * 2 + var(--depth));
background: linear-gradient(to bottom, var(--wall) 0%, var(--wall-lo) 100%);
background: var(--wall-right);
clip-path: polygon(0 0, 100% var(--depth), 100% 100%, 0 calc(100% - var(--depth)));
transform: scaleX(0);
}
@@ -418,7 +470,7 @@ section[id] {
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, filter 450ms ease;
background-color 450ms ease, border-color 450ms ease, color 450ms ease;
}
.site-btn::before,
.site-btn::after,
@@ -432,14 +484,10 @@ section[id] {
.gym-switch-btn:hover,
.gym-switch-btn:focus-visible {
transform: translate(calc(var(--depth-x) * -1), calc(var(--depth) * -1));
/* inner glow on the face (the "lit from inside" look) + a halo round the
whole solid; drop-shadow follows the clipped walls, box-shadow can't */
box-shadow: inset 0 0 14px 1px rgba(var(--fitme-accent-rgb), .4),
inset 0 0 3px 0 rgba(var(--fitme-accent-rgb), .6),
0 0 12px 1px rgba(var(--fitme-accent-rgb), .25);
filter: drop-shadow(0 0 5px rgba(var(--fitme-accent-rgb), .9)) drop-shadow(0 0 14px rgba(var(--fitme-accent-rgb), .55));
transition-duration: 250ms, 250ms, 450ms, 450ms, 450ms, 450ms;
transition-timing-function: cubic-bezier(.3, .7, .4, 1.5), cubic-bezier(.3, .7, .4, 1.5), ease, ease, ease, ease;
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,
@@ -451,7 +499,8 @@ section[id] {
.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 again. Opacity + filter only. */
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,
@@ -468,6 +517,8 @@ section[id] {
.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,
@@ -477,31 +528,29 @@ section[id] {
}
@keyframes neon-flicker-tube {
0%, 100% { opacity: 1; }
0%, 30%, 65%, 100% { opacity: 1; }
15% { opacity: .3; }
30% { opacity: 1; }
50% { opacity: .55; }
65% { opacity: 1; }
}
@keyframes neon-flicker-glow {
0%, 100% { filter: drop-shadow(0 0 5px rgba(var(--fitme-accent-rgb), .9)) drop-shadow(0 0 14px rgba(var(--fitme-accent-rgb), .55)); }
15% { filter: drop-shadow(0 0 1px rgba(var(--fitme-accent-rgb), .3)); }
30% { filter: drop-shadow(0 0 5px rgba(var(--fitme-accent-rgb), .9)) drop-shadow(0 0 14px rgba(var(--fitme-accent-rgb), .55)); }
50% { filter: drop-shadow(0 0 2px rgba(var(--fitme-accent-rgb), .45)); }
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, two gradients); ::after stays the existing gloss overlay
and also carries the inner glow. The frame no longer clips its children
(overflow:hidden would cut the wall), the photo fills the padding box anyway. */
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 */
--wall-hi: color-mix(in srgb, var(--fitme-accent) 45%, #fff);
--wall: var(--fitme-accent);
--wall-lo: color-mix(in srgb, var(--fitme-accent) 65%, #000);
--neon-face-on: var(--neon-on);
--neon-face-dim: var(--neon-dim);
--neon-face-off: var(--neon-off);
overflow: visible;
}
@@ -515,10 +564,15 @@ section[id] {
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 right, var(--wall-lo) 0%, var(--wall) 1%, var(--wall-hi) 50%, var(--wall) 99%, var(--wall-lo) 100%)
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 bottom, var(--wall) 0%, var(--wall-lo) 100%)
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%);
@@ -528,23 +582,22 @@ section[id] {
.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: 0 0 12px 1px rgba(var(--fitme-accent-rgb), .25);
filter: drop-shadow(0 0 5px rgba(var(--fitme-accent-rgb), .9)) drop-shadow(0 0 14px rgba(var(--fitme-accent-rgb), .55));
box-shadow: var(--neon-face-on);
}
.trainer-item .trainer-pic:hover::before { opacity: 1; }
.trainer-item .trainer-pic:hover::after {
box-shadow: inset 0 0 14px 1px rgba(var(--fitme-accent-rgb), .4), inset 0 0 3px 0 rgba(var(--fitme-accent-rgb), .6);
}
.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 .45s ease, filter .45s ease; }
.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 —
the bright neon faces around it then stand out against it. The dark and
ghost variants keep their own backgrounds. */
/* 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,
@@ -552,7 +605,7 @@ section[id] {
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 8px rgba(var(--fitme-accent-rgb), .9), 0 0 2px rgba(255, 255, 255, .6); /* neon lettering */
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; }