/** * --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: ''; 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; } }