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.