Dogfather Universe
+Creator Workspace
+Privater Bereich für Creator, Scouts und Management.
+Womit meldest du dich an?
+ + + + + + ++ Verschlüsselte Verbindung · Zugriffe werden protokolliert +
+diff --git a/workspace/assets/css/gate.css b/workspace/assets/css/gate.css new file mode 100644 index 00000000..c450def0 --- /dev/null +++ b/workspace/assets/css/gate.css @@ -0,0 +1,336 @@ +/* =================================================================== + Creator Workspace - Anmeldeseite + ------------------------------------------------------------------- + Grundsatz: dunkel und ruhig. Die Farben stammen aus dem Motiv, sind + aber bewusst entsättigt - Neon als Stimmung, nicht als Blendung. + Kein reines Weiss auf Schwarz, keine harten Kanten, keine Blinker. + =================================================================== */ + +:root { + --tinte: #05070d; + --glas: rgba(10, 14, 23, 0.74); + --glas-tief: rgba(6, 9, 16, 0.86); + --rand: rgba(150, 186, 220, 0.16); + --rand-hell: rgba(150, 186, 220, 0.30); + + --text: #dbe4ef; /* kein reines Weiss - schont die Augen */ + --text-leise: #94a5bb; + --text-still: #6d7d92; + + --akzent: #3fbdf5; /* aus dem Neon des Motivs, entsättigt */ + --akzent-tief: #1f7fb0; + --violett: #8a76ff; + --warn: #ff9aa2; /* weiches Rot statt Signalrot */ + + --radius: 18px; + --radius-klein: 12px; + --schatten: 0 24px 60px -20px rgba(0, 0, 0, 0.75); + --tempo: .22s cubic-bezier(.4, 0, .2, 1); +} + +*, *::before, *::after { box-sizing: border-box; } + +html { -webkit-text-size-adjust: 100%; } + +body.gate { + margin: 0; + min-height: 100svh; + display: flex; + align-items: center; + background: var(--tinte); + color: var(--text); + font-family: "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif; + font-size: 16px; + line-height: 1.55; + letter-spacing: .01em; + overflow-x: hidden; +} + +/* ---------- Bühne ---------------------------------------------------- */ + +.buehne { position: fixed; inset: 0; z-index: 0; } + +.buehne__bild { + width: 100%; height: 100%; + object-fit: cover; + object-position: center; + /* Minimal abgedunkelt und entsättigt, damit der Text darüber ruhig liegt. */ + filter: saturate(.92) brightness(.9); + transform: scale(1.03); /* Reserve für die Parallaxe */ + transition: transform 1.4s cubic-bezier(.2, .7, .2, 1); + will-change: transform; +} + +/* Der Schleier macht die linke Seite lesbar, ohne das Motiv zu erschlagen. + Rechts bleibt er fast durchsichtig - dort stehen Dogfather und Hasi Dog. */ +.buehne__schleier { + position: absolute; inset: 0; + /* Viele weiche Stufen statt weniger harter - sonst zeichnet sich die Kante + des Verlaufs als sichtbare Diagonale quer durchs Bild. */ + background: + linear-gradient(96deg, + rgba(3, 5, 10, 0.93) 0%, + rgba(3, 5, 10, 0.90) 18%, + rgba(3, 5, 10, 0.82) 28%, + rgba(4, 6, 12, 0.68) 36%, + rgba(4, 6, 12, 0.50) 44%, + rgba(4, 6, 12, 0.33) 52%, + rgba(4, 6, 12, 0.19) 60%, + rgba(4, 6, 12, 0.09) 68%, + rgba(4, 6, 12, 0.03) 78%, + rgba(4, 6, 12, 0.00) 88%), + radial-gradient(120% 90% at 50% 120%, rgba(0, 0, 0, .42), transparent 62%); +} + +/* ---------- Tafel ---------------------------------------------------- */ + +.tafel { + position: relative; + z-index: 1; + margin-left: clamp(24px, 7vw, 104px); + width: min(452px, 44vw); + padding: clamp(28px, 3vw, 40px) clamp(24px, 2.6vw, 36px) clamp(24px, 2.6vw, 32px); + background: var(--glas); + backdrop-filter: blur(22px) saturate(115%); + -webkit-backdrop-filter: blur(22px) saturate(115%); + border: 1px solid var(--rand); + border-radius: var(--radius); + box-shadow: var(--schatten); + animation: auftauchen .7s cubic-bezier(.2, .7, .2, 1) both; +} + +/* Feine Lichtkante links - nimmt das Neon des Motivs auf, ohne zu leuchten. */ +.tafel__kante { + position: absolute; left: -1px; top: 16%; bottom: 16%; + width: 2px; border-radius: 2px; + background: linear-gradient(180deg, + transparent, var(--akzent) 22%, var(--violett) 72%, transparent); + opacity: .55; +} + +@keyframes auftauchen { + from { opacity: 0; transform: translateY(14px); } + to { opacity: 1; transform: none; } +} + +/* ---------- Kopf ----------------------------------------------------- */ + +.marke { + margin: 0 0 6px; + font-size: .74rem; font-weight: 700; + letter-spacing: .18em; text-transform: uppercase; + color: var(--akzent); +} + +.titel { + margin: 0 0 8px; + font-size: clamp(1.6rem, 2.5vw, 2rem); + font-weight: 700; line-height: 1.15; + letter-spacing: -.01em; +} + +.unterzeile { margin: 0 0 26px; color: var(--text-leise); font-size: .94rem; } + +/* ---------- Rollen --------------------------------------------------- */ + +.feldschild { + margin: 0 0 10px; + font-size: .78rem; font-weight: 600; + letter-spacing: .06em; text-transform: uppercase; + color: var(--text-still); +} + +.rollen { margin-bottom: 22px; } + +.rolle { + display: flex; align-items: center; gap: 13px; + width: 100%; margin-bottom: 8px; padding: 11px 14px; + background: rgba(255, 255, 255, .035); + border: 1px solid var(--rand); + border-radius: var(--radius-klein); + color: var(--text); + font: inherit; text-align: left; cursor: pointer; + transition: border-color var(--tempo), background var(--tempo), transform var(--tempo); +} +.rolle:hover { background: rgba(255, 255, 255, .06); border-color: var(--rand-hell); } +.rolle:active { transform: scale(.995); } + +.rolle[aria-checked="true"] { + background: linear-gradient(100deg, rgba(63, 189, 245, .14), rgba(138, 118, 255, .10)); + border-color: rgba(63, 189, 245, .55); +} + +.rolle__zeichen { + flex: 0 0 auto; width: 21px; height: 21px; + fill: none; stroke: var(--text-still); + stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; + transition: stroke var(--tempo); +} +.rolle__zeichen .strich { fill: none; } +.rolle[aria-checked="true"] .rolle__zeichen { stroke: var(--akzent); } + +.rolle__text { display: flex; flex-direction: column; min-width: 0; } +.rolle__name { font-weight: 600; font-size: .95rem; } +.rolle__zusatz { font-size: .8rem; color: var(--text-still); } + +/* ---------- Formular ------------------------------------------------- */ + +.fuer { color: var(--akzent); text-transform: none; letter-spacing: normal; } + +.feld { position: relative; } + +#code { + width: 100%; padding: 13px 46px 13px 15px; + background: rgba(255, 255, 255, .045); + border: 1px solid var(--rand); + border-radius: var(--radius-klein); + color: var(--text); + font: inherit; letter-spacing: .06em; + transition: border-color var(--tempo), background var(--tempo), box-shadow var(--tempo); +} +#code::placeholder { color: var(--text-still); letter-spacing: normal; } +#code:hover { border-color: var(--rand-hell); } +#code:focus { + outline: none; + border-color: rgba(63, 189, 245, .7); + background: rgba(255, 255, 255, .065); + box-shadow: 0 0 0 3px rgba(63, 189, 245, .16); +} + +.feld__auge { + position: absolute; right: 6px; top: 50%; transform: translateY(-50%); + width: 36px; height: 36px; padding: 8px; + background: none; border: 0; border-radius: 9px; + cursor: pointer; color: var(--text-still); + transition: color var(--tempo), background var(--tempo); +} +.feld__auge:hover { color: var(--text-leise); background: rgba(255, 255, 255, .06); } +.feld__auge svg { width: 100%; height: 100%; fill: none; stroke: currentColor; + stroke-width: 1.7; stroke-linecap: round; } +.feld__auge .durch { opacity: 0; transition: opacity var(--tempo); } +.feld__auge[aria-pressed="true"] .durch { opacity: 1; } + +.hinweis, .fehler { + margin: 9px 2px 0; font-size: .84rem; min-height: 0; +} +.hinweis { color: var(--text-leise); } +.fehler { color: var(--warn); } +.fehler:empty { margin: 0; } + +.knopf { + position: relative; + width: 100%; margin-top: 18px; padding: 13px 18px; + background: linear-gradient(100deg, var(--akzent-tief), #2f6fa8 55%, var(--violett)); + border: 0; border-radius: var(--radius-klein); + color: #f2f7fc; font: inherit; font-weight: 650; letter-spacing: .02em; + cursor: pointer; + transition: filter var(--tempo), transform var(--tempo); +} +.knopf:hover { filter: brightness(1.1); } +.knopf:active { transform: scale(.99); } +.knopf[disabled] { cursor: progress; filter: saturate(.6) brightness(.85); } + +.knopf__laden { + position: absolute; right: 16px; top: 50%; margin-top: -8px; + width: 16px; height: 16px; border-radius: 50%; + border: 2px solid rgba(255, 255, 255, .35); + border-top-color: #fff; + opacity: 0; + animation: dreh .8s linear infinite; +} +.knopf[disabled] .knopf__laden { opacity: 1; } +@keyframes dreh { to { transform: rotate(360deg); } } + +.fuss { + margin: 22px 0 0; padding-top: 16px; + border-top: 1px solid var(--rand); + font-size: .78rem; color: var(--text-still); text-align: center; +} + +/* ---------- Übergang beim erfolgreichen Anmelden --------------------- + Die Tafel tritt zurück, das Motiv wird kurz frei - als würde sich das + Tor öffnen. Bewusst kurz und ohne Aufblitzen. */ + +body.oeffnet .tafel { + opacity: 0; transform: translateY(-8px) scale(.985); + transition: opacity .26s ease, transform .26s ease; +} +body.oeffnet .buehne__schleier { opacity: .35; transition: opacity .26s ease; } +body.oeffnet .buehne__bild { filter: saturate(1) brightness(1); transition: filter .26s ease; } + +/* ---------- Tastatur-Bedienung --------------------------------------- */ + +:where(button, input):focus-visible { + outline: 2px solid var(--akzent); + outline-offset: 2px; +} + +/* ---------- Schmale oder hohe Fenster -------------------------------- + Sobald das Fenster nicht mehr deutlich breiter als hoch ist, würde die + Tafel den Figuren zu nahe kommen. Dann wechselt die Seite auf das + Hochformat-Motiv und die Tafel rückt nach unten - die beiden bleiben + oben im Bild sichtbar. */ + +/* Gemeinsam: Tafel wandert nach unten, Motiv bleibt darüber frei. */ +@media (max-width: 899px), (max-aspect-ratio: 7/5) { + body.gate { align-items: flex-end; justify-content: center; } + .tafel { + margin: 0 0 max(18px, env(safe-area-inset-bottom)); + width: min(520px, calc(100vw - 32px)); + } + .tafel__kante { display: none; } +} + +/* Schmale Geräte: Hochformat-Ausschnitt. Die Bildfläche endet oberhalb der + Tafel - dadurch liegt ihr Seitenverhältnis nah am Ausschnitt und der + Browser muss seitlich kaum etwas wegschneiden. Beide Figuren bleiben ganz + im Bild, der Verlauf blendet nach unten ins Dunkle. */ +@media (max-width: 899px) { + .buehne { bottom: auto; height: 66svh; } + .buehne__bild { object-position: center top; } + .buehne__schleier { + background: + linear-gradient(180deg, + rgba(3, 5, 10, 0.30) 0%, + rgba(3, 5, 10, 0.10) 20%, + rgba(3, 5, 10, 0.22) 46%, + rgba(3, 5, 10, 0.55) 66%, + rgba(5, 7, 13, 0.88) 82%, + /* Muss vor der Unterkante voll deckend sein und exakt der Seitenfarbe + entsprechen - sonst zeichnet sich dort eine feine Kante ab. */ + rgba(5, 7, 13, 1.00) 94%); + } +} + +/* Breite, aber hohe Fenster (z. B. 1100x980): Querformat bleibt und füllt die + ganze Fläche - sonst entsteht unten eine tote dunkle Zone. Weil hier + seitlich beschnitten wird, rückt das Motiv nach rechts, damit Dogfather und + Hasi Dog vollständig im Bild bleiben. */ +@media (min-width: 900px) and (max-aspect-ratio: 7/5) { + .buehne__bild { object-position: 82% center; } + .buehne__schleier { + background: + linear-gradient(180deg, + rgba(3, 5, 10, 0.12) 0%, + rgba(3, 5, 10, 0.20) 34%, + rgba(3, 5, 10, 0.48) 56%, + rgba(3, 5, 10, 0.82) 74%, + rgba(3, 5, 10, 0.95) 90%); + } +} + +@media (max-width: 420px) { + .tafel { padding: 22px 18px 20px; border-radius: 16px; } + .rolle__zusatz { display: none; } +} + +/* ---------- Weniger Bewegung ----------------------------------------- */ + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: .001ms !important; + animation-iteration-count: 1 !important; + transition-duration: .001ms !important; + } + .buehne__bild { transform: none; transition: none; } +} diff --git a/workspace/assets/img/buehne/gate-1280.webp b/workspace/assets/img/buehne/gate-1280.webp new file mode 100644 index 00000000..a285e30f Binary files /dev/null and b/workspace/assets/img/buehne/gate-1280.webp differ diff --git a/workspace/assets/img/buehne/gate-1600.webp b/workspace/assets/img/buehne/gate-1600.webp new file mode 100644 index 00000000..e1b7e237 Binary files /dev/null and b/workspace/assets/img/buehne/gate-1600.webp differ diff --git a/workspace/assets/img/buehne/gate-960.webp b/workspace/assets/img/buehne/gate-960.webp new file mode 100644 index 00000000..a0d50c03 Binary files /dev/null and b/workspace/assets/img/buehne/gate-960.webp differ diff --git a/workspace/assets/img/buehne/gate-hoch-440.webp b/workspace/assets/img/buehne/gate-hoch-440.webp new file mode 100644 index 00000000..c17fde69 Binary files /dev/null and b/workspace/assets/img/buehne/gate-hoch-440.webp differ diff --git a/workspace/assets/img/buehne/gate-hoch-580.webp b/workspace/assets/img/buehne/gate-hoch-580.webp new file mode 100644 index 00000000..62378448 Binary files /dev/null and b/workspace/assets/img/buehne/gate-hoch-580.webp differ diff --git a/workspace/assets/img/favicon.png b/workspace/assets/img/favicon.png new file mode 100644 index 00000000..56232a63 Binary files /dev/null and b/workspace/assets/img/favicon.png differ diff --git a/workspace/assets/js/gate.js b/workspace/assets/js/gate.js new file mode 100644 index 00000000..a7dcb74c --- /dev/null +++ b/workspace/assets/js/gate.js @@ -0,0 +1,155 @@ +/* =================================================================== + Creator Workspace - Anmeldeseite + Kein Framework, keine externen Abhängigkeiten. + =================================================================== */ +(() => { + 'use strict'; + + const $ = (id) => document.getElementById(id); + const rollen = Array.from(document.querySelectorAll('.rolle')); + const formular = $('formular'); + const code = $('code'); + const auge = $('auge'); + const fehler = $('fehler'); + const hinweis = $('hinweis'); + const knopf = $('knopf'); + const fuer = $('fuer'); + + const NAMEN = { admin: 'Management', creator: 'Creator', scout: 'Scout' }; + const SPEICHER = 'dfw.rolle'; + let gewaehlt = null; + + /* ---------- Rolle wählen ------------------------------------------ */ + + function waehle(rolle, fokus = false) { + gewaehlt = rolle; + rollen.forEach((b) => b.setAttribute('aria-checked', String(b.dataset.rolle === rolle))); + fuer.textContent = `· ${NAMEN[rolle]}`; + fuer.hidden = false; + melde(''); + try { localStorage.setItem(SPEICHER, rolle); } catch { /* Privater Modus */ } + if (fokus) code.focus(); + } + + rollen.forEach((b) => { + b.addEventListener('click', () => waehle(b.dataset.rolle, true)); + // Pfeiltasten wie bei echten Radio-Feldern + b.addEventListener('keydown', (e) => { + const i = rollen.indexOf(b); + let ziel = null; + if (e.key === 'ArrowDown' || e.key === 'ArrowRight') ziel = rollen[(i + 1) % rollen.length]; + if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') ziel = rollen[(i - 1 + rollen.length) % rollen.length]; + if (!ziel) return; + e.preventDefault(); + ziel.focus(); + waehle(ziel.dataset.rolle); + }); + }); + + // Zuletzt genutzte Rolle vorauswählen - spart bei täglicher Nutzung einen Klick. + try { + const zuletzt = localStorage.getItem(SPEICHER); + if (zuletzt && NAMEN[zuletzt]) waehle(zuletzt); + } catch { /* egal */ } + + /* ---------- Code anzeigen / verbergen ------------------------------ */ + + auge.addEventListener('click', () => { + const sichtbar = code.type === 'text'; + code.type = sichtbar ? 'password' : 'text'; + auge.setAttribute('aria-pressed', String(!sichtbar)); + auge.setAttribute('aria-label', sichtbar ? 'Code anzeigen' : 'Code verbergen'); + code.focus(); + }); + + /* ---------- Feststelltaste ----------------------------------------- */ + + const pruefeFeststell = (e) => { + if (typeof e.getModifierState !== 'function') return; + hinweis.hidden = !e.getModifierState('CapsLock'); + }; + code.addEventListener('keyup', pruefeFeststell); + code.addEventListener('keydown', pruefeFeststell); + code.addEventListener('blur', () => { hinweis.hidden = true; }); + + /* ---------- Rückmeldung -------------------------------------------- */ + + function melde(text) { fehler.textContent = text; } + + function laden(an) { + knopf.disabled = an; + knopf.querySelector('.knopf__text').textContent = an ? 'Prüfe …' : 'Anmelden'; + } + + /* ---------- Absenden ------------------------------------------------ */ + + formular.addEventListener('submit', async (e) => { + e.preventDefault(); + const wert = code.value.trim(); + + if (!gewaehlt) { melde('Bitte zuerst auswählen, womit du dich anmeldest.'); rollen[0].focus(); return; } + if (!wert) { melde('Bitte den Zugangscode eingeben.'); code.focus(); return; } + + melde(''); + laden(true); + + try { + const antwort = await fetch('/api/anmelden', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ rolle: gewaehlt, code: wert }), + credentials: 'same-origin', + }); + + if (antwort.ok) { + const ziel = (await antwort.json().catch(() => ({}))).weiter || '/workspace'; + document.body.classList.add('oeffnet'); + setTimeout(() => { location.assign(ziel); }, 260); + return; + } + + // 404 heißt: Es gibt noch gar keine Gegenstelle. Das ist derzeit der + // Normalfall - die Seite steht schon, der Login-Server wird noch gebaut. + // Eine ehrliche Meldung ist hier besser als "Code stimmt nicht". + if (antwort.status === 404) { + melde('Der Zugang wird gerade eingerichtet – Anmelden ist noch nicht möglich.'); + return; + } + + // Bewusst keine Auskunft darüber, ob Rolle oder Code falsch war. + melde(antwort.status === 429 + ? 'Zu viele Versuche. Bitte kurz warten.' + : 'Zugangscode stimmt nicht.'); + code.select(); + } catch { + melde('Keine Verbindung zum Server. Bitte erneut versuchen.'); + } finally { + laden(false); + } + }); + + /* ---------- Sehr dezente Parallaxe ---------------------------------- */ + + const ruhig = matchMedia('(prefers-reduced-motion: reduce)'); + const bild = document.querySelector('.buehne__bild'); + const grob = matchMedia('(pointer: coarse)'); + + if (bild && !ruhig.matches && !grob.matches) { + let ziel = { x: 0, y: 0 }, ist = { x: 0, y: 0 }, laeuft = false; + + addEventListener('pointermove', (e) => { + ziel.x = (e.clientX / innerWidth - 0.5) * -10; // max. 10 px + ziel.y = (e.clientY / innerHeight - 0.5) * -6; + if (!laeuft) { laeuft = true; requestAnimationFrame(schritt); } + }, { passive: true }); + + function schritt() { + ist.x += (ziel.x - ist.x) * 0.06; + ist.y += (ziel.y - ist.y) * 0.06; + bild.style.transform = `scale(1.03) translate3d(${ist.x.toFixed(2)}px, ${ist.y.toFixed(2)}px, 0)`; + if (Math.abs(ziel.x - ist.x) > 0.05 || Math.abs(ziel.y - ist.y) > 0.05) { + requestAnimationFrame(schritt); + } else { laeuft = false; } + } + } +})(); diff --git a/workspace/index.html b/workspace/index.html new file mode 100644 index 00000000..f314eca2 --- /dev/null +++ b/workspace/index.html @@ -0,0 +1,131 @@ + + +
+ + +Dogfather Universe
+Privater Bereich für Creator, Scouts und Management.
+Womit meldest du dich an?
+ + + + + + ++ Verschlüsselte Verbindung · Zugriffe werden protokolliert +
+