feat: initial mono-repo with 30 vanity domain sites

Scaffold complete repo structure:
- 28 static sites extracted from running containers on mlake
- 2 dynamic sites (dasbes.de, dumusst.com) marked for separate handling
- Template system with 6 templates (person-dark/light, product-dark, editorial, fun, minimal)
- Shared CSS (variables, responsive, animations, noise overlay)
- nginx config generator with multi-domain alias support
- Build script with Docker-based nginx validation
- add-site.sh helper for scaffolding new domains
- Dockerfile for single nginx:alpine container

Sites: clemensplassmann.de, danosi.de, deinesei.de, derkaiseristnackt.de,
elefantenhor.de, fragina.de, frenchkis.de, ichbinaufbali.de, ichbinaufbarley.de,
insain.de, julietensity.de, kainco.de (+keinco.de), kainstress.de, keinefreun.de,
knzlmgmt.de, kopffrai.de, legalais.de, machesdocheinfach.de, mai-otto.de
(+otto.flexsiebels.de, ottomatisch.de, ichbinotto.de), martinsiebels.de,
matthiasbreier.de, osterai.de, paragraphenraiter.de, schulfrai.de, smartin3.de,
sorgenfrai.de, vonschraitter.de, wartebitte.de

Refs: otto#341
This commit is contained in:
mAi
2026-03-29 13:20:27 +02:00
parent ec3e1e42f3
commit 6f5de542ab
78 changed files with 13471 additions and 1 deletions

View File

