Design System

The visual language of sriess.eu. Tokens, components, and conventions that keep the site consistent.

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">&larr;</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">&larr;</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).


Color

All colors are CSS custom properties. They respond to the dark mode toggle automatically.

Palette

--color-primary Light #50A7F9 / Dark #6BB8FF
--color-primary-hover Light #3d94e6 / Dark #8ac8ff
--color-secondary Light #E8913A / Dark #F5A54A
--color-golden #C8D830
--color-tertiary Light #5DB075 / Dark #6DC484
--color-dark Light #424242 / Dark #E0E0E0
--color-gray Light #A1A1A1 / Dark #9E9E9E
--color-light Light #E5E5E5 / Dark #3A3A3A
--color-bg Light #FAFAFA / Dark #1A1A1A
--color-text Light #1A1A1A / Dark #E8E8E8
--color-surface Light white / Dark #242424

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

h1 · clamp(2rem, 5vw, 3rem) · 600 · line-height: 1.2

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) }.

h2 · clamp(1.5rem, 3vw, 2rem) · 600 · line-height: 1.2

The quick brown fox

Section headings. Colored var(--color-dark) in most contexts. Standard bottom margin: var(--space-24).

h3 · 1.25rem (20px) · 600 · line-height: 1.2

The quick brown fox

Subsections, card titles (.card__title), process step headers. Fixed size, no clamp.

h4 · 1.125rem (18px) · 600 · line-height: 1.2

The quick brown fox

Rarely used. Same size as body text but bold. For tertiary headings within case studies or articles.

body · 1.125rem (18px) · 400 · line-height: 1.6

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.

small / caption · 0.875rem (14px) · 400 · line-height: 1.6

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).

serif accent · IBM Plex Serif · 400

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.

--space-2 2px
--space-4 4px
--space-8 8px
--space-16 16px
--space-24 24px
--space-32 32px
--space-48 48px
--space-64 64px
--space-128 128px

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)

--max-width: 1200px

.container--narrow (max-width: 720px)

--content-width: 720px

.grid--2 (gap: 32px)

.grid--4 (gap: 32px)

768px Mobile
900px Splits stack
1440px Type scales
1920px Wide
2560px Ultra

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

Card
0 0 32px 0
Standard
4px
Circle
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.

Hi, I'm Stephan. I lead design and product teams that solve messy problems.
<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

Section label

Dotted separators

Blue (header, footer)
Gray (section dividers)

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.