Dark theme, CSS scanlines, Orbitron: retro aesthetics without a single line of JS

I needed a buying guide for portable retro consoles — the kind of page you browse from a couch on a Saturday night before caving in and ordering something on eBay. Static content, no dynamic behaviour, no API. A perfect excuse to write unbounded CSS without having to justify it to a team. The vision: a cyberpunk dark theme, scanlines that echo old CRT screens, retro-futuristic typography. And crucially, no framework, no JavaScript whatsoever.

What I learned building it is that the most visually striking effects don't come from animation libraries — they come from the right combination of CSS gradients, pseudo-elements, and well-structured custom properties. Here's how it works.

Context

The target page: retro-consoles.html. A buying guide covering around twenty portable retro consoles — Game Boy, Analogue Pocket, Miyoo Mini, Anbernic RG35XX and the rest. Each console gets its specs, its price, its pros and cons.

The technical constraint was simple: a static page hosted on the same Apache server as the CV, zero runtime dependencies. The aesthetic goal was more ambitious: recreating the visual atmosphere of a 1980s CRT screen in a modern browser. The cyan accent colour #00d4ff, chosen for its resemblance to phosphor green shifted to electric blue, sets the tone. The rest of the theme follows from that.

The scanlines effect — pure CSS

Scanlines are the most visually recognisable effect and the simplest to implement. The idea: a ::before pseudo-element fixed to the viewport, covering the entire page, with a repeating-linear-gradient that alternates transparent and semi-opaque every two pixels.

body::before {
    content: '';
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: 9999;
    background: repeating-linear-gradient(
        to bottom,
        transparent,
        transparent 1px,
        rgba(0, 0, 0, 0.08) 1px,
        rgba(0, 0, 0, 0.08) 2px
    );
}

Three decisions here that are worth spelling out.

position: fixed, not absolute. The effect needs to overlay all content even when scrolling, like a real physical screen. fixed stays anchored to the viewport.

pointer-events: none. Without this, the pseudo-element absorbs all clicks and the page becomes unusable. A classic oversight with CSS overlays.

Opacity at 0.08 on the dark lines. That number is arbitrary but the result of several iterations. Too strong (0.2+) and the content becomes unreadable. Too weak (0.03) and the effect disappears on light backgrounds — but here the background is dark, so contrast works at low values.

Retro typography: Orbitron, Space Mono, why these choices

Type choices in a themed design account for 70% of the visual identity. Three fonts, three distinct roles:

/* Headings: Orbitron — geometric shapes, angular uppercase */
h1, h2, h3 {
    font-family: 'Orbitron', monospace;
    letter-spacing: 0.05em;
    text-transform: uppercase;
}

/* Prices, technical specs: Space Mono — monospace with personality */
.price,
.spec-value {
    font-family: 'Space Mono', monospace;
}

/* Body copy: DM Sans — readable, neutral, unobtrusive */
body {
    font-family: 'DM Sans', sans-serif;
}

Orbitron for headings: the archetypal retro-futuristic typeface, drawn with geometric shapes and right angles. Works equally well for "GAME BOY" and "ANALOGUE POCKET". The trade-off: it's unreadable at body copy sizes. A heading set in Orbitron at 14px makes you want to close the tab.

Space Mono for numerical data. A monospace designed by Colophon Foundry with more personality than Courier or Roboto Mono. Prices aligned vertically in Space Mono on a dark background recall 1980s terminal screens.

DM Sans for everything else. The rule is simple: never put a display typeface in body copy. Orbitron for 500 words of console descriptions would be cruel.

Custom properties for the entire theme

A coherent dark theme with a single accent colour is best managed through custom properties from the start. Not a single hardcoded colour anywhere in the CSS — everything goes through :root.

:root {
    /* Backgrounds */
    --bg-primary:   #0a0a0f;
    --bg-secondary: #111118;
    --bg-card:      #16161f;
    --bg-card-hover:#1c1c28;

    /* Main accent */
    --accent:       #00d4ff;
    --accent-dim:   rgba(0, 212, 255, 0.15);
    --accent-glow:  0 0 20px rgba(0, 212, 255, 0.3);

    /* Text */
    --text-primary:  #e8e8f0;
    --text-secondary:#9090a8;
    --text-muted:    #5a5a72;

    /* Borders */
    --border:        rgba(0, 212, 255, 0.2);
    --border-strong: rgba(0, 212, 255, 0.5);
}

A few details that matter. --accent-dim: a heavily attenuated version of the accent for hover backgrounds and badges. Without it, you end up recomputing rgba(0, 212, 255, X) all over the stylesheet. --accent-glow: the neon halo on featured elements, defined once as a box-shadow value.

The card hover glow then reduces to:

.console-card:hover {
    background: var(--bg-card-hover);
    border-color: var(--border-strong);
    box-shadow: var(--accent-glow);
    transform: translateY(-2px);
    transition: all 0.2s ease;
}

Responsive layout without a framework

CSS Grid with auto-fill and minmax — the only way to build a truly responsive multi-column layout without a single media query for the cards:

.consoles-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 24px;
}

With minmax(280px, 1fr), the browser calculates how many columns fit. On a 1440px screen: 4 columns. On an iPad at 768px: 2 columns. On a phone: 1 column. No manual breakpoints needed for the grid itself.

Media queries are still needed for other adjustments: reducing Orbitron font-size on mobile (geometric headline fonts are wide), switching the header from two columns to one, adjusting padding.

.page-header {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: 32px;
}

@media (max-width: 768px) {
    .page-header {
        grid-template-columns: 1fr;
    }

    h1 {
        font-size: clamp(1.4rem, 5vw, 2.5rem);
    }
}

clamp() for Orbitron headings: essential. Without it, a 2.5rem h1 in Orbitron overflows the viewport on an iPhone SE.

Conclusion

This project reinforced something I knew in theory but regularly underestimate in practice: a strong visual design doesn't require JavaScript. The card hover animation, the scanlines effect, the neon glow — all of it works with transition, box-shadow, and a pseudo-element. The browser handles the rendering on the GPU, without a single requestAnimationFrame.

The real complexity wasn't technical — it was visual calibration. Finding the scanline opacity that creates atmosphere without hurting legibility. Choosing a cyan luminosity that evokes phosphor without burning eyes. These are decisions that don't get written in code; they get validated by eye.

The result: retro-consoles.html.

Comments (0)