@@ -0,0 +1,423 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Warte bitte.</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🫖</text></svg>">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&family=Newsreader:ital,wght@0,300;0,400;1,300;1,400&display=swap');
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #faf8f5;
--text: #2a2520;
--text-soft: #6b6259;
--text-faint: #a69e94;
--accent: #c4a882;
--accent-soft: rgba(196, 168, 130, 0.15);
--line: rgba(196, 168, 130, 0.2);
}
html { scroll-behavior: smooth; }
body {
font-family: 'Newsreader', Georgia, serif;
background: var(--bg);
color: var(--text);
line-height: 1.8;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
.page {
max-width: 540px;
margin: 0 auto;
padding: 120px 24px 80px;
}
/* Teapot */
.icon {
font-size: 2.8rem;
margin-bottom: 48px;
opacity: 0.8;
animation: steam 4s ease-in-out infinite;
}
@keyframes steam {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
h1 {
font-family: 'Newsreader', Georgia, serif;
font-size: clamp(2.2rem, 5vw, 3rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.2;
margin-bottom: 40px;
color: var(--text);
}
.rule {
width: 40px;
height: 1px;
background: var(--accent);
margin-bottom: 40px;
opacity: 0.5;
}
.prose {
margin-bottom: 48px;
}
.prose p {
font-size: 1.08rem;
color: var(--text-soft);
font-weight: 300;
line-height: 1.9;
margin-bottom: 24px;
}
.prose p:last-child {
margin-bottom: 0;
}
.prose em {
font-style: italic;
color: var(--text);
}
.highlight {
background: var(--accent-soft);
padding: 28px 32px;
border-radius: 12px;
border-left: 2px solid var(--accent);
margin: 48px 0;
}
.highlight p {
font-size: 1.05rem;
color: var(--text-soft);
font-weight: 300;
line-height: 1.8;
font-style: italic;
}
.closing {
margin-top: 56px;
padding-top: 40px;
border-top: 1px solid var(--line);
}
.closing p {
font-size: 1rem;
color: var(--text-faint);
font-weight: 300;
line-height: 1.8;
}
.breath-container {
text-align: center;
margin: 56px 0;
font-family: 'Inter', sans-serif;
}
.breath-circle {
width: 140px; height: 140px;
border-radius: 50%;
border: 2px solid var(--accent);
margin: 0 auto 24px;
display: flex; align-items: center; justify-content: center;
transition: transform 0.1s ease, box-shadow 0.1s ease, border-color 0.3s;
cursor: pointer;
position: relative;
}
.breath-circle.active {
animation: breathe var(--cycle-duration, 8s) ease-in-out infinite;
}
.breath-circle .ring {
position: absolute; inset: -8px;
border-radius: 50%;
border: 1px solid transparent;
transition: border-color 0.5s;
}
.breath-circle.active .ring {
border-color: var(--accent-soft);
animation: ringPulse var(--cycle-duration, 8s) ease-in-out infinite;
}
@keyframes breathe {
0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 var(--accent-soft); }
25% { transform: scale(1.35); box-shadow: 0 0 40px 10px var(--accent-soft); }
50% { transform: scale(1.35); box-shadow: 0 0 40px 10px var(--accent-soft); }
75% { transform: scale(1); box-shadow: 0 0 0 0 var(--accent-soft); }
}
@keyframes ringPulse {
0%, 100% { transform: scale(1); opacity: 0.3; }
25% { transform: scale(1.3); opacity: 0.6; }
50% { transform: scale(1.3); opacity: 0.6; }
75% { transform: scale(1); opacity: 0.3; }
}
.breath-label {
font-size: 0.85rem;
color: var(--text-faint);
letter-spacing: 0.15em;
font-weight: 300;
min-height: 1.4em;
transition: opacity 0.5s;
}
.breath-label.inhale { color: var(--accent); }
.breath-timer-select {
display: flex; gap: 8px; justify-content: center;
margin-top: 20px;
}
.breath-timer-select button {
font-family: 'Inter', sans-serif;
font-size: 0.72rem;
font-weight: 400;
color: var(--text-faint);
background: none;
border: 1px solid var(--line);
border-radius: 100px;
padding: 5px 14px;
cursor: pointer;
transition: all 0.2s;
letter-spacing: 0.04em;
}
.breath-timer-select button:hover { color: var(--text-soft); border-color: var(--accent); }
.breath-timer-select button.active { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
.breath-countdown {
font-family: 'Inter', sans-serif;
font-size: 0.72rem;
color: var(--text-faint);
margin-top: 12px;
font-weight: 300;
min-height: 1.2em;
}
.dots {
display: flex;
justify-content: center;
gap: 12px;
margin: 48px 0;
}
.dots span {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--accent);
opacity: 0.4;
animation: pulse 3s ease-in-out infinite;
}
.dots span:nth-child(2) { animation-delay: 1s; }
.dots span:nth-child(3) { animation-delay: 2s; }
@keyframes pulse {
0%, 100% { opacity: 0.2; }
50% { opacity: 0.7; }
}
footer {
margin-top: 80px;
padding-top: 24px;
border-top: 1px solid var(--line);
text-align: center;
}
footer p {
font-family: 'Inter', sans-serif;
font-size: 0.72rem;
color: var(--text-faint);
font-weight: 300;
letter-spacing: 0.04em;
}
/* Fade in */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.page > * {
animation: fadeUp 1s ease forwards;
opacity: 0;
}
.page > *:nth-child(1) { animation-delay: 0.2s; }
.page > *:nth-child(2) { animation-delay: 0.5s; }
.page > *:nth-child(3) { animation-delay: 0.8s; }
.page > *:nth-child(4) { animation-delay: 1.1s; }
.page > *:nth-child(5) { animation-delay: 1.4s; }
.page > *:nth-child(6) { animation-delay: 1.7s; }
.page > *:nth-child(7) { animation-delay: 2.0s; }
.page > *:nth-child(8) { animation-delay: 2.3s; }
.page > *:nth-child(9) { animation-delay: 2.6s; }
@media (max-width: 540px) {
.page { padding: 80px 20px 60px; }
.highlight { padding: 20px 24px; }
}
</style>
</head>
<body>
<div class="page">
<div class="icon">🫖</div>
<h1>Warte bitte.</h1>
<div class="rule"></div>
<div class="prose">
<p>
Ich weiss, du wartest. Und Warten fuehlt sich manchmal an wie
Stillstand. Wie verlorene Zeit. Wie etwas, das nicht sein muesste.
</p>
<p>
Aber manche Dinge brauchen genau diese Zeit. Nicht weil jemand
troedelt, sondern weil <em>gute Ergebnisse nicht gehetzt werden koennen</em>.
Ein Gedanke muss reifen. Eine Loesung muss stimmen. Nicht nur schnell,
sondern richtig.
</p>
</div>
<div class="dots"><span></span><span></span><span></span></div>
<div class="highlight">
<p>
Zeit ist unser kostbarstes Gut. Wer sie gibt, gibt das Wertvollste,
was er hat. Und wer sie bekommt, sollte wissen: Da arbeitet jemand
daran, dass es sich lohnt.
</p>
</div>
<div class="prose">
<p>
Es ist okay, ungeduldig zu sein. Ungeduld zeigt, dass dir etwas
wichtig ist. Aber lass sie nicht den Blick trueben fuer das,
was gerade im Hintergrund passiert.
</p>
<p>
Gute Dinge brauchen Raum. Einen ruhigen Moment.
Einen zweiten Blick. Manchmal einen dritten.
</p>
</div>
<div class="breath-container">
<div class="breath-circle" id="breathCircle" onclick="toggleBreathing()">
<div class="ring"></div>
<span class="breath-label" id="breathLabel">start</span>
</div>
<div class="breath-timer-select">
<button onclick="setTimer(1)" id="t1">1 min</button>
<button onclick="setTimer(2)" id="t2" class="active">2 min</button>
<button onclick="setTimer(5)" id="t5">5 min</button>
<button onclick="setTimer(0)" id="t0">endlos</button>
</div>
<div class="breath-countdown" id="countdown"></div>
</div>
<script>
let breathing = false;
let timer = null;
let duration = 120;
let remaining = 0;
let phaseTimer = null;
const cycle = 8000;
const phases = [
{ label: 'einatmen', duration: 2000, cls: 'inhale' },
{ label: 'halten', duration: 2000, cls: '' },
{ label: 'ausatmen', duration: 2000, cls: '' },
{ label: 'ruhe', duration: 2000, cls: '' },
];
function setTimer(min) {
duration = min * 60;
document.querySelectorAll('.breath-timer-select button').forEach(b => b.classList.remove('active'));
document.getElementById('t' + min).classList.add('active');
if (breathing) { stopBreathing(); startBreathing(); }
}
function toggleBreathing() {
if (breathing) stopBreathing(); else startBreathing();
}
function startBreathing() {
breathing = true;
remaining = duration;
const circle = document.getElementById('breathCircle');
circle.style.setProperty('--cycle-duration', cycle + 'ms');
circle.classList.add('active');
runPhases();
if (duration > 0) {
updateCountdown();
timer = setInterval(() => {
remaining--;
updateCountdown();
if (remaining <= 0) stopBreathing();
}, 1000);
}
}
function stopBreathing() {
breathing = false;
document.getElementById('breathCircle').classList.remove('active');
document.getElementById('breathLabel').textContent = 'start';
document.getElementById('breathLabel').className = 'breath-label';
document.getElementById('countdown').textContent = '';
clearInterval(timer);
clearTimeout(phaseTimer);
}
function runPhases() {
let i = 0;
function next() {
if (!breathing) return;
const p = phases[i % phases.length];
const label = document.getElementById('breathLabel');
label.textContent = p.label;
label.className = 'breath-label' + (p.cls ? ' ' + p.cls : '');
i++;
phaseTimer = setTimeout(next, p.duration);
}
next();
}
function updateCountdown() {
if (duration === 0) { document.getElementById('countdown').textContent = ''; return; }
const m = Math.floor(remaining / 60);
const s = remaining % 60;
document.getElementById('countdown').textContent = m + ':' + String(s).padStart(2, '0');
}
</script>
<div class="closing">
<p>
Du bekommst, was du brauchst. Nur nicht genau jetzt.
Und das ist voellig in Ordnung.
</p>
</div>
<footer>
<p>wartebitte.de</p>
</footer>
</div>
</body>
</html>