This whole site is one stylesheet. No images, no JavaScript, no framework. Here's how each piece of the look works, so you can build your own. If you'd rather start from the finished thing, grab crt.css and adapt it.

1. One font, a few colors

The look comes from restraint: a single monospace face and a short palette defined once as CSS variables. Everything else references the variables, so re-theming means changing a handful of lines.

:root {
  --bg: #000000;
  --fg: #33ff66;        /* body text */
  --fg-dim: #1a8033;    /* borders, metadata, prompts */
  --fg-bright: #aaffaa; /* headings, links */
  --accent: #ff6633;    /* hover and focus only */
  --warn: #ffcc33;      /* tags */
  --mono: 'IBM Plex Mono', 'Courier New', monospace;
}

html, body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--mono);
  font-size: 15px;
  line-height: 1.5;
}

Load the font in your <head>:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&display=swap" rel="stylesheet">

Want amber instead of green? Try --fg: #ffb000, --fg-dim: #805800, --fg-bright: #ffe0a0. Keep the accent a contrasting hue so hover states stand out.

2. Scanlines and vignette

The CRT feel is two fixed overlays drawn with gradients on body's pseudo-elements. A repeating linear gradient makes faint horizontal lines; a radial gradient darkens the corners like a curved tube.

body::before { /* scanlines */
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    0deg,
    rgba(0,0,0,0) 0px,
    rgba(0,0,0,0) 3px,
    rgba(0,0,0,0.08) 4px,
    rgba(0,0,0,0) 5px
  );
  z-index: 100;
}

body::after { /* vignette */
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse at center, transparent 50%, rgba(0,0,0,0.4) 100%);
  z-index: 99;
}

Don't skip pointer-events: none. Without it the overlays sit on top of the page and swallow every click.

3. Shell prompts and a blinking cursor

Prompts are added with ::before, so the HTML stays clean and screen readers aren't fed decoration. The cursor is a block that blinks with a two-step animation, which gives a hard on/off rather than a fade.

.prompt::before {
  content: 'you@host:~$ ';
  color: var(--fg-dim);
}

.cursor {
  display: inline-block;
  width: 0.6em;
  background: var(--fg);
  color: var(--fg);
  animation: blink 1.1s steps(2) infinite;
}
@keyframes blink { 50% { opacity: 0; } }
<p class="prompt">uptime</p>
<p>> system online <span class="cursor" aria-hidden="true">_</span></p>

4. Bracketed nav and comment-style headings

The same pseudo-element trick dresses up navigation and section titles without extra markup:

nav a::before { content: '['; color: var(--fg-dim); }
nav a::after  { content: ']'; color: var(--fg-dim); }

.section-title {
  color: var(--fg-bright);
  text-transform: uppercase;
  letter-spacing: 2px;
}
.section-title::before { content: '// '; color: var(--fg-dim); }

a {
  color: var(--fg-bright);
  text-decoration: none;
  border-bottom: 1px dashed var(--fg-dim);
}
a:hover { color: var(--accent); border-bottom-color: var(--accent); }

Dotted and dashed borders (1px dotted var(--fg-dim)) between list rows finish the printout look.

5. The rotating sigil

The faint spinning shape behind the page is one div with stacked gradients: three thin rings from radial gradients and six spokes from a conic gradient, turning once every four minutes.

.sigil {
  position: fixed;
  right: -160px;
  top: 50%;
  width: 520px;
  height: 520px;
  transform: translateY(-50%);
  pointer-events: none;
  opacity: 0.06;
  z-index: 0;
  border-radius: 50%;
  background:
    radial-gradient(circle, transparent 38%, var(--fg) 39%, var(--fg) 40%, transparent 41%),
    radial-gradient(circle, transparent 28%, var(--fg) 29%, var(--fg) 30%, transparent 31%),
    radial-gradient(circle, transparent 18%, var(--fg) 19%, var(--fg) 20%, transparent 21%),
    conic-gradient(var(--fg) 0deg, transparent 2deg 60deg,
                   var(--fg) 62deg, transparent 64deg 120deg,
                   var(--fg) 122deg, transparent 124deg 180deg,
                   var(--fg) 182deg, transparent 184deg 240deg,
                   var(--fg) 242deg, transparent 244deg 300deg,
                   var(--fg) 302deg, transparent 304deg);
  animation: rotate 240s linear infinite;
}
@keyframes rotate { to { transform: translateY(-50%) rotate(360deg); } }

Put <div class="sigil" aria-hidden="true"></div> just inside <body>, and give your content wrapper position: relative; z-index: 1 so it sits above the sigil and below the scanlines.

6. ASCII art headers

The banner on the home page is generated text, not an image. Install figlet (or pip install pyfiglet) and render your name in the smslant font:

figlet -f smslant YOURNAME

Paste the output into a <pre> and escape any <, > or &. On small screens, shrink it rather than letting it wrap:

header.hero pre { font-size: 12px; line-height: 1.1; white-space: pre; overflow-x: auto; }

@media (max-width: 640px) {
  header.hero pre { font-size: 9px; }
  .sigil { display: none; }
}

7. Keep it usable

  • Give keyboard users a visible focus ring: :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  • Mark decoration (sigil, cursors) aria-hidden="true".
  • Check contrast on your dim color. Dark green on black is the first thing to become unreadable. Use it for borders and metadata, never for body text.
  • If motion bothers you or your readers, wrap the blink and rotate animations in @media (prefers-reduced-motion: no-preference).

That's the whole trick. The full stylesheet, including post and download-table styles, is crt.css. Take it, change the colors, make it yours.

End of transmission.