ui/ashoverlay.razor.scss
// ASH OVERLAY — drifting soot while you stand in a fog area.
//
// ⚠️ Opacity is written from code every frame; only the DRIFT lives here. See the header in
// AshOverlay.razor for why this splits differently from DamageOverlay, which drives both from code.
.ash
{
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
// ⛔ BEHIND THE HUD, like the blood vignette. Ash over a points counter or an ammo readout is
// the same mistake as a health bar you cannot read at low health.
z-index: -1;
// ⛔ CLIPS THE OVERSIZED LAYERS. Each one below is 200% and offset by -50% so it always has
// tile to spare in the direction it drifts; without this they would paint outside the root and
// over whatever sits beside it.
overflow: hidden;
}
.layer
{
position: absolute;
// ⚠️ 200% AND CENTRED, NOT 100%. A layer that exactly covers the screen runs out of texture at
// the moment it has translated a full tile, and the loop shows as a visible jump at the edge.
width: 200%;
height: 200%;
left: -50%;
top: -50%;
background-repeat: repeat;
// Set from code every frame. Starting at 0 means walking into a map never flashes a full
// overlay for the frame before OnUpdate first runs.
opacity: 0;
}
// ⛔ EVERY LAYER TRANSLATES BY EXACTLY ITS OWN background-size. 900 here, 420 and 700 below — the
// keyframe distance and the tile size are the same number on purpose. Any other distance leaves a
// seam crossing the screen once per loop, which reads as a rendering fault rather than as ash.
.soot
{
background-image: url( "ui/nz/ash/nz.ash.soot.png" );
background-size: 900px 900px;
animation: nz-ash-soot 60s linear infinite;
}
.motes-far
{
background-image: url( "ui/nz/ash/nz.ash.motes.png" );
background-size: 420px 420px;
animation: nz-ash-far 34s linear infinite;
}
.motes-near
{
background-image: url( "ui/nz/ash/nz.ash.motes.png" );
// ⚠️ THE SAME TEXTURE AT A BIGGER TILE, not a second texture. Scaling it up is what makes these
// specks read as nearer than the ones behind them, and it costs no extra asset.
background-size: 700px 700px;
animation: nz-ash-near 19s linear infinite;
}
// ⛔ AT FILE SCOPE, NOT NESTED. s&box's SCSS does not hoist a @keyframes out of a selector block —
// nested, the rule compiles and the animation simply never runs, which looks like frozen ash.
// MapBrowser.razor.scss records the same trap.
//
// ⚠️ ALL THREE FALL, AND NONE FALLS STRAIGHT DOWN. Pure vertical reads as rain; the sideways
// component is what makes it drift. They disagree about direction on purpose — three layers sliding
// the same way is one moving picture, not depth.
@keyframes nz-ash-soot
{
0% { transform: translateX( 0px ) translateY( 0px ); }
100% { transform: translateX( 900px ) translateY( 900px ); }
}
@keyframes nz-ash-far
{
0% { transform: translateX( 0px ) translateY( 0px ); }
100% { transform: translateX( -420px ) translateY( 420px ); }
}
@keyframes nz-ash-near
{
0% { transform: translateX( 0px ) translateY( 0px ); }
100% { transform: translateX( -700px ) translateY( 700px ); }
}