Design System
The visual language of sriess.eu. Tokens, components, and conventions that keep the site consistent.
Getting Started
Architecture
Static HTML site. No build step, no framework. Every page loads its own CSS stack and shared JS.
File structure
public/
├── index.html Home (hero + cards)
├── about.html About (narrative + timeline)
├── work.html Work overview (card grid)
├── approach.html Approach (numbered steps)
├── contact.html Contact (links + CTA)
├── design-system.html This page (hidden)
├── work/
│ ├── nemetschek.html Case study (process steps + images)
│ ├── casavi.html
│ ├── censhare.html
│ ├── kuka.html
│ └── bmw.html
├── thoughts/
│ ├── culture-eats-ai.html Article (series overview)
│ ├── honesty-problem.html Article (with hero image)
│ └── small-teams-win.html Article
├── css/
│ ├── base.css Reset, variables, typography (load first)
│ ├── layout.css Container, grid, section spacing
│ ├── components.css Header, footer, cards, nav, theme toggle
│ ├── pages.css About, work, contact, case study styles
│ ├── home.css Hero, thoughts section, three-level (index only)
│ ├── effects.css Reveal animation, lightbox
│ ├── articles.css Article header, content, nav (articles only)
│ └── design-system.css This page only
├── js/
│ ├── theme-toggle.js Dark mode (loaded in head, runs immediately)
│ ├── three-level-animate.js Scroll reveal for homepage blocks
│ ├── lightbox.js Image zoom/pan for case studies
│ └── presentation-viewer.js Slide deck overlay (Nemetschek)
└── images/CSS load order
Order matters. Later files override earlier ones. Every page loads base + layout + components. Then add page-specific sheets.
| Page type | CSS stack |
|---|---|
| Homepage | base layout components home effects |
| Standard page (about, work, approach, contact) | base layout components pages effects |
| Case study (work/*) | base layout components pages effects |
| Article (thoughts/*) | base layout components pages effects articles |
Subdirectory pages use ../css/ relative paths. Root pages use css/.
Page Boilerplate
Every page on the site follows this head structure. Copy this as the starting point for any new page.
Root-level page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page Title | Stephan Riess</title>
<meta name="description" content="...">
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/components.css">
<link rel="stylesheet" href="css/pages.css">
<link rel="stylesheet" href="css/effects.css">
<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+Sans:wght@400;500;600&family=IBM+Plex+Serif:wght@400;500&display=swap" rel="stylesheet">
<script defer data-domain="sriess.eu" src="https://plausible.io/js/script.tagged-events.outbound-links.js"></script>
<script src="js/theme-toggle.js"></script>
</head>Subdirectory page (work/, thoughts/)
Only the paths change. CSS and JS use ../css/ and ../js/. Nav links use ../about.html etc. Logo links to ../index.html.
Header (same on every page)
<header class="site-header">
<div class="container">
<a href="index.html" class="site-nav__logo">
<span class="logo-reveal">Stephan</span> Riess
</a>
<nav aria-label="Main navigation">
<ul class="site-nav__list">
<li><a href="work.html" class="nav-link">Work</a></li>
<li><a href="about.html" class="nav-link active">About</a></li>
<li><a href="approach.html" class="nav-link">Approach</a></li>
<li><a href="contact.html" class="nav-link">Contact</a></li>
</ul>
</nav>
</div>
</header>Add active class to the nav-link matching the current page. Homepage gets active on the logo instead. Logo has .logo-reveal: "Riess" is always visible, "Stephan" expands on hover via max-width transition.
Footer (same on every page)
<footer class="site-footer">
<div class="container">
<button class="theme-toggle" aria-label="Toggle dark mode">
<span class="theme-toggle__icon theme-toggle__sun">...svg...</span>
<span class="theme-toggle__icon theme-toggle__moon">...svg...</span>
</button>
<div class="footer-mark">
<svg>...sofa icon...</svg>
<a href="/design-system.html" class="footer-mark__year">2026</a>
</div>
</div>
</footer>Theme toggle shows sun/moon icons. Footer has a dotted blue top border (same pattern as header bottom). The "2026" is a hidden link to this design system page. Sofa SVG matches the homepage hero illustration.
Analytics
Plausible (privacy-friendly, no cookies). Loaded with defer on every public page. Supports tagged events and outbound link tracking. Not included on this design system page.
Page Templates
Five page types. Each follows the same header/footer shell but with different main content structures.
Standard page (about, work, approach, contact)
<main>
<section class="page-header">
<div class="container">
<h1 class="reveal-line">About</h1>
</div>
</section>
<section class="section">
<div class="container">
<!-- Content here -->
</div>
</section>
<div class="section-divider"></div>
<section class="section">
<div class="container">
<!-- More content -->
</div>
</section>
</main>.page-header gives h1 the blue color. .section adds 64px vertical padding. .section-divider renders a gray dotted line between sections. Use .container for max-width content, .container--narrow (or max-width: var(--content-width)) for text-heavy areas.
Homepage (index.html)
<main>
<section class="hero">
<div class="container">
<div class="hero__content">
<h1>
<span class="reveal-line">First line (blue)</span>
<span class="reveal-line">Remaining lines (dark)</span>
</h1>
<p class="hero__tagline reveal-line">Tagline</p>
</div>
</div>
<a href="about.html" class="hero__illustration">
<img src="images/sofa_man.svg" alt="" class="hero__svg">
</a>
</section>
<section class="thoughts-section">
<div class="container">
<p class="section-label">Thoughts</p>
<div class="grid grid--2">
<a href="..." class="card">...</a>
<a href="..." class="card">...</a>
</div>
</div>
</section>
<section class="three-level">
<div class="container">
<p class="section-label">Approach</p>
<div class="three-level__grid">
<!-- 3 blocks with SVG icons -->
</div>
</div>
</section>
</main>Homepage loads home.css instead of pages.css. Hero first line is colored blue via .hero h1 .reveal-line:first-child. The sofa SVG floats (6s ease-in-out infinite). Sections are separated by gray dotted borders on their container tops, not by .section-divider. The three-level section animates blocks on scroll via three-level-animate.js.
Case study (work/*.html)
<main>
<section class="page-header">
<div class="container">
<a href="../work.html" class="back-link">
<span class="back-link__icon">←</span>
Back to Work
</a>
<h1 class="reveal-line">Company Name</h1>
<p class="page-header__subtitle reveal-line">Role or project description</p>
</div>
</section>
<section class="case-study-section">
<div class="container">
<h2>Process</h2>
<div class="process-step">
<div class="process-step__number">01</div>
<div class="process-step__content">
<h3 class="process-step__header">Step Title</h3>
<div class="process-step__body">
<p>Description...</p>
</div>
</div>
</div>
<div class="process-step__media">
<img src="..." class="case-study-image" alt="...">
</div>
</div>
</section>
<section class="case-study-section">
<div class="container">
<h2>My Role</h2>
<div class="case-study-content">
<p>...</p>
</div>
</div>
</section>
</main>Process steps use CSS Grid with named lines (number-start, content-start, content-end). The .process-step class uses display: contents so number and content align to the parent grid. Images go in .process-step__media and align with content column. Alternating sections use .case-study-section--alt for background change. Case studies load lightbox.js for image zoom. Nemetschek also loads presentation-viewer.js.
Article (thoughts/*.html)
<main>
<section class="article-header article-header--with-image">
<div class="container">
<div class="article-header__content">
<a href="culture-eats-ai.html" class="back-link">
<span class="back-link__icon">←</span>
Series overview
</a>
<h1 class="reveal-line">Article title</h1>
<p class="article-header__tagline">Subtitle text</p>
<p class="article-header__dateline">Munich, 25.01.2026</p>
<img src="..." class="article-header__image article-header__image--light" alt="">
<img src="..." class="article-header__image article-header__image--dark" alt="">
</div>
</div>
</section>
<section class="article-section">
<div class="container">
<div class="article-content">
<p>First paragraph (full color).</p>
<p>Subsequent paragraphs (gray).</p>
<h3>Subheading (resets to full color).</h3>
<p>Content after heading (full color).</p>
</div>
</div>
</section>
</main>Articles load articles.css in addition to the standard stack. Hero images support light/dark variants (shown/hidden via [data-theme]). On desktop (>900px), the image floats to the right of the header text. Article content uses a progressive dimming pattern: first paragraph and paragraphs after headings are var(--color-text), all others are var(--color-gray). Articles include .article-nav for previous/next links and .article-sources for citations.
Dark Mode
Theme is toggled via a data-theme="dark" attribute on <html>. The toggle button is in the footer. Preference persists in localStorage under key theme-preference. If no stored preference, falls back to system prefers-color-scheme.
How it works
// theme-toggle.js (loaded in <head>, runs immediately)
// 1. Check localStorage for 'theme-preference'
// 2. If none, check prefers-color-scheme
// 3. Set data-theme on <html>
// 4. On button click: toggle dark/light, save to localStorage
// 5. Listen for system preference changes (auto-switch if no manual override)Script runs before DOM paint to prevent flash of wrong theme. It sets data-theme on document.documentElement immediately. Toggle button setup waits for DOMContentLoaded.
What changes beyond color variables
Most dark mode behavior is automatic via CSS custom properties. But some components have explicit dark overrides:
| Component | Light mode | Dark mode |
|---|---|---|
| Cards | box-shadow: var(--shadow-card) |
No shadow. On hover: outline: 1px solid var(--color-golden) |
| Theme toggle icon | Moon icon visible | Sun icon visible |
| Hero SVG | filter: invert(1) brightness(0.3) (dark outlines) |
filter: brightness(0.85) (light outlines) |
| Hero SVG hover | Golden/lime color shift | Golden/lime color shift (different filter chain) |
| Three-level icons | var(--color-dark) |
var(--color-gray), hover: var(--color-golden) |
| Article hero images | .article-header__image--light shown |
.article-header__image--dark shown |
| Altitude list headings | var(--color-dark) |
var(--color-text), icons turn var(--color-golden) |
CSS pattern
/* Standard: use variables (automatic) */
.card {
background: var(--color-surface);
box-shadow: var(--shadow-card);
}
/* Override: when variables aren't enough */
[data-theme="dark"] .card {
box-shadow: none;
}
[data-theme="dark"] .card:hover {
box-shadow: none;
outline: 1px solid var(--color-golden);
}Rule of thumb: if a component just uses color/shadow/background variables, dark mode is automatic. Only add [data-theme="dark"] selectors when the behavior changes (shadow removed, outline added, element swapped, filter adjusted).
Foundation
Color
All colors are CSS custom properties. They respond to the dark mode toggle automatically.
Palette
Color semantics
Colors have assigned roles. Don't pick by hex value, pick by meaning.
| Token | Role | Used on |
|---|---|---|
--color-primary |
Interactive, brand | Links, buttons, page-header h1, active nav, card icons, dotted header/footer borders |
--color-primary-hover |
Hover state for primary | Button hover background |
--color-secondary |
Hover accent | Link hover color, back-link hover. Never used as a background. |
--color-golden |
Dark mode accent, highlight | Card outline on dark hover, icon hover (three-level, altitude), sofa SVG hover, series current article marker |
--color-tertiary |
Reserved | Defined but currently unused. Available for future success/positive states. |
--color-dark |
Strong text | h2 headings, timeline roles, competency h3, contact intro, hero h1 (non-first line) |
--color-gray |
Secondary text, muted UI | Subtitles, nav links (inactive), footer text, datelines, section labels, card subtitles, dotted section dividers |
--color-light |
Borders, faint fills | Table borders, section borders, process-step numbers (faint large text), swatch borders |
--color-bg |
Page background | Body, CTA sections, competency card fills, button text color on primary bg |
--color-text |
Primary text | Body text, paragraph content, article first paragraphs, process step body |
--color-surface |
Card/panel backgrounds | Cards, competency blocks, case-hero full-width background, header backdrop |
Shadows
| Token | Light | Dark |
|---|---|---|
--shadow-card |
0 2px 8px rgba(0,0,0,0.06) | 0 2px 12px rgba(255,255,255,0.04) |
--shadow-card-hover |
0 8px 24px rgba(0,0,0,0.12) | 0 8px 32px rgba(255,255,255,0.08) |
--shadow-image |
0 2px 8px rgba(0,0,0,0.08) | 0 2px 12px rgba(255,255,255,0.05) |
--shadow-overlay |
rgba(0,0,0,0.9) | rgba(0,0,0,0.95) |
Typography
Type specimens
The quick brown fox
Page titles. One per page. On the homepage the hero uses a lighter weight (400) and a smaller clamp: clamp(1.5rem, 3vw, 2.75rem). Page headers color h1 blue via .page-header h1 { color: var(--color-primary) }.
The quick brown fox
Section headings. Colored var(--color-dark) in most contexts. Standard bottom margin: var(--space-24).
The quick brown fox
Subsections, card titles (.card__title), process step headers. Fixed size, no clamp.
The quick brown fox
Rarely used. Same size as body text but bold. For tertiary headings within case studies or articles.
Good design is as little design as possible. Less, but better, because it concentrates on the essential aspects, and the products are not burdened with non-essentials.
Default paragraph. Set on body element. Articles use line-height: 1.7 for longer reads. Paragraphs have margin-bottom: var(--space-16), last-child resets to 0.
Supplementary text, captions, footer copy, and meta information.
Nav links, card subtitles (.card__subtitle), footer text, datelines, section labels. Also used for the .section-label component (uppercase, letter-spaced).
When you need a softer, editorial feel. Used sparingly as an accent.
Apply with class .serif. For editorial highlights or pull quotes. Never for body text or UI.
HTML heading hierarchy
Each page follows a strict heading hierarchy. No levels are skipped.
<h1>Page title</h1> <!-- One per page -->
<h2>Section heading</h2> <!-- Major sections -->
<h3>Subsection</h3> <!-- Cards, process steps, subsections -->
<h4>Tertiary</h4> <!-- Rare, within case studies -->The homepage hero <h1> uses .reveal-line spans for the staggered animation. Page headers wrap h1 in .page-header for the blue color. Case study section headings are always h2, step names are h3.
Font families
| Token | Stack | Use |
|---|---|---|
--font-sans |
IBM Plex Sans, -apple-system, BlinkMacSystemFont, sans-serif | Primary UI, body text, headings |
--font-serif |
IBM Plex Serif, Georgia, serif | Accents, editorial highlights |
Responsive scaling
| Breakpoint | html font-size | Base px |
|---|---|---|
| Default | 100% | 16px |
| 1440px+ | 112.5% | 18px |
| 1920px+ | 125% | 20px |
| 2560px+ | 137.5% | 22px |
Spacing
8px base grid. All spacing values are multiples of the base unit.
Structure
Layout
Two nested containers control width. Grids handle columns inside them. Everything stacks to a single column below 768px.
Visual overview
.container (max-width: 1200px, padding: 32px)
.container--narrow (max-width: 720px)
.grid--2 (gap: 32px)
.grid--4 (gap: 32px)
Mobile collapse
Below 768px all grids collapse to a single column. Container padding narrows from 32px to 16px.
Desktop: .grid--2
Mobile: stacked
Usage
<!-- Full-width container -->
<div class="container">
<div class="grid grid--2">
<div>Column 1</div>
<div>Column 2</div>
</div>
</div>
<!-- Narrow container for readable text -->
<div class="container container--narrow">
<p>Prose content, max 720px wide.</p>
</div>Container widths by breakpoint
| Token | Default | 1920px+ | 2560px+ |
|---|---|---|---|
--max-width |
1200px | 1680px | 1800px |
--content-width |
720px | 960px | 1000px |
Breakpoints
| Breakpoint | Type | What changes |
|---|---|---|
max-width: 768px |
Mobile | Grids stack to 1 col, container padding 32 → 16px, nav font shrinks |
max-width: 900px |
Tablet | Case study split layouts (.case-study-split) stack vertically |
min-width: 1440px |
Large | html font-size scales to 112.5% (18px base) |
min-width: 1920px |
Wide | Container 1680px, content 960px, font-size 125% |
min-width: 2560px |
Ultra | Container 1800px, content 1000px, font-size 137.5% |
Grid classes
| Class | Columns | Gap | Used for |
|---|---|---|---|
.grid--2 |
2 (1 on mobile) | 32px | Card pairs, thought articles |
.grid--4 |
4 (1 on mobile) | 32px | Competency blocks on about page |
.grid--content |
1fr 1fr (1 on mobile) | 32px | Text beside image layouts |
Border Radius
0 0 32px 0
4px
50%
Motion
Text reveal animation
Headlines animate in on page load using .reveal-line spans. Each line fades up with a staggered delay. The homepage hero has a longer pause after the first line.
<h1>
<span class="reveal-line">First line</span>
<span class="reveal-line">Second line</span>
</h1>CSS: opacity: 0 → 1, translateY(20px) → 0, 0.6s ease. Delays: 0.1s per line (default), hero uses 1.1s+ for lines 2 onwards. Disabled when prefers-reduced-motion: reduce.
Transition tokens
| Token / Value | Duration | Easing | Context |
|---|---|---|---|
--theme-transition |
0.3s | ease | Background, color, border, shadow on theme change |
| Links, buttons | 0.2s | ease | Color changes on hover/focus |
| Cards | 0.2s | ease | Transform + shadow on hover |
| Logo reveal | 0.6s | ease-in-out | Max-width + margin on hover |
| Text reveal | 0.6s | ease | Opacity + translateY on page load |
| Image compare | 0.4s | ease | Opacity crossfade on hover |
| Hero float | 6s | ease-in-out | Gentle vertical oscillation, infinite |
All animations respect prefers-reduced-motion: reduce. When active, reveal animations are instant and floating is disabled.
Components
Components
Button
Links
Section label
Thoughts
Dotted separators
Voice
Writing Style
The site speaks in a human, warm tone. Sentences are short. Every word earns its place. The voice is direct without being blunt, confident without showing off.
Rules
- No em dashes. They signal AI-generated text. Use commas, periods, or restructure the sentence.
- No buzzwords. Say what you mean in plain language.
- Active voice. "I led the team" not "The team was led by me."
- Concise. If a sentence works without a word, remove the word.
- IBM Plex Sans for all UI and body text. Serif only as an accent, used sparingly.
- No filler phrases. "In order to" becomes "to." "At the end of the day" gets deleted.