VSEARCH / src /neon-frame.css
bep40's picture
fix(neon): shrink ring stroke to 1px and halo to 2.5px (half width)
a6f73b7 verified
Raw History Blame Contribute Delete
5.9 kB
/**
* --neon-angle must be REGISTERED to be animatable: an unregistered custom
* property is not a number the engine can interpolate, so a keyframe from 0deg
* to 360deg would flip once at the midpoint instead of sweeping.
*
* `inherits: false` keeps the angle on the ring itself. The tile is transformed
* every frame by the field engine, and an inherited animated property would be
* recomputed against a moving box, which is how the gradient ends up sliding
* relative to the picture it is supposed to be travelling around.
*
* These rules are kept byte-compatible with dist/assets/neon-frame.css, which is
* what the Space loads today on top of the built app.css. The build therefore
* reproduces this frame rather than replacing it, and the override file becomes
* a no-op instead of a divergence between two definitions of the same effect.
*/
@property --neon-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.image-tile { --neon: #22f5d0; --neon-soft: #7ef9ff; --neon-run: 2.6s; }
.image-tile.exact { --relevance-opacity: 1 !important; --relevance-blur: 0px !important; }
/* A featured tile -- the exact answer, or a photograph just added on
vai-avatar -- is never dimmed either. Same reason: it is the thing being
pointed at, and a ring around something at 14% opacity is not a pointer. */
.image-tile.featured { --relevance-opacity: 1 !important; --relevance-blur: 0px !important; }
/* The ring is drawn on `.exact` AND `.featured`. They are not alternatives: the
brief asks for the running neon on the exact result in every query AND on the
newest photographs, so both ring. A featured tile that is also the exact
answer matches both selectors and still renders exactly one ring. */
.image-tile.exact::before,
.image-tile.featured::before {
content: ''; position: absolute; inset: 0; z-index: 2;
border-radius: 2px;
/* A 1px gap between the photo edge and the light keeps the frame legible
against the tile's dark background. */
padding: 1px;
/* `from var(--neon-angle)` is the whole effect. The layer itself never moves:
no transform, so the mask stays welded to the tile's edges and the gradient
is what sweeps. The stops are a white-hot head, a comet tail fading back
round to 250deg, and a clear gap before the head returns -- so the eye
reads one light travelling rather than a ring pulsing in place. */
background: conic-gradient(from var(--neon-angle, 0deg),
var(--neon) 0deg,
var(--neon-soft) 38deg,
#ffffff 70deg,
var(--neon-soft) 102deg,
var(--neon) 148deg,
#22f5d05c 250deg,
#22f5d000 344deg,
var(--neon) 360deg);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask-composite: exclude;
/* Any transform here drags the mask off the frame, which is exactly the bug
this replaced. Stated explicitly so a later edit cannot reintroduce it. */
transform: none;
pointer-events: none;
}
/* The halo: the same sweep from the same angle, spread wider and blurred, so
the light blooms off the edge instead of being cut flat against it. A
pseudo-element can only carry one background, and a neon frame needs both the
stroke and the glow -- hence two layers, one pseudo-element apart. */
.image-tile.exact::after,
.image-tile.featured::after {
content: ''; position: absolute; inset: 0; z-index: 1;
border-radius: 2px;
/* Half the previous padding, halving the ring width. */
padding: 2.5px;
background: conic-gradient(from var(--neon-angle, 0deg),
#22f5d000 0deg,
#22f5d085 62deg,
#b6fdffc4 92deg,
#22f5d05e 138deg,
#22f5d01f 250deg,
#22f5d000 348deg,
#22f5d000 360deg);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask-composite: exclude;
filter: blur(4px);
opacity: .82;
/* No box-shadow animation. A shadow's shape is the tile's shape, so it
breathes in place and can never run along the perimeter -- which is why it
is replaced by the gradient sweep above rather than merely re-timed. */
box-shadow: none;
pointer-events: none;
}
/* The head sweeps one full turn clockwise; the halo sweeps the same turn over
the same duration, so the two re-align every lap instead of drifting apart. */
@keyframes neon-run {
from { --neon-angle: 0deg; }
to { --neon-angle: 360deg; }
}
/*
* Gate the animation behind @supports at-rule(@property).
*
* Without the registration the angle is an untyped token, so `neon-run` would
* jump from 0deg to 360deg at the halfway point -- the ring would blink rather
* than run, which is worse than the plain static ring it replaced. Declaring the
* angle and then animating it unconditionally would put that blink on every
* browser that has not shipped @property. Browsers outside the gate keep the
* gradient, which is a lit neon border with the bright head parked at the top.
*/
@supports at-rule(@property) {
.image-tile.exact::before,
.image-tile.featured::before,
.image-tile.exact::after,
.image-tile.featured::after {
animation: neon-run var(--neon-run) linear infinite;
}
}
/* A featured tile runs faster and thinner, so the newest photographs are
distinguishable from an exact hit by pace as well as by size. */
.image-tile.featured { --neon-run: 2s; }
.image-tile.featured::before { padding: 1.5px; }
/* A slow, low-frequency sweep reads as "lit"; a fast strobe reads as an alarm,
and this outline marks a correct answer rather than an error. */
@media (prefers-reduced-motion: reduce) {
.image-tile.exact::before, .image-tile.exact::after,
.image-tile.featured::before, .image-tile.featured::after { animation: none; }
}