Sichtbares Raster (Swiss/Blueprint-Linien)
Hero eines Leipziger Markenbüros auf einem sichtbaren 12-Spalten-Achsraster: Linien zeichnen sich, ein Fadenkreuz meldet Achse und Zeile, drei Varianten von Blueprint bis Punktmatrix.
Das Spaltenraster der Seite wird sichtbar: feine 1px-Linien für Spalten und Sektionen, oft mit kleinen Plus-Markern an den Schnittpunkten. Inhalte rasten exakt auf diese Linien ein. Zellen werden durch Linien abgegrenzt, nicht durch Karten mit Schatten. Dazu kommen technische Beschriftungen in Mono-Schrift: Indexnummern, Achsen, Koordinaten, Abschnittscodes. Beim Hover füllt sich eine Rasterzelle oder ihre Linien leuchten auf.
Die Seite zeigt offen, wie sie konstruiert ist, wie eine technische Zeichnung oder ein Schweizer Plakat. Das wirkt präzise, ehrlich und hochwertig, ganz ohne Dekoration.
Wann einsetzen
Tech- und Entwicklerprodukte, Architektur und Ingenieurwesen, Studios mit systematischer Haltung, Dokumentationen, typolastige Portfolios.
Wann nicht
Bei warmen, emotionalen Marken (Kinder, Wellness, Food). Und immer dann, wenn das Raster nicht konsequent eingehalten wird: Sichtbare Linien entlarven jede Ungenauigkeit.
Varianten
- Blueprint-Linien mit Plus-Markern an den Kreuzungen (Vercel-Stil)
- Punktmatrix als Hintergrund
- Swiss-Raster mit sichtbaren Spaltenlinien und großer Grotesk
- Nur Zeilenlinien für Listen und Tabellen
- Extrem breites Mehrspalten-Raster als Experimentier-Vorlage
Umsetzung
Linien nicht als Hintergrundbild über alles legen, sondern als Kanten der Rasterzellen (border-inline-start auf Spalten, border-block auf Sektionen). Dann bleiben sie bei jedem Umbruch richtig. Feine Spalten-Hilfslinien kommen als Overlay mit repeating-linear-gradient im selben Spaltenmaß dazu (CSS-Variablen --cols, --gutter). Plus-Marker als ::before und ::after oder als kleine SVGs an den Schnittpunkten. subgrid hält verschachtelte Inhalte auf den Hauptlinien. Ein Akzent, sonst Schwarz und Weiß, Mono-Schrift für Labels. Tailwind nutzt 1px-Linien mit 5 % Schwarz und schraffierte Stege (315°, 10 × 10 px). Soll sich das Raster zeichnen, die Kanten als linear-gradient-Hintergrund anlegen und deren Größe über @property animieren.
Robust umsetzen
Telefon. Raster auf 2 bis 4 Spalten reduzieren, die Linien bleiben. Plus-Marker nur an den Sektionskanten. Hover-Effekte werden zu Tippen.
Ohne Bewegung. Unkritisch, das Raster ist statisch. Linien-Aufbau und weiche Folgebewegungen weglassen.
Barrierefrei. Linien sind dekorativ und gehören ins CSS, nicht in den DOM. Text braucht ausreichend Kontrast zum Linienmuster. Linien nie als einzigen Trenner für Bedeutung nutzen, Tabellenspalten etwa zusätzlich mit Überschriften.
Code
<!DOCTYPE html>
<!--
Sichtbares Raster (Swiss/Blueprint-Linien)
Fundus · Hero & Section-Layouts · https://cecodesign.de/lab/fundus/hero-layouts/blueprint-raster-sichtbar/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sichtbares Raster (Swiss/Blueprint-Linien) · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Instrument Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-sans:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-display: swap; }
@font-face { font-family: 'Instrument Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-serif@latest/latin-400-italic.woff2') format('woff2'); font-weight: 400; font-style: italic; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
/* Animierbare Strichlängen und Farben (für den Linien-Aufbau und die Schraffur) */
@property --kx { syntax: '<percentage>'; inherits: false; initial-value: 100%; }
@property --ky { syntax: '<percentage>'; inherits: false; initial-value: 100%; }
@property --zieh { syntax: '<percentage>'; inherits: false; initial-value: 130%; }
@property --schraffur { syntax: '<color>'; inherits: false; initial-value: transparent; }
:root {
--kurve: cubic-bezier(0.16, 1, 0.3, 1); /* expo.out für den Aufbau */
--ui: 150ms; /* Hover, Schraffur, Fadenkreuz */
--zeichnen: 1200ms; /* eine Zellkante */
--versatz: 60ms; /* je Zelle */
--cols: 12; /* Spaltenzahl, Linien und Raster teilen dieses Maß */
--pad: clamp(12px, 1.35vw, 20px); /* Innenabstand jeder Zelle */
--steg: clamp(16px, 4vw, 56px); /* schraffierte Randstege */
}
/* Variante 1: Blueprint (Vercel/Tailwind): Plus-Marker, Hilfslinien 5 %, schraffierte Stege */
[data-v='blueprint'] {
--grund: #f6f5f1; --tinte: #111214; --leise: rgba(17, 18, 20, .56);
--kante-h: rgba(17, 18, 20, .13); --kante-v: rgba(17, 18, 20, .13);
--hilfe: rgba(17, 18, 20, .05); --muster: rgba(17, 18, 20, .05);
--marke: rgba(17, 18, 20, .6); --akzent: #2a4fe8; --schraffur-an: rgba(42, 79, 232, .24); --fuell: rgba(42, 79, 232, .035);
}
/* Variante 2: Swiss: sichtbare Spaltenlinien, kräftige Zeilenlinien, große Grotesk, ein Rot */
[data-v='swiss'] {
--grund: #f2f0ea; --tinte: #121212; --leise: rgba(18, 18, 18, .58);
--kante-h: rgba(18, 18, 18, .85); --kante-v: transparent;
--hilfe: rgba(18, 18, 18, .12); --muster: transparent;
--marke: transparent; --akzent: #e1301a; --schraffur-an: rgba(225, 48, 26, .3); --fuell: rgba(225, 48, 26, .04);
}
/* Variante 3: Punktmatrix auf Lichtpause-Blau */
[data-v='matrix'] {
--grund: #0f2a4d; --tinte: #edf1f7; --leise: rgba(237, 241, 247, .62);
--kante-h: rgba(237, 241, 247, .22); --kante-v: rgba(237, 241, 247, .22);
--hilfe: transparent; --punkt: rgba(237, 241, 247, .3); --muster: rgba(237, 241, 247, .07);
--marke: rgba(237, 241, 247, .75); --akzent: #8fd8ff; --schraffur-an: rgba(143, 216, 255, .32); --fuell: rgba(143, 216, 255, .05);
}
* { box-sizing: border-box; margin: 0; }
html { font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { background: var(--grund); color: var(--tinte); transition: background-color 600ms var(--kurve), color 600ms var(--kurve); }
button { font: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }
.mono { font: 500 11px/1.3 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
em { font-family: 'Instrument Serif', Georgia, serif; font-weight: 400; font-style: italic; letter-spacing: -.01em; }
/* ---------- Bühne: Stege links und rechts, dazwischen das Blatt ---------- */
.buehne {
min-height: 100svh; display: grid;
grid-template-columns: var(--steg) minmax(0, 1fr) var(--steg);
grid-template-rows: auto 1fr auto;
/* Schraffur wie bei Tailwind: 315°, 1px Strich, Kachel 10 × 10 px, 5 % */
background: repeating-linear-gradient(315deg, var(--muster) 0 1px, transparent 0 50%) 0 0 / 10px 10px fixed;
}
.blatt { grid-column: 2; grid-row: 1 / 4; display: grid; grid-template-rows: auto 1fr auto; position: relative; background: var(--grund); transition: background-color 600ms var(--kurve); min-height: 100svh; }
/* Achsen-Lineal: dieselben Spalten wie das Raster */
.lineal { display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); height: 30px; align-items: end; position: relative; }
.lineal span { padding: 0 0 7px 6px; font: 500 10px/1 'Geist Mono', monospace; letter-spacing: .08em; color: var(--leise); transition: color var(--ui) linear; }
.lineal span.an { color: var(--akzent); }
.lineal span::before { content: ''; display: inline-block; width: 4px; height: 4px; margin-right: 5px; border-radius: 50%; background: currentColor; opacity: 0; transform: translateY(-1px); transition: opacity var(--ui) linear; }
.lineal span.an::before { opacity: 1; }
/* ---------- Das Raster ---------- */
.raster {
--kx: 100%; --ky: 100%;
position: relative; display: grid; min-height: 0;
grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
grid-template-rows: auto 1fr auto;
}
/* Hilfslinien im Spaltenmaß (dekorativ, nicht im DOM): Zeichnen per Maske */
.raster::before {
content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 0;
background: repeating-linear-gradient(to right, var(--hilfe) 0 1px, transparent 1px calc(100% / var(--cols)));
-webkit-mask: linear-gradient(172deg, #000 calc(var(--zieh) - 30%), transparent var(--zieh));
mask: linear-gradient(172deg, #000 calc(var(--zieh) - 30%), transparent var(--zieh));
transition: --zieh 1400ms var(--kurve);
}
[data-v='matrix'] .raster::before {
/* Punktmatrix: ein Punkt auf jeder halben Spalte, 24 px Zeilenabstand, beginnt auf der Spaltenlinie */
background: radial-gradient(circle at 1px 1px, var(--punkt) 1px, transparent 1.4px) 0 0 / calc(100% / var(--cols) / 2) 24px;
}
/* Rechte und untere Außenkante */
.raster::after {
content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 2;
background:
linear-gradient(var(--kante-v), var(--kante-v)) 100% 0 / 1px var(--ky) no-repeat,
linear-gradient(var(--kante-h), var(--kante-h)) 0 100% / var(--kx) 1px no-repeat;
transition: --kx var(--zeichnen) var(--kurve) 300ms, --ky var(--zeichnen) var(--kurve) 300ms;
}
[data-v='swiss'] .raster::after { background: linear-gradient(var(--kante-h), var(--kante-h)) 0 100% / var(--kx) 1px no-repeat; }
/* Jede Zelle zeichnet ihre obere und linke Kante selbst: Linien bleiben bei jedem Umbruch richtig */
.zelle {
--i: 0;
position: relative; z-index: 1; min-width: 0;
padding: var(--pad);
background:
linear-gradient(var(--kante-h), var(--kante-h)) 0 0 / var(--kx) 1px no-repeat,
linear-gradient(var(--kante-v), var(--kante-v)) 0 0 / 1px var(--ky) no-repeat,
repeating-linear-gradient(315deg, var(--schraffur) 0 1px, transparent 0 50%) 0 0 / 10px 10px;
transition:
--kx var(--zeichnen) var(--kurve) calc(var(--i) * var(--versatz)),
--ky var(--zeichnen) var(--kurve) calc(var(--i) * var(--versatz)),
--schraffur var(--ui) linear, background-color var(--ui) linear, outline-color var(--ui) linear;
outline: 1px solid transparent; outline-offset: -1px;
}
.zelle > * { position: relative; transition: opacity 900ms var(--kurve) calc(300ms + var(--i) * var(--versatz)), translate 900ms var(--kurve) calc(300ms + var(--i) * var(--versatz)); }
/* Hover: Zelle wird schraffiert, Kanten leuchten im Akzent */
.zelle.an { --schraffur: var(--schraffur-an); background-color: var(--fuell); outline-color: var(--akzent); z-index: 3; }
/* Plus-Marker an den Schnittpunkten (Vercel-Stil), 11 px */
.plus-l::before, .plus-r::after, .lineal::before, .lineal::after, .blatt::before, .blatt::after, .w2::after {
content: ''; position: absolute; z-index: 4; width: 11px; height: 11px; pointer-events: none;
background: linear-gradient(var(--marke), var(--marke)) 50% 50% / 100% 1px no-repeat, linear-gradient(var(--marke), var(--marke)) 50% 50% / 1px 100% no-repeat;
transition: scale 700ms var(--kurve) calc(500ms + var(--i, 0) * var(--versatz)), rotate 700ms var(--kurve) calc(500ms + var(--i, 0) * var(--versatz));
}
.plus-l::before { left: -5px; top: -5px; }
.plus-r::after, .w2::after { right: -6px; top: -5px; }
.w2:not(.plus-r)::after { display: none; }
.lineal::before { left: -5px; bottom: -5px; }
.lineal::after { right: -6px; bottom: -5px; }
.blatt::before { left: -5px; bottom: calc(var(--fuss-h, 44px) - 6px); }
.blatt::after { right: -6px; bottom: calc(var(--fuss-h, 44px) - 6px); }
[data-v='matrix'] .plus-l::before, [data-v='matrix'] .plus-r::after, [data-v='matrix'] .lineal::before, [data-v='matrix'] .lineal::after, [data-v='matrix'] .blatt::before, [data-v='matrix'] .blatt::after, [data-v='matrix'] .w2::after {
width: 7px; height: 7px; background: var(--grund); border: 1px solid var(--marke); border-radius: 50%;
}
[data-v='matrix'] .plus-l::before { left: -3px; top: -3px; }
[data-v='matrix'] .plus-r::after, [data-v='matrix'] .w2::after { right: -4px; top: -3px; }
[data-v='matrix'] .lineal::before { left: -3px; bottom: -3px; }
[data-v='matrix'] .lineal::after { right: -4px; bottom: -3px; }
[data-v='matrix'] .blatt::before { left: -3px; bottom: calc(var(--fuss-h, 44px) - 4px); }
[data-v='matrix'] .blatt::after { right: -4px; bottom: calc(var(--fuss-h, 44px) - 4px); }
/* Aufbau: Ausgangszustand ohne Übergang */
.blatt.aufbau .raster, .blatt.aufbau .zelle, .blatt.aufbau .raster::after { --kx: 0%; --ky: 0%; transition: none; }
.blatt.aufbau .raster::before { --zieh: 0%; transition: none; }
.blatt.aufbau .zelle > *, .blatt.aufbau .nav a { opacity: 0; translate: 0 12px; transition: none; }
.blatt.aufbau .plus-l::before, .blatt.aufbau .plus-r::after, .blatt.aufbau .lineal::before, .blatt.aufbau .lineal::after, .blatt.aufbau .w2::after,
.blatt.aufbau::before, .blatt.aufbau::after { scale: 0; rotate: -90deg; transition: none; }
/* ---------- Inhalte ---------- */
.kopf { grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; align-items: center; padding: 0; min-height: 58px; }
.kopf > * { padding: 0 var(--pad); }
.marke { grid-column: 1 / 4; display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 17px; letter-spacing: -.02em; }
.marke svg { width: 20px; height: 20px; flex: none; }
.marke svg .q { fill: var(--akzent); transition: fill 600ms var(--kurve); }
.nav { display: contents; }
.nav a { font-size: 14px; color: var(--leise); transition: color var(--ui) linear, opacity 900ms var(--kurve) 300ms, translate 900ms var(--kurve) 300ms; }
.nav a:hover, .nav a:focus-visible { color: var(--tinte); }
.nav a:nth-child(1) { grid-column: 5; } .nav a:nth-child(2) { grid-column: 6; } .nav a:nth-child(3) { grid-column: 7; } .nav a:nth-child(4) { grid-column: 8; }
.blattnr { grid-column: 10 / 13; text-align: right; }
.menue { display: none; }
.held { grid-column: 1 / 9; display: flex; flex-direction: column; justify-content: space-between; gap: 24px; min-height: 0; padding-bottom: calc(var(--pad) * 1.4); }
.mass { display: flex; align-items: center; gap: 10px; color: var(--leise); }
.mass i { flex: 1; height: 9px; position: relative; }
.mass i::before { content: ''; position: absolute; left: 0; right: 0; top: 4px; height: 1px; background: currentColor; opacity: .55; }
.mass i::after { content: ''; position: absolute; inset: 0; border-inline: 1px solid currentColor; opacity: .55; }
.held h1 {
font-size: clamp(42px, 6.6vw, 112px); font-weight: 500; letter-spacing: -.045em; line-height: .94; max-width: 9.5em;
transition: font-size 600ms var(--kurve), letter-spacing 600ms var(--kurve), font-weight 600ms var(--kurve),
opacity 900ms var(--kurve) calc(300ms + var(--i) * var(--versatz)), translate 900ms var(--kurve) calc(300ms + var(--i) * var(--versatz));
}
.held h1 em { color: var(--akzent); transition: color 600ms var(--kurve); letter-spacing: -.02em; }
[data-v='swiss'] .held h1 { font-size: clamp(46px, 7.7vw, 132px); font-weight: 650; letter-spacing: -.06em; line-height: .88; }
[data-v='swiss'] .held h1 em { font-family: inherit; font-style: normal; font-weight: inherit; letter-spacing: inherit; }
.seite { grid-column: 9 / 13; display: flex; flex-direction: column; gap: 22px; padding-bottom: calc(var(--pad) * 1.4); }
.seite .gruppe { margin-top: auto; display: flex; flex-direction: column; gap: 22px; padding-bottom: clamp(8px, 2.4vh, 28px); }
.seite p.text { font-size: clamp(15px, 1.15vw, 17px); line-height: 1.5; max-width: 30em; text-wrap: pretty; }
.cta { display: inline-flex; align-items: center; gap: 10px; font-weight: 550; font-size: 15px; border-bottom: 1px solid currentColor; padding-bottom: 4px; align-self: flex-start; }
.cta svg { width: 12px; height: 12px; transition: translate var(--ui) linear; }
.cta:hover svg { translate: 2px -2px; }
.ort { display: flex; justify-content: space-between; gap: 12px; }
.arbeit { grid-column: span 3; display: grid; grid-template-rows: auto auto; gap: 12px; cursor: default; }
.arbeit .bild { aspect-ratio: 16 / 10; overflow: hidden; background: rgba(0, 0, 0, .04); max-height: 22vh; width: 100%; }
.arbeit img { display: block; width: 100%; height: 100%; object-fit: cover; transition: scale 900ms var(--kurve), filter 600ms var(--kurve); }
.arbeit.an img { scale: 1.035; }
[data-v='matrix'] .arbeit .bild { background: #12345e; }
[data-v='matrix'] .arbeit img { filter: grayscale(1) contrast(1.05); mix-blend-mode: luminosity; opacity: .9; }
.arbeit .zeile { display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: baseline; }
.arbeit .name { font-size: 15px; font-weight: 550; letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ---------- Fadenkreuz mit Koordinaten ---------- */
.kreuz { position: absolute; inset: 0; pointer-events: none; z-index: 5; opacity: 0; transition: opacity var(--ui) linear; overflow: hidden; }
.kreuz.an { opacity: 1; }
.kreuz .v, .kreuz .h { position: absolute; left: 0; top: 0; background: var(--akzent); }
.kreuz .v { width: 1px; height: 100%; }
.kreuz .h { height: 1px; width: 100%; }
.kreuz .wert { position: absolute; left: 0; top: 0; padding: 4px 6px; background: var(--akzent); color: var(--grund); font: 500 10px/1 'Geist Mono', monospace; letter-spacing: .08em; white-space: nowrap; }
.zeilennamen { position: absolute; left: calc(var(--steg) * -1); width: var(--steg); top: 0; bottom: 0; pointer-events: none; }
.zeilennamen span { position: absolute; left: 0; width: 100%; text-align: center; font: 500 10px/1 'Geist Mono', monospace; color: var(--leise); transition: color var(--ui) linear; }
.zeilennamen span.an { color: var(--akzent); }
/* ---------- Fußleiste: Varianten und Werte ---------- */
.fuss { display: flex; justify-content: space-between; align-items: center; gap: 8px 24px; flex-wrap: wrap; padding: 12px 0 14px; }
.modi { display: flex; gap: 4px; }
.modi button { letter-spacing: inherit; text-transform: inherit; color: var(--leise); background: none; border: 1px solid var(--kante-h); height: 28px; padding: 0 11px; cursor: pointer; transition: color var(--ui) linear, background-color var(--ui) linear, border-color var(--ui) linear; }
[data-v='swiss'] .modi button { border-color: rgba(18, 18, 18, .2); }
.modi button:hover { color: var(--tinte); }
.modi button[aria-pressed='true'] { background: var(--tinte); color: var(--grund); border-color: var(--tinte); }
.werte { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 4px 16px; }
.werte b { color: var(--tinte); font-weight: 500; }
.zeiger { position: fixed; left: 0; top: 0; z-index: 9; width: 18px; height: 18px; pointer-events: none; display: none; }
.vorfuehrung .zeiger { display: block; }
:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
/* ---------- Größen ---------- */
@media (min-width: 821px) and (min-height: 560px) { .blatt { height: 100svh; } }
@media (max-width: 820px) {
:root { --cols: 8; }
.marke { grid-column: 1 / 4; } .nav { display: none; } .blattnr { grid-column: 6 / 9; }
.held { grid-column: 1 / -1; min-height: 46svh; }
.seite { grid-column: 1 / -1; }
.arbeit { grid-column: span 4; }
.arbeit .bild { max-height: none; }
.w2:not(.plus-r)::after { display: block; }
}
@media (max-width: 640px) {
:root { --cols: 4; }
.marke { grid-column: 1 / 3; } .blattnr { display: none; }
.menue { display: block; grid-column: 3 / 5; text-align: right; }
.held { min-height: 0; gap: 40px; padding-top: calc(var(--pad) * 1.2); }
.held h1 { font-size: clamp(38px, 11.5vw, 56px); }
[data-v='swiss'] .held h1 { font-size: clamp(40px, 12.4vw, 60px); }
.arbeit { grid-column: span 2; gap: 10px; }
.arbeit .zeile { grid-template-columns: 1fr; gap: 4px; }
.arbeit .zeile .jahr { display: none; }
.fuss { flex-direction: column; align-items: flex-start; }
.werte { justify-content: flex-start; }
.lineal span { padding-left: 5px; }
}
/* Vorschau in der Galerie: nur die Bühne */
.vorschau body { overflow: hidden; }
/* Touch: kein Hover-Fadenkreuz, Tippen setzt es */
@media (hover: none) { .cta:hover svg { translate: none; } }
/* Reduzierte Bewegung: kein Linien-Aufbau, keine Übergänge, Fadenkreuz springt */
.reduziert body, .reduziert .blatt, .reduziert .zelle, .reduziert .zelle > *, .reduziert .raster::before, .reduziert .raster::after,
.reduziert .held h1, .reduziert .nav a, .reduziert .arbeit img, .reduziert .lineal span { transition: none !important; }
.reduziert .plus-l::before, .reduziert .plus-r::after, .reduziert .lineal::before, .reduziert .lineal::after, .reduziert .blatt::before, .reduziert .blatt::after, .reduziert .w2::after { transition: none !important; }
.reduziert .arbeit.an img { scale: 1; }
</style>
</head>
<body data-v="blueprint">
<div class="buehne" data-buehne>
<div class="blatt" data-blatt>
<div class="lineal mono" aria-hidden="true" data-lineal></div>
<div class="raster" data-raster>
<header class="zelle kopf" style="--i:0">
<a class="marke" href="#start" aria-label="Achsmaß, Startseite">
<svg viewBox="0 0 20 20" aria-hidden="true"><rect x=".5" y=".5" width="19" height="19" fill="none" stroke="currentColor"/><path d="M10 .5v19M.5 10h19" stroke="currentColor" stroke-width=".8" opacity=".5"/><rect class="q" x="10" y="10" width="9.5" height="9.5"/></svg>
Achsmaß
</a>
<nav class="nav" aria-label="Hauptnavigation">
<a href="#arbeiten">Arbeiten</a><a href="#buero">Büro</a><a href="#haltung">Haltung</a><a href="#kontakt">Kontakt</a>
</nav>
<p class="mono blattnr">Blatt 01 / 04 · M 1:1</p>
<p class="mono menue">Menü</p>
</header>
<section class="zelle held plus-l" style="--i:1" aria-labelledby="titel">
<div class="mass mono" aria-hidden="true"><span data-mass>8 Sp.</span><i></i></div>
<h1 id="titel">Marken mit Maß und <em>System.</em></h1>
</section>
<aside class="zelle seite plus-l plus-r" style="--i:2">
<p class="mono">(01) Büro für Markengestaltung</p>
<div class="gruppe">
<p class="text">Wir entwickeln Erscheinungsbilder für Cafés, Handwerk und Mittelstand. Logo, Gestaltungsraster und Website entstehen auf einem gemeinsamen Maß.</p>
<a class="cta" href="#kontakt">Projekt anfragen <svg viewBox="0 0 12 12" aria-hidden="true"><path d="M3 9 9 3M4 3h5v5" fill="none" stroke="currentColor" stroke-width="1.4"/></svg></a>
</div>
<div class="ort mono"><span>Leipzig</span><span>51.3397° N · 12.3731° E</span></div>
</aside>
<article class="zelle arbeit plus-l" style="--i:3">
<div class="bild"><img src="bilder/marke-krume.jpg" alt="Wortmarke und Brot-Zeichen der Backstube Krume auf Senfgelb"></div>
<div class="zeile"><span class="mono">C1</span><h2 class="name">Krume</h2><span class="mono jahr">2025</span></div>
</article>
<article class="zelle arbeit w2 plus-l" style="--i:4">
<div class="bild"><img src="bilder/marke-roestkontor.jpg" alt="Wortmarke der Kaffeerösterei Röstkontor in Creme auf Ziegelrot"></div>
<div class="zeile"><span class="mono">C2</span><h2 class="name">Röstkontor</h2><span class="mono jahr">2024</span></div>
</article>
<article class="zelle arbeit plus-l" style="--i:5">
<div class="bild"><img src="bilder/marke-eselsohr.jpg" alt="Bildmarke Eselsohr, Buchhandlung und Lesecafé, auf Anthrazit"></div>
<div class="zeile"><span class="mono">C3</span><h2 class="name">Eselsohr</h2><span class="mono jahr">2024</span></div>
</article>
<article class="zelle arbeit w2 plus-l plus-r" style="--i:6">
<div class="bild"><img src="bilder/typo-plakat.jpg" alt="Ausstellungsplakat Licht: schwarze Versalien und orange Scheibe auf Neongelb"></div>
<div class="zeile"><span class="mono">C4</span><h2 class="name">Licht, Halle 4</h2><span class="mono jahr">2025</span></div>
</article>
<div class="kreuz" data-kreuz aria-hidden="true"><i class="v" data-kv></i><i class="h" data-kh></i><span class="wert" data-kwert></span></div>
<div class="zeilennamen" data-zeilennamen aria-hidden="true"></div>
</div>
<div class="fuss" data-fuss>
<div class="modi mono" role="group" aria-label="Variante">
<button type="button" data-wahl="blueprint" aria-pressed="true">Blueprint</button>
<button type="button" data-wahl="swiss" aria-pressed="false">Swiss</button>
<button type="button" data-wahl="matrix" aria-pressed="false">Punktmatrix</button>
</div>
<p class="werte mono" data-werte aria-live="polite"></p>
</div>
</div>
</div>
<svg class="zeiger" data-zeiger viewBox="0 0 18 18" aria-hidden="true"><path d="M2 1.5 15.5 9.2l-6 1.3-2.9 5.6Z" fill="#111214" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/></svg>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = (s, r = document) => r.querySelector(s), $$ = (s, r = document) => [...r.querySelectorAll(s)];
const buehne = $('[data-buehne]'), blatt = $('[data-blatt]'), raster = $('[data-raster]'), lineal = $('[data-lineal]');
const kreuz = $('[data-kreuz]'), kv = $('[data-kv]'), kh = $('[data-kh]'), kwert = $('[data-kwert]');
const zeilennamenEl = $('[data-zeilennamen]'), massEl = $('[data-mass]'), werteEl = $('[data-werte]'), fuss = $('[data-fuss]');
const zeigerEl = $('[data-zeiger]'), held = $('.held');
const zellen = $$('.zelle', raster);
const BUCHST = 'ABCDEFGH';
let variante = 'blueprint', spalten = 12, zeilen = []; // zeilen: [{top, bottom}]
/* ---------- Geometrie: Lineal, Zeilenbuchstaben, Maßkette ---------- */
function messen() {
spalten = getComputedStyle(raster).gridTemplateColumns.split(' ').length;
if (lineal.children.length !== spalten) lineal.innerHTML = Array.from({ length: spalten }, (_, i) => `<span>${String(i + 1).padStart(2, '0')}</span>`).join('');
const tops = [...new Set(zellen.map(z => z.offsetTop))].sort((a, b) => a - b);
zeilen = tops.map((t, i) => ({ top: t, bottom: tops[i + 1] ?? raster.offsetHeight }));
zeilennamenEl.innerHTML = zeilen.map((z, i) => `<span style="top:${Math.round(z.top + Math.min(24, (z.bottom - z.top) / 2) - 5)}px">${BUCHST[i]}</span>`).join('');
const sp = Math.round(held.offsetWidth / (raster.clientWidth / spalten));
massEl.textContent = `${sp} Sp. · ${Math.round(held.offsetWidth)} px`;
blatt.style.setProperty('--fuss-h', fuss.offsetHeight + 'px');
werteZeigen();
}
/* ---------- Werte-Leiste ---------- */
const WERTE = {
blueprint: ['Zellkante <b>1px · 13 %</b>', 'Hilfslinie <b>1px · 5 %</b>', 'Steg-Schraffur <b>10 × 10 px · 315° · 5 %</b>', 'Plus <b>11 px</b>'],
swiss: ['Spaltenlinie <b>1px · 12 %</b>', 'Zeilenlinie <b>1px · 85 %</b>', 'Titel <b>650 · −0,06em</b>', 'Akzent <b>#e1301a</b>'],
matrix: ['Punkt <b>2px · ½ Spalte × 24 px</b>', 'Zellkante <b>1px · 22 %</b>', 'Marker <b>Kreis 7 px</b>'],
};
function werteZeigen() {
const raster_ = `Raster <b>${spalten} × 1fr · Steg 0</b>`;
const bewegung = reduziert() ? 'Aufbau <b>aus (reduzierte Bewegung)</b>' : 'Aufbau <b>1200 ms expo.out · Versatz 60 ms</b>';
werteEl.innerHTML = [raster_, ...WERTE[variante], bewegung].map(s => `<span>${s}</span>`).join('');
}
/* ---------- Linien-Aufbau ---------- */
let aufbauUhr = null;
function aufbauen() {
cancelAnimationFrame(aufbauUhr);
if (reduziert()) { blatt.classList.remove('aufbau'); return; }
blatt.classList.add('aufbau');
void blatt.offsetWidth; // Ausgangszustand ohne Übergang festschreiben
aufbauUhr = requestAnimationFrame(() => { aufbauUhr = requestAnimationFrame(() => blatt.classList.remove('aufbau')); });
}
function wechseln(v) {
variante = v;
document.body.dataset.v = v;
$$('[data-wahl]').forEach(b => b.setAttribute('aria-pressed', b.dataset.wahl === v));
requestAnimationFrame(messen);
aufbauen();
}
$$('[data-wahl]').forEach(b => b.addEventListener('click', () => { if (html.classList.contains('vorfuehrung')) neustart(); wechseln(b.dataset.wahl); }));
/* ---------- Fadenkreuz: folgt dem Zeiger (Lerp), meldet Achse und Zeile ---------- */
const LERP = 0.22; // bei 60 Bildern/s, auf echte Bildrate umgerechnet
const ziel = { x: 0, y: 0 }, ist = { x: 0, y: 0 };
let sichtbar = false, laeuft = false, letzte = 0, frisch = true;
function zeigeBei(x, y) { // x, y relativ zum Raster; null = aus
if (x == null) {
sichtbar = false; kreuz.classList.remove('an'); frisch = true;
zellen.forEach(z => z.classList.remove('an'));
$$('span', lineal).forEach(s => s.classList.remove('an'));
$$('span', zeilennamenEl).forEach(s => s.classList.remove('an'));
return;
}
ziel.x = x; ziel.y = y;
if (frisch || reduziert()) { ist.x = x; ist.y = y; frisch = false; }
sichtbar = true; kreuz.classList.add('an');
starten();
}
function malen() {
const w = raster.clientWidth, h = raster.clientHeight;
kv.style.transform = `translateX(${ist.x}px)`;
kh.style.transform = `translateY(${ist.y}px)`;
const sp = Math.min(spalten - 1, Math.max(0, Math.floor(ist.x / (w / spalten))));
const zi = Math.max(0, zeilen.findIndex(z => ist.y >= z.top && ist.y < z.bottom));
kwert.textContent = `${String(sp + 1).padStart(2, '0')} / ${BUCHST[zi]} · ${Math.round(ist.x)}, ${Math.round(ist.y)}`;
const lw = kwert.offsetWidth, lh = kwert.offsetHeight;
const lx = ist.x + 8 + lw > w ? ist.x - 8 - lw : ist.x + 8;
const ly = ist.y - 8 - lh < 0 ? ist.y + 8 : ist.y - 8 - lh;
kwert.style.transform = `translate(${lx}px, ${ly}px)`;
$$('span', lineal).forEach((s, i) => s.classList.toggle('an', i === sp));
$$('span', zeilennamenEl).forEach((s, i) => s.classList.toggle('an', i === zi));
// Zelle unter dem Fadenkreuz füllen
zellen.forEach(z => {
const drin = ist.x >= z.offsetLeft && ist.x < z.offsetLeft + z.offsetWidth && ist.y >= z.offsetTop && ist.y < z.offsetTop + z.offsetHeight;
z.classList.toggle('an', drin && !z.classList.contains('kopf'));
});
}
function bild(t) {
const dt = Math.min(64, t - letzte) / 16.667; letzte = t;
const k = reduziert() ? 1 : 1 - Math.pow(1 - LERP, dt);
ist.x += (ziel.x - ist.x) * k; ist.y += (ziel.y - ist.y) * k;
if (sichtbar) malen();
if (!sichtbar || (Math.abs(ziel.x - ist.x) < .2 && Math.abs(ziel.y - ist.y) < .2)) { if (sichtbar) { ist.x = ziel.x; ist.y = ziel.y; malen(); } laeuft = false; return; }
requestAnimationFrame(bild);
}
function starten() { if (!laeuft) { laeuft = true; letzte = performance.now(); requestAnimationFrame(bild); } }
const lokal = e => { const r = raster.getBoundingClientRect(); return [e.clientX - r.left, e.clientY - r.top]; };
raster.addEventListener('pointermove', e => { if (e.pointerType === 'mouse' && !html.classList.contains('vorfuehrung')) zeigeBei(...lokal(e)); });
raster.addEventListener('pointerleave', e => { if (e.pointerType === 'mouse' && !html.classList.contains('vorfuehrung')) zeigeBei(null); });
// Touch: Tippen setzt das Fadenkreuz, Tippen außerhalb nimmt es weg
raster.addEventListener('pointerdown', e => { if (e.pointerType !== 'mouse') { if (html.classList.contains('vorfuehrung')) neustart(); zeigeBei(...lokal(e)); } });
document.addEventListener('pointerdown', e => { if (e.pointerType !== 'mouse' && !raster.contains(e.target) && !e.target.closest('.fuss')) zeigeBei(null); });
// Tastatur: Fokus in einer Zelle zeigt deren Mitte
zellen.forEach(z => z.addEventListener('focusin', () => { if (!html.classList.contains('vorfuehrung')) zeigeBei(z.offsetLeft + z.offsetWidth / 2, z.offsetTop + z.offsetHeight / 2); }));
raster.addEventListener('focusout', e => { if (!raster.contains(e.relatedTarget)) zeigeBei(null); });
raster.addEventListener('click', e => { if (e.target.closest('a')) e.preventDefault(); }); // Demo: keine echten Ziele
/* ---------- Vorführung: Aufbau, gedachter Zeiger fährt das Raster ab, Variante wechselt ---------- */
// Runde je Variante 7600 ms: 0–1500 Aufbau, 1500–7000 Zeiger, dann nächste Variante.
const RUNDE = 7600, FOLGE = ['blueprint', 'swiss', 'matrix'];
// Wegpunkte: [Zeit ms, Zelle, x 0–1, y 0–1]
const WEG = [[1500, 1, .2, .78], [2600, 1, .62, .42], [3600, 4, .45, .4], [4500, 6, .55, .5], [5400, 2, .4, .45], [6400, 3, .6, .35], [7000, 3, .6, .35]];
let uhr = null, start = 0, runde = -1;
const sanft = p => p < .5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
function punkt(t) {
for (let i = 0; i < WEG.length - 1; i++) {
const [t0, z0, x0, y0] = WEG[i], [t1, z1, x1, y1] = WEG[i + 1];
if (t >= t0 && t <= t1) {
const p = sanft((t - t0) / (t1 - t0)), a = zellen[z0], b = zellen[z1];
const ax = a.offsetLeft + a.offsetWidth * x0, ay = a.offsetTop + a.offsetHeight * y0;
const bx = b.offsetLeft + b.offsetWidth * x1, by = b.offsetTop + b.offsetHeight * y1;
return [ax + (bx - ax) * p, ay + (by - ay) * p];
}
}
return null;
}
function schritt(now) {
const gesamt = now - start, r = Math.floor(gesamt / RUNDE), t = gesamt % RUNDE;
if (r !== runde) { runde = r; zeigeBei(null); wechseln(FOLGE[r % FOLGE.length]); }
const p = punkt(t), rr = raster.getBoundingClientRect();
if (p) {
zeigeBei(p[0], p[1]);
zeigerEl.style.opacity = 1;
zeigerEl.style.transform = `translate(${rr.left + p[0] - 2}px, ${rr.top + p[1] - 1}px)`;
} else {
if (sichtbar) zeigeBei(null);
zeigerEl.style.opacity = 0;
}
uhr = requestAnimationFrame(schritt);
}
function vorfuehren() {
pause();
html.classList.add('vorfuehrung');
start = performance.now(); runde = -1;
uhr = requestAnimationFrame(schritt);
}
let pausiertBei = 0;
function pause() { if (uhr) { cancelAnimationFrame(uhr); uhr = null; pausiertBei = performance.now(); } }
function weiter() {
if (uhr || !html.classList.contains('vorfuehrung')) return;
start += performance.now() - pausiertBei;
uhr = requestAnimationFrame(schritt);
}
function neustart() {
pause(); html.classList.remove('vorfuehrung'); zeigeBei(null);
wechseln(variante);
}
messen();
aufbauen();
new ResizeObserver(messen).observe(raster);
document.fonts && document.fonts.ready.then(messen);
new MutationObserver(() => { werteZeigen(); if (reduziert()) blatt.classList.remove('aufbau'); }).observe(html, { attributes: true, attributeFilter: ['class'] });
})();
</script>
</body>
</html>