vt-b-visitrans-design-system¶
Brand bible for VisiTrans, VisiMatch, VisiFair, and VisiArea surfaces. Use when designing or building any web/UI work for these brands — covers locked color palette (Anthracite/Orange/White primary, logo base stays pure black, pure Magenta accent, Yellow/Green/Cyan status secondaries), Inter typography (9 weights), 60+ logo lockups, surface-driven two-theme system (dark by default for websites and logged-out product UI, switchable light for logged-in product UI), voice/tone, and the cross-brand-link prohibition. Includes drop-in CSS tokens, reference HTML builds for visitrans.de / visimatch.com / visifair.com, and specimen previews.
Plugin: vt-brand
Category: Design
Command: /vt-b-visitrans-design-system
SKILL.md — VisiTrans Design System¶
This file is the entry-point manifest for AI agents working on VisiTrans, VisiMatch, VisiFair, or VisiArea surfaces. Read it first.
Note: This skill covers the Astro/Tailwind website stack (V024-Websites-dev). For the Angular 21 + PrimeNG product UI stack, do not look here — that guidance lives in
vt-d-pd-3-prototype§ Path A (Angular + PrimeNG). The PrimeNG preset itself is generated from the token YAML byscripts/design_system/generate_primeng_preset.py(the canonical source);/vt-d-bootstrapinvokes that generator. The theme tokens incolors_and_type.cssand the switcher pattern below apply to both stacks.
What you are designing for¶
VisiTrans GmbH is a B2B SaaS company in Paderborn, Germany operating a "House of Brands":
- VisiTrans (visitrans.de) — corporate holding, German-default
- VisiMatch (visimatch.com) — container-logistics platform, English-default
- VisiFair (visifair.com) — trade-fair operations, German-default
- VisiArea — defense / site management; mentioned on corporate site only
Cross-brand linking between VisiMatch and VisiFair is forbidden — keep users in their tunnel.
Files in this design system¶
| File | Purpose |
|---|---|
README.md |
Full brand bible: voice, color, type, motion, iconography, brand inventory |
colors_and_type.css |
Drop-in stylesheet — CSS custom properties for both themes + utility classes |
assets/logos/*.png |
32 official lockups (brand × sub-product × color combo). Never recreate; pick the right file |
preview/*.html |
Specimen cards for the Design System tab. Each is pinned to an explicit data-theme. Read for reference only |
ui_kits/visitrans/index.html |
Reference build of visitrans.de (dark, corporate) |
ui_kits/visimatch/index.html |
Reference build of visimatch.com (dark, dashboard-led) |
ui_kits/visifair/index.html |
Reference build of visifair.com (dark, hall-plan hero) |
Non-negotiables¶
- Color — Primärfarben are Anthrazit
#101216(Logo-Basis bleibt reines#000000), Orange#FF9E00, Weiß#FFFFFF. Akzent is Magenta#FF00FF(pure electric). Sekundär are Gelb#FFFF00, Grün#73FF59, Cyan#00FFFF— used for status / charts only. Never invent colors. - Type — Inter only, all 9 weights licensed. ExtraBold (800) on display with
-0.025emtracking. No serif. No Montserrat / Roboto / Arial. - Tone — Confident, technical, factual. No exclamation marks. No emoji. No "revolutionize". Use Sie in German.
- Wordmark casing — In running text always VisiTrans, VisiMatch, VisiFair (CamelCase). The actual logo glyph reads "ViSiTRANS" — use the PNG, never typeset by hand.
- Sub-products are ALL CAPS proper nouns: CARRIER, PRICING, TERMINAL, MARKER, OUTDOOR, PLANNER, SHOW.
- Theme polarity is a property of the SURFACE, not the brand. There is no such thing as a "light brand" — every VisiTrans brand website is dark. Pick the theme from the surface you are building:
| Surface | Theme |
|---|---|
| Marketing websites — visitrans.de, visimatch.com, visifair.com | Dark, always |
| Presentations / anything shown on a screen | Dark |
| SaaS product UI, logged-out (login, signup, marketing shell) | Dark |
| SaaS product UI, logged-in | User-switchable light ↔ dark |
| Word / PDF documents | White — unchanged, see vt-b-visitrans-cd |
The light theme is retained and first-class, not deprecated: it is the theme a logged-in user can switch to, and preview/product-ui-light.html is its reference surface. All themes share the orange + magenta palette; only neutrals invert.
7. Dark theme signature = (a) subtle 64px grid rgba(255,255,255,0.025) and/or (b) radial orange spotlight at top of hero. No other backgrounds; no gradients.
8. No iconography library baked in. Use Lucide via CDN if you need icons. 2px stroke, rounded caps, currentColor.
9. No emoji. No noise/grain textures. No stock photography of smiling office workers.
10. Motion — fade only. ~150ms color transitions on hover. No transforms, no springs, no parallax.
How to start¶
- Link
colors_and_type.css. Dark is the default — build a website, a presentation or a logged-out product screen and you are already correct, no attribute needed. Only a logged-in product surface opts in with<html data-theme="light">, and it must ship the switcher (see below) rather than hardcoding light. - Open the matching
ui_kits/<brand>/index.htmlfor layout, spacing, hero patterns and component usage. - Pull lockups from
assets/logos/. Usebl-oron white,wh-oron dark backgrounds. - Section eyebrows always orange, uppercase,
0.18emtracking, 12px, weight 600. - Primary CTAs: magenta
#FF00FFon light, orange#FF9E00(with glow) on dark.
Light↔dark switcher (logged-in product UI)¶
This is the only place this pattern is documented. vt-d-pd-3-prototype § Path A and
vt-d-kw-prototype cross-reference this section; never copy the code into them — a second
copy drifts, and the no-flash script is exactly the kind of thing that rots silently.
Who gets it: logged-in product UI only. Marketing websites, presentations and logged-out product screens are dark only and ship no switcher and no script — a switcher there would let a visitor put a brand website into a state the brand does not have.
Default resolution: OS prefers-color-scheme on first load → the user's explicit
choice persists to localStorage and from then on overrides the OS signal, including
when the OS later flips. That override is the point: a user who chose light at 09:00 does
not want their dashboard inverting when their laptop switches to dark at sunset.
All five elements are load-bearing. Dropping any one produces a bug that only appears in one specific sequence, which is why they are enumerated rather than described.
Framework note (Angular/PrimeNG product UIs). The body script below is written for a
raw HTML page, but it is deliberately framework-safe: the toggle/reset handling is one
delegated listener on document, not a querySelector captured at load. That is what
makes it correct in an Angular app, where index.html's body is just <app-root> and the
real toggle mounts ~200 ms later — a load-time document.querySelector('[data-theme-toggle]')
would return null, throw, and take the OS-following and reset registrations down with it.
Delegation needs no toggle to exist yet, survives every re-render and route change, and needs
no teardown. An Angular component contributes only a data-theme-toggle attribute on its
button and, if it renders a toggle after navigation, calls syncControls() in
ngAfterViewInit — no service, no signal, no DestroyRef.
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<!-- (1) BLOCKING, INLINE, AND BEFORE THE FIRST STYLESHEET LINK.
No defer, no async, no type="module" — every one of those makes it run
after first paint, which is the flash of the wrong theme this exists to
prevent. It must also come before the stylesheet so the attribute is on
<html> when the tokens first resolve. -->
<script>
(function () {
var stored = null, source = null;
try {
stored = localStorage.getItem('vt-theme');
source = localStorage.getItem('vt-theme-source');
} catch (e) { /* private mode / storage disabled — fall through to the OS */ }
// (2) The data-theme-source guard: distinguishes "the user chose dark"
// from "the OS happened to be dark". Without it you cannot tell
// whether a later OS change should be honoured or ignored.
var theme = (source === 'user' && stored)
? stored
: (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
var root = document.documentElement;
// Dark is the CSS default, so only light needs the attribute; remove it
// for dark in case the server or a cached shell pre-set it.
if (theme === 'light') root.setAttribute('data-theme', 'light');
else root.removeAttribute('data-theme');
root.setAttribute('data-theme-source', source === 'user' ? 'user' : 'system');
// (5) color-scheme MUST be set HERE, before first paint — not only inside
// apply() in the body script, which does not run until after the page
// has painted. It is what themes the scrollbars, native form controls
// and the canvas behind an overscroll; CSS variables cannot reach
// them. Omit it here and a dark-resolving page paints anthracite
// tokens framed by a bright UA-default scrollbar until the first click.
root.style.colorScheme = theme;
})();
</script>
<link rel="stylesheet" href="colors_and_type.css">
</head>
<body>
<!-- aria-pressed reflects STATE (pressed = light is active). The visible label
is static ("Helles Design") so a screen reader never hears an action verb
that contradicts the pressed state. syncControls() keeps it truthful on
load and on every transition — not just on click. -->
<button type="button" data-theme-toggle aria-pressed="false">Helles Design</button>
<button type="button" data-theme-reset>Systemeinstellung</button>
<script>
(function () {
var root = document.documentElement;
// Bind exactly once. The delegated listeners below live on `document` and
// are never removed; if this IIFE is ever re-run (dropped into a swapped
// fragment rather than the app shell) a second bind would stack a duplicate
// click handler and the toggle would double-flip. This guard makes the
// "needs no teardown" guarantee true by construction, not by placement.
if (root.__vtThemeBound) return;
root.__vtThemeBound = true;
// Keep ONE MediaQueryList at this scope. WebKit has historically collected
// unreferenced MQL objects, silently stopping OS-following; the reset
// handler reuses this same reference rather than constructing a second.
var mql = window.matchMedia('(prefers-color-scheme: light)');
function currentTheme() { // single source of truth: the DOM
return root.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
}
function syncControls() { // querySelectorAll — a page may have >1 toggle
var light = currentTheme() === 'light';
document.querySelectorAll('[data-theme-toggle]').forEach(function (b) {
b.setAttribute('aria-pressed', String(light));
});
}
function apply(theme, source) {
if (theme === 'light') root.setAttribute('data-theme', 'light');
else root.removeAttribute('data-theme');
root.setAttribute('data-theme-source', source);
// (5) color-scheme agreement at runtime — see the head script.
root.style.colorScheme = theme;
syncControls();
}
// ONE delegated listener on document handles both buttons. It needs no
// toggle to exist at load, survives every Angular re-render / route change
// / Turbo swap, holds no stale node reference, and needs no teardown —
// which is what makes the pattern safe on the Angular stack.
document.addEventListener('click', function (e) {
if (e.target.closest('[data-theme-toggle]')) {
var next = currentTheme() === 'light' ? 'dark' : 'light';
apply(next, 'user');
try {
localStorage.setItem('vt-theme', next);
localStorage.setItem('vt-theme-source', 'user');
} catch (e) { /* choice holds for this session only */ }
} else if (e.target.closest('[data-theme-reset]')) {
// (4) Reset-to-system. Without it, "follow my OS" is a one-way door:
// the first toggle pins the theme with no way back short of
// clearing site data.
try {
localStorage.removeItem('vt-theme');
localStorage.removeItem('vt-theme-source');
} catch (e) { /* nothing persisted to clear */ }
apply(mql.matches ? 'light' : 'dark', 'system');
}
});
// (3) The media listener acts ONLY while the source is still 'system'.
// Unguarded, it would stomp the user's explicit choice the next time
// the OS flipped — the single most common bug in this pattern.
mql.addEventListener('change', function (e) {
if (root.getAttribute('data-theme-source') !== 'system') return;
apply(e.matches ? 'light' : 'dark', 'system');
});
// Sync aria-pressed to the theme the head script already resolved. Angular
// components that render a toggle after navigation call this in
// ngAfterViewInit — idempotent and DOM-derived, so it is safe to re-run.
syncControls();
})();
</script>
</body>
</html>
Note what is not here: a prefers-color-scheme media query inside
colors_and_type.css. The OS signal is read in JavaScript, on product surfaces only. Putting
it in the token file would apply it to every surface that links the stylesheet and silently
light up the dark-only marketing sites for any visitor whose OS is set to light.
Reference implementation: preview/product-ui-light.html.
Customer / proof references (verified, reusable verbatim)¶
- Logistics customers: COSCO, ZIM, ONE
- Trade-fair customers: Landesmesse Stuttgart, Messe Berlin, NürnbergMesse
- Tagline: "Software für komplexe Operationen — Made in Germany"
- CEO Ralf Niemeier, "Wir bauen Software, die in den komplexesten Umgebungen der Welt funktioniert — zuverlässig, jeden Tag."
- Address: Technologiepark 20, D-33100 Paderborn
Open question¶
Inter is loaded via Google Fonts. The brand book (p.11) names Inter as the official face — self-host the static .woff2 files for production. The pure #FF00FF magenta is intentional (brand-book p.8) and supersedes any softer codebase value.