DEV Community

Cover image for Building NICHLYST: The Architecture of Afterlife — Designing Post-Final UI Decay in CSS
Eugene Kozlovsky
Eugene Kozlovsky

Posted on Originally published at dev.to AI-assisted

Building NICHLYST: The Architecture of Afterlife — Designing Post-Final UI Decay in CSS

Most video games treat the user interface as immortal chrome. The HUD sits above the world, pristine and unaffected by whatever apocalypse unfolds beneath it. The interface is the one thing that never dies.

NICHLYST rejects that premise entirely.

In NICHLYST, you spend forty days cataloguing the final archive of a dead language in a damp basement. The interface is not a sterile heads-up display. It is paper. It is ink on aging fiber. Over forty days of neglect, humidity, and moral erosion, it rots.

The Immortal UI Fallacy

Every game maintains a quiet contract with the player: the interface is sacred ground. Even in survival horror, menus remain immaculate.

NICHLYST breaks that contract. You are an archivist in a basement. The walls sweat. The paper records you handle are already decaying. Why should the interface be exempt from the same entropy that consumes everything else?

By Day 16, when the Postscript phase begins and the player confronts the weight of their choices, the UI degrades. Not through dramatic effects, but through the slow processes that govern real material decay. Ink fades. Borders fray. Text drifts off its baseline.

The Eight Stages of Decomposition

The decay system is organized into eight discrete levels, indexed 0 through 7. Each level corresponds to a range of game days and a specific phase of material deterioration.

Level 0 (Days 1-15, Pristine): Full ink contrast using #EDE6D8 on the near-black base. Solid 1px borders at rgba(125, 114, 99, 0.3). WCAG AAA contrast ratio of 8.4:1. The interface as the archivist first encounters it.

Level 1 (Days 16-18, Subtle): A slight desaturation filter (saturate(0.95)) cools the palette. Secondary elements drop to 90% opacity. The change is barely perceptible, which is precisely the point.

Level 2 (Days 19-21, Noticeable): Desaturation deepens to saturate(0.88). A text-shadow appears on the visitor name, simulating ink bleed. Resource icons dim to 90%. Day counter and mood text fall to 85%.

Level 3 (Days 22-24, Significant): Saturation drops to saturate(0.8). A solid border appears on the screen container. Mood text receives a hairline blur (filter: blur(0.3px)). Choice buttons maintain opacity: 0.95 and filter: none -- interactive elements must never be degraded beyond usability.

Level 4 (Days 25-28, Deep): The Hidden Postscript begins, and with it paper grain. An SVG feTurbulence filter is injected as a ::before pseudo-element, rendering fractal noise at baseFrequency: 0.9 with four octaves, desaturated to grayscale via feColorMatrix, composited at 3% opacity.

Level 5 (Days 29-32, Profound): Saturation falls to saturate(0.6). The content column narrows to max-width: 440px. Letter-spacing increases by 0.3px globally. Mood text shifts from opacity reduction to color substitution: var(--c-3) instead of var(--c-5).

Level 6 (Days 33-36, Ash): Saturation drops to saturate(0.45). Background shifts from var(--c-1) to var(--c-2). Visitor names shift to var(--c-4). Choice buttons gain a glitch overlay on hover: "ZHYVY" (LIVE) flickers via a stepped keyframe animation.

Level 7 (Days 37-40, Dissolution): Terminal afterlife rot. Saturation reaches saturate(0.3). Horizontal scan-lines overlay the viewport via repeating-linear-gradient at 2% opacity. Screen borders pulse in an 8-second cycle. Choice buttons overlay "DYKHAI" (BREATHE) on hover. On Day 40, the interface reaches terminal archival collapse.

Engineering Decay Inside an Immutable 5-Color Palette

The hardest constraint was not the decay system itself. It was the color palette.

NICHLYST enforces a strict 5-color palette at build time. The script scripts/check_colors.sh scans every CSS, JS, and HTML file, extracts all hex color values, and compares them against a whitelist:

#1A1C1C  #2D3131  #7D7263  #BCB2A1  #EDE6D8
Enter fullscreen mode Exit fullscreen mode

Any unauthorized color -- blood-red warning hues, rusty oranges, sepia browns -- triggers an immediate commit rejection. The palette is the game's visual identity. We cannot break it to make rot look more dramatic.

So how do you simulate decomposition when you cannot add new colors? The answer is CSS math.

Opacity Reduction: Early levels reduce opacity on secondary elements. Level 1 drops day counters to 90%. Level 3 drops mood text to 80%. This is safe because the underlying tokens already yield 12.5:1 contrast (WCAG AAA), so even at 80% opacity the effective contrast remains well above 4.5:1.

Color-Shifting via Token Substitution: At Level 5, instead of reducing opacity further, the system swaps token references. Mood text shifts from var(--c-5) to var(--c-3). No new colors are introduced. The system reuses existing palette members to simulate ink dilution.

