Add full deck, config tool, and updated design docs

Adds the complete "KI verstehen" presentation (36 slides), the
Präsentations-Konfigurator (config.html + slides-manifest.js) for
curating which slides to show, and updates DESIGN.md/PATTERNS.md/
deck-template.html to reflect the new house font (General Sans) and
newly established patterns (chapter dividers, term tooltips, the
mitmach game, ambient sound, and the slide configurator).
This commit is contained in:
Erik Thiele
2026-07-25 16:29:28 +02:00
parent a0641c6fbf
commit 2633ca0e0d
9 changed files with 4570 additions and 0 deletions

263
config.html Normal file
View File

@@ -0,0 +1,263 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CANCOM · Deck konfigurieren</title>
<link rel="preconnect" href="https://api.fontshare.com">
<link rel="stylesheet" href="https://api.fontshare.com/v2/css?f[]=general-sans@700,600,500&f[]=satoshi@700,500,400&f[]=jetbrains-mono@700,500,400&display=swap">
<script src="slides-manifest.js?v=2"></script>
<style>
:root {
--cancom-red: #E4002B;
--cancom-red-glow: rgba(228,0,43,0.55);
--cancom-red-dim: rgba(228,0,43,0.15);
--ink: #0A0A0F;
--ink-soft: #14141C;
--ink-line: #23232E;
--paper: #F5F5F2;
--paper-dim: rgba(245,245,242,0.6);
--paper-mute: rgba(245,245,242,0.35);
--font-display: 'General Sans', sans-serif;
--font-body: 'Satoshi', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { background: var(--ink); color: var(--paper); font-family: var(--font-body); min-height: 100%; }
body { padding: 64px 64px 140px; max-width: 1100px; margin: 0 auto; }
.page-eyebrow {
font-family: var(--font-mono); font-size: 14px; letter-spacing: 0.28em; text-transform: uppercase;
color: var(--cancom-red); display: inline-flex; align-items: center; gap: 14px; margin-bottom: 20px;
}
.page-eyebrow::before { content: ""; width: 40px; height: 2px; background: var(--cancom-red); }
h1 {
font-family: var(--font-display); font-weight: 700; font-size: 48px; letter-spacing: -0.03em;
line-height: 1.1; margin-bottom: 14px;
}
.page-lead { font-size: 18px; color: var(--paper-dim); line-height: 1.5; max-width: 640px; margin-bottom: 40px; }
.toolbar {
position: sticky; top: 0; z-index: 50;
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
background: rgba(10,10,15,0.9); backdrop-filter: blur(12px);
border: 1px solid var(--ink-line); border-radius: 14px;
padding: 18px 22px; margin-bottom: 36px;
}
.toolbar .count {
font-family: var(--font-mono); font-size: 14px; letter-spacing: 0.1em; color: var(--paper-mute);
margin-right: auto;
}
.toolbar .count strong { color: var(--paper); font-weight: 700; }
.btn {
font-family: var(--font-mono); font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase;
padding: 12px 18px; border-radius: 8px; border: 1px solid var(--ink-line);
background: #0f0f16; color: var(--paper-dim); cursor: pointer;
transition: border-color 0.12s ease-out, color 0.12s ease-out, background 0.12s ease-out;
}
.btn:hover { border-color: var(--cancom-red); color: var(--paper); }
.btn.primary {
background: var(--cancom-red); border-color: var(--cancom-red); color: #fff; font-weight: 700;
box-shadow: 0 10px 30px var(--cancom-red-glow);
}
.btn.primary:hover { transform: translateY(-1px); }
.chapter { margin-bottom: 36px; }
.chapter-head {
display: flex; align-items: center; gap: 14px; margin-bottom: 14px;
font-family: var(--font-mono); font-size: 13px; letter-spacing: 0.18em; text-transform: uppercase;
color: var(--paper-mute);
}
.chapter-head .chapter-toggle-all {
margin-left: auto; font-size: 11px; color: var(--cancom-red); cursor: pointer; letter-spacing: 0.1em;
background: none; border: none; font-family: var(--font-mono); padding: 4px 8px;
}
.chapter-head .chapter-toggle-all:hover { text-decoration: underline; }
.row {
display: flex; align-items: center; gap: 18px;
padding: 16px 20px; border: 1px solid var(--ink-line); border-radius: 10px;
background: #0f0f16; margin-bottom: 10px;
transition: border-color 0.12s ease-out, background 0.12s ease-out;
}
.row:hover { border-color: rgba(228,0,43,0.35); background: #14141c; }
.row.divider-row { background: transparent; border-style: dashed; }
.row.locked { opacity: 0.55; }
.row input[type="checkbox"] {
appearance: none; width: 22px; height: 22px; flex-shrink: 0;
border: 1.5px solid var(--ink-line); border-radius: 6px; background: #0a0a0f;
cursor: pointer; position: relative; transition: border-color 0.12s ease-out, background 0.12s ease-out;
}
.row input[type="checkbox"]:checked { background: var(--cancom-red); border-color: var(--cancom-red); }
.row input[type="checkbox"]:checked::after {
content: ""; position: absolute; left: 7px; top: 3px; width: 5px; height: 10px;
border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.row input[type="checkbox"]:disabled { cursor: not-allowed; }
.row .slide-id {
font-family: var(--font-mono); font-size: 13px; color: var(--paper-mute); width: 28px; flex-shrink: 0;
}
.row .slide-title { font-size: 17px; color: var(--paper); flex: 1; }
.row.divider-row .slide-title { color: var(--paper-mute); font-family: var(--font-mono); font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase; }
.row .badge {
font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
color: var(--paper-mute); border: 1px solid var(--ink-line); border-radius: 20px; padding: 4px 10px;
}
.footer-bar {
position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
display: flex; justify-content: center; gap: 16px;
padding: 22px; background: linear-gradient(0deg, var(--ink) 60%, transparent);
}
.footer-bar .btn { padding: 16px 32px; font-size: 14px; }
.hint { font-size: 14px; color: var(--paper-mute); margin-top: -8px; margin-bottom: 30px; }
</style>
</head>
<body>
<div class="page-eyebrow">Präsentations-Konfigurator</div>
<h1>Welche Slides sollen gezeigt werden?</h1>
<p class="page-lead">Häkchen entfernen blendet eine Slide für den Vortrag aus, ohne sie zu löschen. Die Auswahl wird lokal in diesem Browser gespeichert und beim nächsten Öffnen des Decks automatisch angewendet.</p>
<div class="toolbar">
<span class="count" id="count"> / Slides aktiv</span>
<button class="btn" id="btnAll">Alle an</button>
<button class="btn" id="btnNone">Alle aus</button>
<button class="btn" id="btnReset">Zurücksetzen</button>
<button class="btn primary" id="btnOpen">Zur Präsentation →</button>
</div>
<div id="chapters"></div>
<div class="footer-bar">
<button class="btn primary" id="btnSaveBottom">Speichern & zur Präsentation →</button>
</div>
<script>
(function () {
const chaptersEl = document.getElementById('chapters');
const countEl = document.getElementById('count');
// Slides nach Kapitel gruppieren, Reihenfolge aus dem Manifest beibehalten
const groups = [];
const groupIndex = {};
SLIDE_MANIFEST.forEach((slide) => {
const key = slide.kapitel;
if (!(key in groupIndex)) {
groupIndex[key] = groups.length;
groups.push({ kapitel: key, slides: [] });
}
groups[groupIndex[key]].slides.push(slide);
});
let enabled = loadSlideConfig();
if (!enabled) {
enabled = new Set(SLIDE_MANIFEST.map((s) => s.id));
}
function render() {
chaptersEl.innerHTML = '';
groups.forEach((group) => {
const chapterDiv = document.createElement('div');
chapterDiv.className = 'chapter';
const head = document.createElement('div');
head.className = 'chapter-head';
head.innerHTML = '<span>' + group.kapitel + '</span>';
const toggleAllBtn = document.createElement('button');
toggleAllBtn.className = 'chapter-toggle-all';
toggleAllBtn.textContent = 'Kapitel an/aus';
toggleAllBtn.addEventListener('click', () => {
const toggleable = group.slides.filter((s) => !s.locked);
const allOn = toggleable.every((s) => enabled.has(s.id));
toggleable.forEach((s) => {
if (allOn) enabled.delete(s.id); else enabled.add(s.id);
});
render();
});
head.appendChild(toggleAllBtn);
chapterDiv.appendChild(head);
group.slides.forEach((slide) => {
const row = document.createElement('label');
row.className = 'row' + (slide.type === 'divider' ? ' divider-row' : '') + (slide.locked ? ' locked' : '');
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = enabled.has(slide.id);
cb.disabled = !!slide.locked;
cb.addEventListener('change', () => {
if (cb.checked) enabled.add(slide.id); else enabled.delete(slide.id);
updateCount();
});
const idEl = document.createElement('span');
idEl.className = 'slide-id';
idEl.textContent = String(slide.id).padStart(2, '0');
const titleEl = document.createElement('span');
titleEl.className = 'slide-title';
titleEl.textContent = slide.title;
row.appendChild(cb);
row.appendChild(idEl);
row.appendChild(titleEl);
if (slide.locked) {
const badge = document.createElement('span');
badge.className = 'badge';
badge.textContent = 'immer aktiv';
row.appendChild(badge);
} else if (slide.type === 'divider') {
const badge = document.createElement('span');
badge.className = 'badge';
badge.textContent = 'Kapitel-Trenner';
row.appendChild(badge);
}
chapterDiv.appendChild(row);
});
chaptersEl.appendChild(chapterDiv);
});
updateCount();
}
function updateCount() {
countEl.innerHTML = '<strong>' + enabled.size + '</strong> / ' + SLIDE_MANIFEST.length + ' Slides aktiv';
saveSlideConfig(enabled);
}
document.getElementById('btnAll').addEventListener('click', () => {
enabled = new Set(SLIDE_MANIFEST.map((s) => s.id));
render();
});
document.getElementById('btnNone').addEventListener('click', () => {
enabled = new Set(SLIDE_MANIFEST.filter((s) => s.locked).map((s) => s.id));
render();
});
document.getElementById('btnReset').addEventListener('click', () => {
enabled = new Set(SLIDE_MANIFEST.map((s) => s.id));
render();
});
function goToDeck() {
saveSlideConfig(enabled);
// localStorage wird bei file:// nicht zuverlässig zwischen Dateien geteilt (v. a. Safari) —
// die Auswahl deshalb zusätzlich als URL-Parameter mitgeben, das ist die verlässliche Quelle.
const ids = Array.from(enabled).sort((a, b) => a - b).join(',');
window.location.href = 'ki-verstehen.html?slides=' + encodeURIComponent(ids);
}
document.getElementById('btnOpen').addEventListener('click', goToDeck);
document.getElementById('btnSaveBottom').addEventListener('click', goToDeck);
render();
})();
</script>
</body>
</html>