style(site): lighting model for the 3D neon buttons (AO, graded walls, bloom, shaded face)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
andreandClaude Sonnet 5.5 committed 2026-10-13 18:29:34 +01:00
1 parent 4b99991c11
commit f28b2b8482
1 file changed
+99 -46
+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 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: --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 ::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 (clip-path) joining the face's edge to where the face used to be; they share
solid reads as an extruded block viewed from the right. Walls are lit in the the diagonal from the face's corner, so the join has no gap and no overlap.
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 Light model (key light top-left, the walls are the neon tubes):
(overshoot curve), release is slow, press is instant. Colours come from - Glow lives on the walls and spills onto the page under/around them. The
--fitme-accent, so each gym glows in its own colour. */ 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; } :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, .site-btn,
.gym-switch-btn { .gym-switch-btn {
--depth: 5px; /* lift (and bottom wall height) */ --depth: 5px; /* lift (and bottom wall height) */
--depth-x: 4px; /* shift to the left (and right wall width) */ --depth-x: 4px; /* shift to the left (and right wall width) */
--bw: 0px; /* the element's own border, so the walls start outside it */ --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 */ --neon-face-on: var(--face-light), var(--neon-on);
--wall: var(--fitme-accent); --neon-face-dim: var(--face-light), var(--neon-dim);
--wall-lo: color-mix(in srgb, var(--fitme-accent) 65%, #000); --neon-face-off: var(--face-light), var(--neon-off);
} }
.site-btn.sb-line, .site-btn.sb-line,
.site-btn.sb-ghost { --bw: 2px; } .site-btn.sb-ghost { --bw: 2px; }
@@ -395,7 +447,7 @@ section[id] {
top: calc(100% + var(--bw)); top: calc(100% + var(--bw));
width: calc(100% + var(--bw) * 2 + var(--depth-x)); width: calc(100% + var(--bw) * 2 + var(--depth-x));
height: var(--depth); 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%); clip-path: polygon(0 0, calc(100% - var(--depth-x)) 0, 100% 100%, var(--depth-x) 100%);
transform: scaleY(0); transform: scaleY(0);
} }
@@ -407,7 +459,7 @@ section[id] {
top: calc(var(--bw) * -1); top: calc(var(--bw) * -1);
width: var(--depth-x); width: var(--depth-x);
height: calc(100% + var(--bw) * 2 + var(--depth)); 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))); clip-path: polygon(0 0, 100% var(--depth), 100% 100%, 0 calc(100% - var(--depth)));
transform: scaleX(0); transform: scaleX(0);
} }
@@ -418,7 +470,7 @@ section[id] {
will-change: transform; will-change: transform;
/* the face colour fades slowly so the change to near-black never "pops" */ /* 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), 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::before,
.site-btn::after, .site-btn::after,
@@ -432,14 +484,10 @@ section[id] {
.gym-switch-btn:hover, .gym-switch-btn:hover,
.gym-switch-btn:focus-visible { .gym-switch-btn:focus-visible {
transform: translate(calc(var(--depth-x) * -1), calc(var(--depth) * -1)); 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 box-shadow: var(--neon-face-on);
whole solid; drop-shadow follows the clipped walls, box-shadow can't */ /* lift springs fast; the bloom eases in a little slower so it grows with the lift */
box-shadow: inset 0 0 14px 1px rgba(var(--fitme-accent-rgb), .4), transition-duration: 250ms, 380ms, 450ms, 450ms, 450ms;
inset 0 0 3px 0 rgba(var(--fitme-accent-rgb), .6), transition-timing-function: cubic-bezier(.3, .7, .4, 1.5), ease-out, ease, ease, ease;
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;
} }
.site-btn:hover::before, .site-btn:hover::before,
.site-btn:focus-visible::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); } .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 /* 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:hover,
.site-btn:focus-visible, .site-btn:focus-visible,
.gym-switch-btn:hover, .gym-switch-btn:hover,
@@ -468,6 +517,8 @@ section[id] {
.site-btn:active, .site-btn:active,
.gym-switch-btn:active { .gym-switch-btn:active {
transform: translate(-1px, -1px); transform: translate(-1px, -1px);
box-shadow: var(--face-light), var(--neon-press);
animation: none;
transition-duration: 34ms; transition-duration: 34ms;
} }
.site-btn:active::before, .site-btn:active::before,
@@ -477,31 +528,29 @@ section[id] {
} }
@keyframes neon-flicker-tube { @keyframes neon-flicker-tube {
0%, 100% { opacity: 1; } 0%, 30%, 65%, 100% { opacity: 1; }
15% { opacity: .3; } 15% { opacity: .3; }
30% { opacity: 1; }
50% { opacity: .55; } 50% { opacity: .55; }
65% { opacity: 1; }
} }
@keyframes neon-flicker-glow { @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)); } 0%, 30%, 65%, 100% { box-shadow: var(--neon-face-on); }
15% { filter: drop-shadow(0 0 1px rgba(var(--fitme-accent-rgb), .3)); } 15% { box-shadow: var(--neon-face-off); }
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% { box-shadow: var(--neon-face-dim); }
50% { filter: drop-shadow(0 0 2px rgba(var(--fitme-accent-rgb), .45)); }
} }
/* ── Team photo frames: the same neon extrusion ──────────────────────────────── /* ── Team photo frames: the same neon extrusion ────────────────────────────────
.trainer-pic is the face. Its ::before draws the L-shaped wall (bottom + right, .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 one clipped polygon, the same wall gradients); ::after stays the existing
and also carries the inner glow. The frame no longer clips its children gloss overlay and also carries the face shading. The halo is the frame's
(overflow:hidden would cut the wall), the photo fills the padding box anyway. */ 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 { .trainer-item .trainer-pic {
--depth: 5px; --depth: 5px;
--depth-x: 4px; --depth-x: 4px;
--bw: 4px; /* the frame's own border */ --bw: 4px; /* the frame's own border */
--wall-hi: color-mix(in srgb, var(--fitme-accent) 45%, #fff); --neon-face-on: var(--neon-on);
--wall: var(--fitme-accent); --neon-face-dim: var(--neon-dim);
--wall-lo: color-mix(in srgb, var(--fitme-accent) 65%, #000); --neon-face-off: var(--neon-off);
overflow: visible; overflow: visible;
} }
@@ -515,10 +564,15 @@ section[id] {
z-index: -1; z-index: -1;
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
/* the two gradient pairs, each sized to its wall's box (bottom strip, right strip) */
background: 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, 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; 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)), 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%); 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, .trainer-item .trainer-pic:hover,
#powerRotation>div:nth-child(even) .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); 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); box-shadow: var(--neon-face-on);
filter: drop-shadow(0 0 5px rgba(var(--fitme-accent-rgb), .9)) drop-shadow(0 0 14px rgba(var(--fitme-accent-rgb), .55));
} }
.trainer-item .trainer-pic:hover::before { opacity: 1; } .trainer-item .trainer-pic:hover::before { opacity: 1; }
.trainer-item .trainer-pic:hover::after { .trainer-item .trainer-pic:hover::after { box-shadow: var(--face-light); }
box-shadow: inset 0 0 14px 1px rgba(var(--fitme-accent-rgb), .4), inset 0 0 3px 0 rgba(var(--fitme-accent-rgb), .6);
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) { @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 { animation: neon-flicker-glow 600ms linear 1.4s 1; }
.trainer-item .trainer-pic:hover::before { animation: neon-flicker-tube 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 — /* 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 (white on it is >= 15:1 for any brand colour, even pure white or black).
ghost variants keep their own backgrounds. */ 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):hover,
.site-btn:not(.sb-dark):not(.sb-line):not(.sb-ghost):focus-visible, .site-btn:not(.sb-dark):not(.sb-line):not(.sb-ghost):focus-visible,
.gym-switch-btn:hover, .gym-switch-btn:hover,
@@ -552,7 +605,7 @@ section[id] {
background: color-mix(in srgb, var(--fitme-accent) 10%, #000); background: color-mix(in srgb, var(--fitme-accent) 10%, #000);
border-color: color-mix(in srgb, var(--fitme-accent) 10%, #000); border-color: color-mix(in srgb, var(--fitme-accent) 10%, #000);
color: #fff; 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-dark:hover,
.site-btn.sb-line:hover { color: #fff; } .site-btn.sb-line:hover { color: #fff; }