Dynamic Contrast Enforcement: The UIDecayManager includes a WCAG contrast guard that activates at Level 5 and above. It calculates effective luminance of desaturated text against the background:

const textRgb = _hexToRgb('#EDE6D8');   // --c-5
const bgRgb = _hexToRgb('#1A1C1C');     // --c-1
const grayPoint = Math.round(
  0.299 * textRgb[0] + 0.587 * textRgb[1] + 0.114 * textRgb[2]
);
const desatText = textRgb.map((c) =>
  Math.round(c * satFilter + grayPoint * (1 - satFilter))
);
const ratio = _contrastRatio(desatText, bgRgb);
Enter fullscreen mode Exit fullscreen mode

If the ratio drops below 4.5:1, a brightness boost is injected as --nl-decay-text-safe, applied via !important override on all text elements. The decay becomes aggressive. The text remains readable.

SVG feTurbulence for Paper Grain: At Level 4, fractal noise is injected as an inline SVG data URI in a ::before pseudo-element using feTurbulence with baseFrequency="0.9" and numOctaves="4". At Level 7, scan lines appear via ::after using repeating-linear-gradient.

Letter-Spacing Drift: Level 5 introduces letter-spacing: 0.3px globally, simulating ink spreading on damp paper. The characters drift apart, as if the fibers holding them in place are relaxing.

The complete CSS cascade for representative decay transitions:

.decay-3 { filter: saturate(0.8); }
.decay-3 .ds-screen {
  border: 1px solid rgba(125, 114, 99, 0.4);
}
.decay-3 .ds-mood-text {
  filter: blur(0.3px); opacity: 0.8;
}

.decay-5 { filter: saturate(0.6); letter-spacing: 0.3px; }
.decay-5 .ds-mood-text { color: var(--c-3); filter: none; }
.decay-5 .ds-day-counter { color: var(--c-3); }

.decay-7 { filter: saturate(0.3); background-color: var(--c-1); }
.decay-7 .ds-screen {
  border-color: rgba(125, 114, 99, 0.15);
  animation: borderFlicker 8s ease-in-out infinite;
}
.decay-7 .ds-body {
  text-shadow: 0 0 1px rgba(237, 230, 216, 0.15);
}
Enter fullscreen mode Exit fullscreen mode

Every property references existing palette tokens. The decay is real, but the palette is untouched.

Mitigating Mobile WebView GPU Choke

The original implementation chained CSS filters: filter: blur(1px) drop-shadow(0 0 4px rgba(0,0,0,0.3)). On desktop, trivial. On a budget Android WebView, catastrophic.

The Redmi Note 9 Pro thermal-throttles within 90 seconds when chaining multiple CSS filter primitives. The GPU cannot rasterize blur() and drop-shadow() simultaneously at 60fps on a Snapdragon 720G. The frame rate collapses to 15fps.

The architectural trade-off: replace dynamic CSS filters with static SVG noise tiles and pre-calculated opacity steps.

The paper grain at Level 4 is an inline SVG data URI rendered once as a ::before pseudo-element with pointer-events: none. The browser composites it as a single texture layer. Scan lines at Level 7 use repeating-linear-gradient, a static paint operation the GPU caches immediately.

On mobile, the @media (max-width: 480px) block reduces noise overlay opacity from 3% to 2% and trims letter-spacing from 0.3px to 0.2px, preventing the compositor from accumulating state across frames.

Result: 60fps locked on the Redmi Note 9 Pro, no thermal throttling on an AMD A4 laptop. The decay feels heavy. The rendering is lightweight.

The Somatic Impact of Visual Erosion

The decay system is not decorative. It is narrative machinery.

When the "CONTINUE" button on Day 35 overlays "BREATHE" in a flickering glitch, that is not visual polish. That is the interface speaking in a voice that is no longer the archivist's own. The paper is speaking. The archive is speaking. And what it says is: keep going, even though everything is dissolving.

Watching resource bars fade toward illegibility creates psychological tension faster than exposition. The player does not need to be told their situation is deteriorating. They can see it in the menu. The ink is running. The borders are fraying. The interface they trusted is becoming unreliable.

The UI is not separate from the story. It is a character in the story. And like every character in NICHLYST, it is subject to the same entropy, the same forty days, the same inevitable collapse.

Countdown to Production

With the September 30 Devpost deadline approaching, the decay system represents the final major technical milestone for the visual layer. The 14-day Closed Testing window on Google Play is underway, and the CSS architecture described here runs in production builds.

Every decay level has been tested against the 5-color palette constraint. Every opacity value verified against WCAG AA 4.5:1 contrast minimums. Every animation respects prefers-reduced-motion. The system is not a prototype. It is a production-ready framework built on CSS custom properties, SVG filters, and mathematical constraints.

Top comments (0)