Compare commits

...
55 Commits
Author SHA1 Message Date
DogFatherGitandClaude Opus 5 5a5912636d Registrierung: E-Mail-Bestätigung per Einmalcode vor dem Zugangscode
Nutzer-Wunsch 21.08.2026: "bei der ersten registrierung sollen die eine
email bekommen mit einem einmaligen code damit wir auch wissen dass
email stimmt und danach wenn sie den code eingegeben haben sollen die
erst ihren zugangscode auswählen/eintippen können."

Damit wird eine echte Lücke geschlossen: registerSupporter() hat bisher
`email_verified = 1` gesetzt, OHNE dass irgendetwas geprüft wurde -- man
konnte sich mit einer fremden oder erfundenen Adresse registrieren und
kam sofort rein.

Neuer Ablauf in drei Schritten:
1. Name/TikTok/E-Mail -> Konto wird als UNBESTÄTIGT angelegt
   (email_verified = 0, noch kein Zugangscode). Es kommt bewusst KEIN
   Session-Token zurück -- eingeloggt ist man hier noch nicht.
2. Einmalcode aus der E-Mail eingeben -> verify-email bestätigt und
   loggt ein. Die Willkommens-Mail wandert hierher, sie ging vorher an
   eine noch ungeprüfte Adresse.
3. Erst jetzt den eigenen Zugangscode festlegen.

Serverseitig abgesichert: setSupporterAccessCode() lehnt ab, solange die
E-Mail nicht bestätigt ist -- der Schritt ist damit nicht nur im
Formular versteckt, sondern auch per Direktaufruf nicht überspringbar.

Wiederverwendet wird die bereits vorhandene Mechanik (issueAuthCode/
verifyAuthCode mit Zweck "verify_email", sendVerifyEmailCode, die Panels
#panel-code und #panel-neuer-code) -- der "Code vergessen?"-Weg nutzt
dieselbe Code-Eingabe und bleibt unverändert; eine neue Variable
codeZweck unterscheidet, welcher Endpunkt aufgerufen wird.

Mit Playwright end-to-end geprüft (14 Tests): Reihenfolge der Aufrufe,
kein Token vor der Bestätigung, falscher Code kommt nicht weiter,
Zugangscode-Feld erst im letzten Schritt, "Code vergessen?" unberührt.

Cache-Busting-Version auf 20260821o erhöht.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-21 17:48:08 +02:00
DogFatherGitandClaude Opus 5 5d0ea11570 Arbeit der parallelen Session in git gesichert (war nur auf dem Server)
Diese Änderungen liefen bereits live auf dem Server, lagen dort aber
ausschließlich als nicht eingecheckte Arbeitskopie — bei jedem Deploy
(stash/pull/pop) und bei jedem Serverproblem wären sie verloren gewesen.
Deshalb hier unverändert in git übernommen, bevor darauf aufgebaut wird.

Enthalten (nicht von mir gebaut, nur gesichert):
- Supporter: eigener fester Zugangscode statt Einmalcode-Login
  (Migration 0009, lib/crypto.js scrypt-Hash, routes/supporter.js,
  abonnieren.html, supporter.html, i18n-abonnieren/-supporter)
- Stimmen: Profilbilder (Migration 0010, routes/testimonials.js)
- Event-Bild-Upload: voller Pfad statt relativem (routes/events.js)
- Neue/überarbeitete Hintergrundbilder für viele Seiten
- Teilen-Funktion (streamplan.js, i18n-index/-streamplan, main.css)

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-21 17:35:14 +02:00
DogFatherGitandClaude Sonnet 5 4b294f6c6b Startseite: die vier Personen-Kacheln unter "Team Dogi" entfernt
Nutzer-Wunsch 21.08.2026: "die 4 kacheln da sollen da weg bitte."
Überschrift, Beschreibungstext und der Knopf zum Modi-Team bleiben --
dadurch kommt das "Team Dogi"-Hintergrundbild dieser Sektion jetzt
unverdeckt zur Geltung. Der zugehörige Render-Code ist ebenfalls raus;
data-modis.js/data-scouts.js bleiben eingebunden, weil die
Statistik-Zeile weiter oben sie weiterhin auszählt (geprüft: zeigt
weiterhin korrekt "8+"). Die Profile selbst gibt es unverändert
vollständig auf team-modis.html.

Cache-Busting-Version auf 20260821n erhöht.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 16:53:56 +02:00
DogFatherGitandClaude Sonnet 5 d0f3edb079 bewerben.html: Überschrift-Umbruch verbessert (kein einsames letztes Wort mehr)
Nutzer-Wunsch 21.08.2026: "Weg" stand auf manchen Bildschirmbreiten
allein in einer eigenen Zeile, sah unruhig aus. Non-breaking-Space
zwischen den letzten zwei Wörtern in allen 5 Sprachen -- bricht die
Zeile jetzt nie mehr genau zwischen ihnen um.

Cache-Busting-Version auf 20260821m erhöht.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 16:51:00 +02:00
DogFatherGitandClaude Sonnet 5 76f70ffcf2 Zeitreise: Namen "Cidem" zu "Cidgem" korrigiert (Creator Cup)
Nutzer-Wunsch 21.08.2026: Tippfehler beim Namen des Kooperationspartners
beim Creator Cup korrigiert, in allen 5 Sprachen plus dem
HTML-Fallbacktext.

Cache-Busting-Version auf 20260821l erhöht.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 12:58:29 +02:00
DogFatherGitandClaude Sonnet 5 971c5ea3f6 Bugfix: Team-Foto-Upload lieferte relativen statt vollen Pfad zurück
Gleicher Fehler wie in routes/events.js/uploadEventImage (dort gerade
direkt auf dem Server gefunden und gefixt, siehe git stash auf
dogiintern): die Verwaltung und die echte Website laufen auf
dogfather-universe.com (dogiweb), hochgeladene Fotos liegen aber unter
postfach.dogfather-universe.com (dogiintern). Ein relativer Pfad wurde
vom Browser also gegen die falsche Domain aufgelöst -> kaputtes
Bild-Symbol für jedes über die Team-Verwaltung hochgeladene Foto.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 04:37:04 +02:00
DogFatherGitandClaude Sonnet 5 81f55d67db Team-Verwaltung Runde 2: bestehende Mitglieder importieren + Kategorien/Seitentitel editierbar
Nutzer-Wunsch 21.08.2026: "wenn ich eine änderung mache ist es auch für
jeden für die die schon da sind und die neuen" + "ich will ich titel
und namen von kategorien und seiten ändern können in der verwaltung".

Backend (braucht Filipes manuellen Deploy):
- Migration 0008: team_members bekommt intro/bioHtml/extraCta-Spalten
  -- volle Feld-Parität mit den von Hand gepflegten Profilen (Diene/
  Patrick/Bananenstift/Marina nutzen diese Felder).
- routes/team.js: neue importLegacyMember()-Funktion -- übernimmt ein
  bestehendes Profil 1:1 in die Datenbank, OHNE erneut zu übersetzen
  (die vorhandenen, von Hand geschriebenen Übersetzungen bleiben
  erhalten). Idempotent: mehrfacher Import erzeugt keine Duplikate.
- routes/site-texts.js (neu): admin-editierbare Kategorie-Namen und
  Seitentitel, generischer key->{de,...}-Override über app_settings
  (wie "Event des Jahres"), fester Schlüssel-Katalog aus
  Sicherheitsgründen. Leeres Feld setzt auf den Standardtext zurück.
- 27 Tests in isolierter Testumgebung geprüft (kein better-sqlite3
  lokal kompilierbar).

Frontend:
- window.dogiTeamZusammenfuehren() (main.js): admin-Mitglieder
  ÜBERSCHREIBEN jetzt gleichnamige statische Einträge (per slug) statt
  sie zu duplizieren -- eine Bearbeitung wirkt dadurch für alle.
- window.dogiSiteTexteLaden() + applyTranslations() erweitert um
  data-site-text-key -- Kategorie-Seitentitel (team-modis.html/
  team-scouts.html/creator.html/manager.html) sind jetzt live editierbar.
- Verwaltung: "📥 Bestehende Mitglieder importieren"-Knopf (holt VanVan/
  Diene/Funny/Miss/Marina/Ghost/Patrick/Bananenstift aus den data-*.js-
  Dateien, VanVan bewusst ausgenommen -- eigene Sonderkarte + Seite),
  erweitertes Formular (Intro/ausführliche Vorstellung/zweiter Button,
  eingeklappt unter "Erweitert"), neue Sektion "Kategorien &
  Seitentitel" (4 Karten, sofort wirksam auf Website UND Verwaltung).

Beim Testen einen echten UX-Bug gefunden und gefixt: die "Gespeichert"-
Meldung nach dem Speichern eines Mitglieds wurde von der direkt
anschließenden Formular-Zurücksetzung sofort wieder überschrieben und
war nie sichtbar.

Cache-Busting-Version auf 20260821k erhöht.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 03:47:51 +02:00
DogFatherGitandClaude Sonnet 5 7060a4a6ae Verwaltungsseite perfektioniert: Team-Verwaltung für Modis/Scouts/Creator/Manager
Nutzer-Wunsch 21.08.2026: "überall wo Modis sind oder Scouts oder
Manager, oder Creator, will ich dass ich die easy über meine
Verwaltungsseite hinzufüge und die automatisch in der Website
hinzugefügt werden ... auch mit Fotos und TikTok Link."

Backend (server-internal, braucht Filipes manuellen Deploy):
- Neue Tabelle team_members (Migration 0007) für alle vier Kategorien
  gemeinsam -- ergänzt, überschreibt NIE die von Hand gepflegten
  Einträge in data-modis.js/data-scouts.js/data-creator.js.
- routes/team.js: öffentliches Lesen (nur aktive Mitglieder, optional
  nach Kategorie gefiltert), TEAM_MANAGE-geschütztes Anlegen/Bearbeiten/
  Löschen, Foto-Upload (gleiches Muster wie "Event des Jahres"),
  automatische Übersetzung von Rolle/Bio/Zitat wie bei anderen
  admin-gepflegten Texten. Slug global eindeutig (mit automatischer
  Kollisionsauflösung), TikTok-Kurzname wird zu voller URL ergänzt.
  27 Tests in isolierter Testumgebung geprüft (kein better-sqlite3
  lokal kompilierbar).

Frontend:
- window.dogiTeamLaden() in main.js: holt admin-gepflegte Mitglieder
  einer (oder aller) Kategorien und bringt sie in exakt dieselbe Form
  wie die bestehenden data-*.js-Einträge.
- team-scouts.html/team-modis.html/creator.html hängen das Ergebnis
  einfach an ihre bestehenden Arrays an und rendern erneut -- exakt
  derselbe Look wie die schon bestehenden Profile (Foto/Avatar-Initiale,
  Rolle, Zitat, TikTok-Button). creator.html hatte bisher ein "return"
  bei leerem CREATORS-Array, wodurch admin-Creator NIE geladen worden
  wären -- gefixt. team-modis.html berücksichtigt Rang/Rang-Bezeichnung
  für die Pyramide.
- Neue Sektion "Weitere Manager" auf manager.html, komplett unsichtbar
  bis der erste Manager angelegt wird (data-manager.js neu, wie
  data-creator.js aktuell leer).
- profil.html sucht jetzt kategorieübergreifend auch in admin-gepflegten
  Profilen, inkl. korrektem Theme/Zurück-Link auch für Manager.

Verwaltung: neue Sektion "Team verwalten" (TEAM_MANAGE-Berechtigung) --
ein Formular für alle vier Kategorien mit Foto-Sofort-Upload,
TikTok-Feld, Bio/Zitat, bedingten Rang-Feldern (nur Modi), Kategorie-
Filterleiste und Bearbeiten/Löschen pro Eintrag.

Beim Testen mit Playwright einen echten Syntaxfehler gefunden und
gefixt (ASCII-Anführungszeichen statt schließendem „" in einem
Statustext), der das GESAMTE Verwaltungs-Skript und damit die komplette
Seite lahmgelegt hätte.

Cache-Busting-Version auf 20260821j erhöht.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 03:06:21 +02:00
DogFatherGitandClaude Sonnet 5 8ead1b750b KRITISCH: echten Ursprung der "rohen Übersetzungs-Schlüssel"-Bugs gefunden und behoben
Beide heutigen Meldungen ("pr_quote_open...", "ts_profil_ansehen") waren
KEIN Caching-Problem, sondern ein echter Bug in main.js: nach Klick auf
einen internen Link und dann "Zurück" (oder generell jede zweite Seite
innerhalb einer Browser-Sitzung) wurde die seiteneigene i18n-*.js-Datei
nie erneut ausgeführt -> window.I18N_PAGE blieb auf null -> jede
Übersetzung fiel auf den rohen Schlüsselnamen zurück.

Ursache: skripteUebernehmen() erkennt i18n-Dateien am Muster ".js$"
(String-Ende). Seit die Cache-Busting-Version ("?v=...") an jede
Skript-URL angehängt wird, endet der echte src-Wert aber nie mehr auf
".js", sondern auf ".js?v=...". Der Test schlug seitdem für JEDE
i18n-Datei fehl und sie wurde wie eine normale, schon geladene Datei
behandelt und beim nächsten Seitenwechsel übersprungen.

Mit Playwright reproduziert (Klick auf Profil-Kachel + Zurück-Button)
und nach dem Fix erneut verifiziert -- betraf praktisch jede Seite mit
eigenem i18n-*.js beim Navigieren per Klick, nicht nur team-scouts.html.

Cache-Busting-Version auf 20260821i erhöht.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 02:12:24 +02:00
DogFatherGitandClaude Sonnet 5 fbd11b7be4 "Was gerade läuft"-Aktionen-Teaser von der Startseite entfernt
Nutzer-Wunsch 21.08.2026: Überschrift + Button "Alle Aktionen & Projekte"
auf index.html weg. data-aktionen.js-Script-Tag (nur noch dafür gebraucht)
und die zugehörigen JS-Renderzeilen ebenfalls entfernt, dazugehörige
i18n-Keys (idx_aktionen_h2/idx_aktionen_btn) aus i18n-index.js aufgeräumt.
aktion-detail.html und data-aktionen.js selbst bleiben unangetastet.

Cache-Busting-Version auf 20260821h erhöht.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 02:04:43 +02:00
DogFatherGitandClaude Sonnet 5 ca5793e79f Patricks Profil-Abschluss um "Scout von Dogfather" ergänzt (wie Bananenstift)
Nutzer-Wunsch 21.08.2026: Patricks Bio endete nur mit "- euer Patrick",
Bananenstift hat zusätzlich eine zweite Zeile "Scout von Dogfather".
Für Konsistenz zwischen beiden Scout-Profilen in allen 5 Sprachen ergänzt.

Cache-Busting-Version auf 20260821g erhöht.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 01:53:57 +02:00
DogFatherGitandClaude Sonnet 5 c7904ccd62 TikTok-Buttons auf Patrick- und Bananenstift-Scout-Profilen ergänzt
Nutzer-Wunsch 21.08.2026: unter dem Namen/Rolle-Text je ein TikTok-Button
(@patrick180585 bzw. @bananenstift009). Beide nutzen den bereits
bestehenden social-Mechanismus in profil.html (social.tiktok -> echter
.btn-tiktok-Button), kein neuer Code nötig.

Cache-Busting-Version auf 20260821f erhöht (data-scouts.js ist .js und
damit von Cloudflares erzwungenem Edge-Cache betroffen).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 01:49:00 +02:00
DogFatherGitandClaude Sonnet 5 ac01a74d08 Zeitreise: neue Station "Erstes Fan-Treffen: Team TiliDog" (25. Juli 2026)
Nutzer-Wunsch 21.08.2026: erstes Fan-Treffen von Dogi & Tili (Team
TiliDog) in Wuppertal ergänzt -- gleichzeitig das erste persönliche
Aufeinandertreffen von DogFather und Tili. Als Meilenstein markiert und
chronologisch korrekt zwischen "Juli 2026 — Start als Manager" und
"27. Juli bis 2. August 2026 — Creator Cup" einsortiert.

Cache-Busting-Version auf 20260821e erhöht.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 01:39:47 +02:00
DogFatherGitandClaude Sonnet 5 360f19a52d Echtes Foto von Filipe in der Intro-Sektion auf streamer.html ergänzt
Nutzer-Wunsch 21.08.2026: "füge das foto was ich am ende mitschicke
neben den text im screen1". Sektion von einspaltigem Fließtext auf
grid-2 umgebaut, Foto (assets/img/streamer-filipe-portrait.jpg, aus
Pictures/Filipe, auf 1000x1500 verkleinert) rechts daneben mit
frame-gold-Rahmen für Abwechslung zur Casper-Sektion (frame-lila).

Cache-Busting-Version auf 20260821d erhöht.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 01:29:03 +02:00
DogFatherGitandClaude Sonnet 5 6106cf9f03 Modi-Gang TikTok-Button von streamer.html nach team-modis.html verschoben
Der Button gehört thematisch besser zum Modi-Team als zur Casper-Sektion
auf streamer.html (Nutzer-Wunsch 21.08.2026: "den knopf da weg bitte und
in die modie seite zu den modis hinzufügen und perfekt in die seite
anpassen"). Jetzt im Hero von team-modis.html, unter dem Lead-Text,
zentriert im bestehenden btn-row-Muster der Seite.

Cache-Busting-Version auf 20260821c erhöht.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 01:24:43 +02:00
DogFatherGitandClaude Sonnet 5 ed9b373a74 Husky-Bild auf streamer.html an Texthöhe angepasst statt fixem Mini-Deckel
Vorheriger max-height-Deckel (460px) fürs sehr hochkantige
streamer-mascot.jpg (900x2812) war zu knapp und ließ eine sichtbare
Lücke neben dem längeren Text ("die größe vom bild vom husky soll dem
text rechts nebendran angepasst werden"). Deckel auf 720px erhöht
(orientiert an der typischen Texthöhe in diesem Abschnitt), Grid-Stretch-
Ansatz ausprobiert und wegen Rückkopplung mit dem extremen
Seitenverhältnis verworfen (siehe Kommentare in main.css/streamer.html).

Cache-Busting-Version auf 20260821b erhöht (Cloudflare cached CSS/JS
sonst bis zu 4h am Edge).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-21 01:21:31 +02:00
DogFatherGitandClaude Opus 5 1f87b54351 Nav umbenannt/umsortiert + Stimmen-Kachel und Zitat-Karten aufgewertet
Nutzer-Wuensche 20.08.2026 (2. Runde):
- Kategorie "DogFather" -> "Filipe", Kategorie "HasiDog" -> "Dogi&Hasi".
- Unterpunkt "Streamer" (streamer.html) aus "Filipe" heraus nach
  "Dogi&Hasi" verschoben, dort GANZ NACH OBEN (ueber "HasiDog") und in
  "DogFather" umbenannt.
- Unterpunkt "HasiDog-Welt" -> "HasiDog".
- Fusszeile zieht mit: Spaltenueberschrift nutzt jetzt dieselbe
  Kategorie-Bezeichnung wie die Nav ("Filipe" / "Dogi&Hasi & Manager"),
  streamer.html steht dort ebenfalls in der Dogi&Hasi-Spalte ganz oben.
  Die internen Schluesselnamen (nav_dogfather...) bleiben bewusst
  unveraendert -- reine Bezeichner, ein Umbenennen waere nur Fehlerquelle.

- "die kachel soll noch spezieller und geiler aussehen": Einreich-Kachel
  auf stimmen.html deutlich aufgewertet -- wandernder Farbverlauf-Rahmen
  (Zwei-Ebenen-/background-position-Technik, KEIN rotierender Ring: siehe
  dokumentierte Projekt-Lektion zu Verzerrungen auf eckigen Flaechen),
  schwebende Herz-Partikel, Medaillon statt nacktem Emoji,
  Farbverlauf-Ueberschrift, aufleuchtende Eingabefelder.
- "und auch die die danach kommen ... sollen viel geiler sein": freigegebene
  Stimmen sind jetzt echte Praesentationskarten -- farbige Akzentlinie oben,
  grosses Anfuehrungszeichen als Wasserzeichen, Avatar-Medaillon mit
  Anfangsbuchstabe, TikTok-Handle als eigener Chip, sanftes Anheben beim
  Ueberfahren.

Alles augenschonend gehalten (gedeckte Toene, langsame Bewegungen,
vollstaendiger Stillstand bei prefers-reduced-motion) und per Playwright
visuell geprueft, keine JS-Fehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-21 01:02:36 +02:00
DogFatherGitandClaude Opus 5 5146cef164 TikTok-Knoepfe fuer HasiDog + Modi-Gang, Zeitreise-Reihenfolge, Bildgroesse
Nutzer-Wuensche 20.08.2026:
- hasidog.html: TikTok-Knopf direkt unter der grossen Ueberschrift,
  Account @hasidog0804 (alle 5 Sprachen uebersetzt).
- streamer.html: TikTok-Knopf unter "Casper - mein treuer Begleiter",
  Account @dogis.modi.gang (alle 5 Sprachen uebersetzt).
- zeitreise.html: die beiden letzten Karten getauscht -- der Website-Start
  (21.08.2026, konkretes Datum) steht jetzt VOR der Teddy-Kooperation
  ("Datum folgt"), damit die Zeitleiste durchgehend chronologisch bleibt.
- streamer.html Bildgroesse gefixt ("der soll bissl kleiner sein und nicht
  so riesig"): streamer-mascot.jpg ist 900x2812 px und lief mit
  aspect-ratio:auto + height:auto in voller natuerlicher Hoehe -- rund
  dreimal so hoch wie die Textspalte daneben. Neue Klasse
  .collage-photo-kompakt deckelt die Hoehe auf 460px und laesst den Rahmen
  eng am Bild sitzen (width:fit-content), die ganze Figur bleibt sichtbar.
  Regel steht bewusst NACH ".collage-photo img" -- gleiche Spezifitaet,
  die spaetere gewinnt, sonst haette width:100%/height:100% sie aufgehoben.

Alles per Playwright verifiziert (beide Knoepfe mit korrekten Links,
Bildhoehe 460px statt ~1780px, neue Zeitreise-Reihenfolge), keine JS-Fehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 23:46:04 +02:00
DogFatherGitandClaude Opus 5 399260b805 "Stimmen" verwandelt: Fans reichen selbst Testimonials ein
Nutzer-Wunsch 20.08.2026: "eine richtig geile kachel ... wo die leute mit
namen und tiktok namen mir eine nachricht schreiben können die ich in die
verwaltung kriege, und dann kann ich die ausgewählten über die
verwaltungsseite auf die website hinzufügen ... das wird mega persoenlich
zu den fans, bitte wirklich krass geil speziell."

- Neue, augenschonend gestaltete Einreich-Kachel auf stimmen.html: Name,
  TikTok-Name (optional), Nachricht -- landet NIE automatisch oeffentlich,
  sondern immer erst als Entwurf in der Verwaltung.
  Warmer Babyblau/Rosé-Farbverlauf, wandernder Lichtschein, pulsierendes
  Herz -- ersetzt die 3 ewigen "Hier steht bald..."-Platzhalterkarten.
- Neue Verwaltungs-Sektion "💬 Stimmen verwalten": Warteschlange (offene
  zuerst), Freigeben/Ablehnen/Löschen pro Eintrag.
- Freigegebene Stimmen erscheinen automatisch in einer neuen, spezielleren
  Zitat-Kartenoptik (großes Anführungszeichen, Name + TikTok-Chip) --
  Abschnitt bleibt komplett unsichtbar, solange keine einzige freigegeben
  wurde.
- Backend: neue Tabelle `testimonials` (Migration 0006), routes/
  testimonials.js (oeffentliches Einreichen + Lesen freigegebener,
  admin-Warteschlange + Status/Loeschen mit neuer TESTIMONIALS_MANAGE-
  Berechtigung). 18 automatisierte Tests gegen eine Fake-DB bestanden.
- Alles per Playwright visuell durchgespielt: leerer Zustand, Einreichen
  -> Erfolgsmeldung, freigegebene Stimmen-Anzeige, Verwaltungs-Warteschlange
  mit allen Aktionen.

Backend-Teil (server-internal/, cloudflare-worker/migrations/) noch ohne
Deploy-Zugriff -- Dogi muss ihn manuell auf dogiintern ausrollen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 23:37:31 +02:00
DogFatherGitandClaude Opus 5 519e209bce Zweite Live-Uhr fuer "spezielles Live-Event" + mehr Farbe im Radar
Nutzer-Wunsch 20.08.2026: "wuensch mir nur noch bissl farbe und sehr
wichtig ist dass ich auch spezielle live events da auch eintragen kann
in der verwaltungsseite so dass da eine zweite uhr erscheint wo die zeit
dann fuer dieses spezielle event laeuft."

- Neue Verwaltungs-Sektion "Spezielles Live-Event": Titel (Deutsch, wird
  automatisch uebersetzt), echtes Datum+Uhrzeit, optionaler Link, Aktiv-
  Schalter -- nur mit Titel + Zukunftsdatum aktivierbar.
- Zweite Uhr auf streamplan.html (Magenta/Violett statt Cyan/Gold, direkt
  neben der bestehenden), nur sichtbar wenn ein Event aktiviert ist und
  das Zieldatum noch nicht vorbei ist. Echter Countdown inkl. Tage (z.B.
  "2T 03:14:59"), "Jetzt"-Zeiger zeigt wie bei Uhr 1 die tatsaechliche
  Uhrzeit, der magentafarbene Fixpunkt markiert die Tageszeit des Events.
  Zieldatum wird als UTC gespeichert -- jede besuchende Person sieht den
  exakt richtigen Countdown in ihrer eigenen Zeitzone.
- "Bissl Farbe": bunter Farbverlauf (Cyan/Violett/Gold) auf dem aeusseren
  Ring statt reinem Grauton, kraeftigerer Sweep-Farbschein.
- Backend: neue routes/special-event.js (getSpecialEventPublic nur bei
  aktiv+zukuenftig, getSpecialEventAdmin fuer Entwuerfe, saveSpecialEvent
  mit Validierung), 13 automatisierte Tests gegen eine Fake-DB bestanden.
- Bugfix unterwegs gefunden (Playwright-Screenshot, wiederholtes Muster
  auf dieser Seite): .stream-radar-wrap blieb trotz [hidden]-Attribut
  sichtbar (display:block ueberschreibt die eingebaute [hidden]-Regel bei
  gleicher Spezifitaet) -- explizite Regel ergaenzt, per Playwright erneut
  verifiziert (display:none bestaetigt).

Backend-Teil (server-internal/) noch ohne Deploy-Zugriff -- Dogi muss ihn
manuell auf dogiintern ausrollen, sonst bleibt die zweite Uhr unsichtbar.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 23:24:17 +02:00
DogFatherGitandClaude Opus 5 7fdceadb9e Fix: Sprachschalter-Menue nicht mittig wie die Kategorie-Menues
Nutzer-Report per Screenshot-Vergleich: "Medien"-Kategorie ist mittig
unter dem Knopf, der Sprachschalter (DE/CH/EN/FR/PT) aber rechtsbuendig --
war bewusst so gebaut, als der Schalter noch ganz am rechten Rand sass.
Seit DogiCrew-/Bewerben-Knopf daneben stehen, ist genug Platz fuer
dieselbe zentrierte Ausrichtung wie ueberall sonst -- macht die Optik
konsistent und behebt nebenbei einen Detail-Fehler: der Verbindungspfeil
zeigte schon immer mittig, waehrend die Box selbst rechts daneben sass.
Per Playwright verifiziert: Dropdown jetzt exakt unter dem Knopf zentriert,
laeuft bei keiner getesteten Breite ueber den Rand.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 23:10:27 +02:00
DogFatherGitandClaude Opus 5 7dadd84cbb Neue Hintergrundbilder: Startseite (Trio) + Streamplan
Nutzer-Wunsch 20.08.2026: Startseiten-Hero (universe-trio.jpg) und
Streamplan-Hero (bg-streamplan.jpg/-mobile.jpg) durch neu bereitgestellte
Bilder ersetzt. Blur-Variante fuer die Startseite neu aus dem frischen
Bild erzeugt (Gaussian Blur, passend zur bestehenden Cover-Verlauf-Technik).
Mobile-Ausschnitt fuer Streamplan mehrfach nachjustiert, damit der
"STREAMPLAN"-Schriftzug im schmalen Hochformat-Crop komplett sichtbar
bleibt. Alte Bilder als .bak-20-08-2026 gesichert (nicht eingecheckt).
Referenzen mit Versions-Stempel (?v=) versehen, damit der neue Stand
sofort sichtbar ist statt bis zu 4 Std im Cache zu haengen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 23:07:33 +02:00
DogFatherGitandClaude Opus 5 19636a25ec Fix: per Klick geoeffnetes Nav-Kategorie-Menue schloss sich nicht von selbst
Nutzer-Report per Screenshot: Klick auf eine Kategorie (z.B. "DogFather")
liess das Untermenue offen stehen, bis irgendwo anders hingeklickt wurde --
verliess man es einfach mit der Maus, blieb es haengen. Jetzt schliesst
sich ein per Klick geoeffnetes Menue automatisch, sobald die Maus die
Kategorie (Knopf + Untermenue) verlaesst -- nur auf echten Maus-Geraeten
(hover:hover + pointer:fine), auf Touch/Tablet aendert sich nichts, dort
gibt es kein "mit der Maus verlassen". Per Playwright verifiziert: Klick
oeffnet, Mausbewegung weg schliesst.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 22:56:49 +02:00
DogFatherGitandClaude Opus 5 a5615331aa Streamplan-Seite: interaktives Live-Radar statt schlichter Textkarten
Nutzer-Wunsch 20.08.2026: "diese seite vom streamplan soll viel geiler
und spezieller sein, ueberrasch mich."

Neues Herzstueck: ein 24-Stunden-Ziffernblatt (reines SVG, keine
Bild-Assets), das den festen 20-Uhr-Termin als dauerhaft leuchtenden
Fixpunkt zeigt (goldener Puls) und einen "Jetzt"-Zeiger in Echtzeit mit
der tatsaechlichen Uhrzeit der besuchenden Person mitbewegt (Babyblau,
DogFather-Markenfarbe). Nutzt denselben /live-status-Endpunkt wie die
Startseite:
- Offline: echter Sekunden-Countdown zum naechsten lokalen 20-Uhr-Termin.
- Live: komplettes Radar schaltet auf Rot/Puls um, zeigt den Stream-Titel
  und einen "Jetzt anschauen"-Knopf direkt zu TikTok.
Sanft rotierender Lichtschein fuer Atmosphaere, komplett augenschonend
(gedeckte Farben, respektiert prefers-reduced-motion vollstaendig).

Bugfix unterwegs gefunden (Playwright-Screenshot, dritte Wiederholung
desselben Musters heute): der "Jetzt anschauen"-Knopf blieb trotz
[hidden]-Attribut sichtbar, weil .btn selbst "display" setzt und damit
gleiche Spezifitaet wie die eingebaute [hidden]-Regel hat -- explizite
Regel ergaenzt.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 22:50:49 +02:00
DogFatherGitandClaude Opus 5 b3e191d3ca "Event des Jahres": Aktivieren-Schalter, leer bis befuellt, Detail-Fenster
Nutzer-Wunsch 20.08.2026: "diese zwei kisten sollen leer sein solang wie
ich nichts rein setze, am besten sollen die sogar immer nur erscheinen
wenn ich was rein setze und sie aktiviere ... wen ich drauf druecke dass
dann ein kleines fenster aufgeht wo das bild bissl groesser ist mit einem
groesseren text und beschreibung, und mit einem link ... die kacheln
sollen auch bissl spezieller sein."

- Kein hartkodierter Standardinhalt mehr auf der Startseite -- die Kachel
  UND der ganze Abschnitt bleiben komplett unsichtbar, bis mindestens ein
  Event in der Verwaltung ausgefuellt UND ueber einen neuen "Aktiv"-Schalter
  freigeschaltet ist. Genau 1 aktives Event -> zentrierte Einzelkachel
  statt halbleerem Zwei-Spalten-Raster.
- Klick auf eine Kachel oeffnet jetzt ein Detail-Fenster (groesseres Bild,
  groesserer Titel/Text, optionaler direkter Link) statt sofort
  wegzunavigieren.
- Kacheln bekommen einen goldenen Trophaeen-Akzent + dezenten wandernden
  Lichtschimmer statt der neutralen Standardkarten-Optik.
- Bugfix unterwegs gefunden (Playwright-Screenshot): .jahres-event-card
  blieb trotz [hidden]-Attribut sichtbar (dieselbe Ursache wie der
  frühere .jahres-event-bild-Bug: display:block ueberschreibt die
  eingebaute [hidden]-Regel bei gleicher Spezifitaet) -- explizite
  [hidden]-Regel ergaenzt.
- Backend: server-internal/routes/events.js liefert oeffentlich NUR noch
  aktivierte Slots aus (getEventsPublic), neuer authentifizierter Endpunkt
  getEventsAdmin liefert der Verwaltung auch Entwuerfe zum Vorausfuellen.
  10 automatisierte Tests gegen eine Fake-DB bestanden.

Backend-Teil (server-internal/) noch ohne Deploy-Zugriff -- Dogi muss ihn
manuell auf dogiintern ausrollen, sonst bleibt die Startseite beim alten
Verhalten (immer beide Slots zeigen, kein Aktiv-Schalter in der Verwaltung).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 22:43:28 +02:00
DogFatherGitandClaude Opus 5 208b5ba4ed Fix: gemeinsame Navigationsleiste war auf der Verwaltungsseite ~16% groesser
Endlich gefunden (Screenshot-Vergleich Haupt-/Verwaltungsseite Seite an
Seite, exakt gleiche Fensterbreite): verwaltung.html setzt bewusst
`html { font-size: 18.5px }` fuer ihr eigenes "Jewelen-Tresor"-Design
(statt der normalen 16px) -- das ist eine GLOBALE rem-Basis und vergroesserte
dadurch ungewollt auch die gemeinsame, aus main.css/main.js kommende
Navigationsleiste um denselben Faktor (~16%). Dieselbe Leiste brauchte
dadurch spuerbar mehr Breite als auf jeder anderen Seite und lief bei
Fensterbreiten ueber, bei denen sie ueberall sonst laengst gut passte --
kein Cache-Problem, ein echter CSS-Bug, der die ganze vorherige
Fehlersuche erklaert.

Fix: die Navigationsleiste bekommt in verwaltung.html ihre Masse fest in
Pixel zurueck (exakt die Werte, die bei 16px-Basis herauskaemen) statt in
rem -- dadurch bleibt sie unabhaengig von der Basis-Schriftgroesse dieser
Seite exakt so gross wie ueberall sonst. Lokal verifiziert: beide Seiten
liefern jetzt bei identischer Fensterbreite exakt dieselbe Leisten-Breite
(1560px).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 22:22:45 +02:00
DogFatherGitandClaude Opus 5 fece1dad56 Revert: Hamburger-Schwelle zurueck auf 1650px
Die Erhoehung auf 1900px eben war ein Fehlgriff -- der Nutzer sah dadurch
bei seiner eigentlichen Fensterbreite (die volle Desktop-Nav laengst
gepasst haette, zweifach live bestaetigt) nur noch die schmale Menue-
Ansicht statt der gewohnten vollen Leiste. 1650px war die korrekte,
bereits bestaetigte Schwelle -- das eigentliche Problem war durchgehend
Browser-/CDN-Caching, nicht die Schwelle selbst.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 20:06:18 +02:00
DogFatherGitandClaude Opus 5 0b0e7a2eb9 Nav-Sicherheitsabstand nochmal deutlich vergroessert (Hamburger bis 1900px)
Nutzer meldet weiterhin Ueberlauf trotz zweifach bestaetigtem Live-Test
(exakt seine Fensterbreite 1993x931 gegen den echten Server, 0 Ueberlauf,
mehrfach reproduziert) -- Ursache vermutlich hartnaeckiger lokaler Cache
im jeweiligen Browserprofil, nicht mehr abschliessend ferndiagnostizierbar.
Statt weiter zu diskutieren: Sicherheitsabstand brachial vergroessert,
unabhaengig von der genauen Ursache. Hamburger-Schwelle 1650px -> 1900px --
deckt praktisch jede Laptop-/Desktop-Fensterbreite ab, echte Desktop-Nav
zeigt sich jetzt erst ab sehr breiten Fenstern (>1900px), dort mit viel
Luft (min(1560px, 94vw)).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 20:00:07 +02:00
DogFatherGitandClaude Opus 5 7099d80491 Sichtbarer "App installieren"-Knopf statt versteckter Browser-Funktion
Nutzer-Report: "ich kann sie immer noch nicht runter laden" -- Manifest +
Service Worker reichen technisch fuer Installierbarkeit, aber ohne
sichtbaren Knopf muss man wissen, dass Chrome/Edge das Adressleisten-
Symbol/3-Punkte-Meny dafuer versteckt. Jetzt: echter Knopf im Footer
("Als App installieren", jede Seite) und in der Verwaltung-Session-Leiste
("Als eigene App installieren"), nutzt beforeinstallprompt + prompt() --
loest pro Seite automatisch mit GENAU dem Manifest aus, das diese Seite
selbst verlinkt (index.html -> manifest.json, verwaltung.html ->
manifest-verwaltung.json), kein Sonderfall-Code noetig. Bleibt unsichtbar,
wenn der Browser das nicht unterstuetzt (Safari/iOS) oder die Seite schon
als App laeuft.

Ausserdem: eigener apple-mobile-web-app-title fuer verwaltung.html
("DogiCrew-Verwaltung" statt generisch "DogFather" beim iOS-Home-Bildschirm).

Cache-Busting-Version (?v=) erneut hochgezaehlt (20260820 -> 20260820b),
da main.js sich durch diese Aenderung erneut geaendert hat.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 19:30:42 +02:00
DogFatherGitandClaude Opus 5 4173e43c78 Cache-Busting fuer CSS/JS auf allen Seiten (?v=20260820)
Zusammen mit dem no-cache-Header-Fix in server/index.js: Cloudflare (die
Seite laeuft hinter einem orange-cloud-Proxy) liefert fuer .css/.js einen
eigenen festen Standard-Cache (Browser Cache TTL 4 Std) aus, UNABHAENGIG
vom Origin-Cache-Control -- der no-cache-Header allein reichte deshalb
nicht (per curl bestaetigt: main.css zeigte weiterhin max-age=14400 direkt
nach dem Deploy). Robuste, von Cloudflare-Zoneneinstellungen unabhaengige
Loesung: jede lokale CSS/JS-Referenz auf allen 34 Seiten bekommt einen
Versions-Query-String (?v=20260820) -- fuer Browser/CDN ist das eine neue
URL, alte gecachte Kopien werden dadurch nie mehr faelschlich weiterverwendet.

WICHTIG fuer kuenftige Aenderungen an main.css/main.js: das Datum in ?v=
muss bei der naechsten inhaltlichen Aenderung an einer dieser Dateien
wieder hochgezaehlt werden, sonst greift der Cache-Bust nicht erneut.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 19:26:11 +02:00
DogFatherGitandClaude Opus 5 6dad6a5af5 Fix: Deploys blieben bei Besuchern bis zu 4 Std im Browser-Cache haengen
Nutzer-Report: sah den Nav-Fix und die neue installierbare Verwaltungsseite
trotz erfolgreichem Deploy nicht. Ursache: Express schickt standardmaessig
KEIN Cache-Control mit -- da die Seite hinter Cloudflare (orange-cloud)
liegt, sprang Cloudflare dafuer mit seinem eigenen Standardwert ein
(Browser Cache TTL 4 Std, per curl bestaetigt: max-age=14400). Ein frischer
Deploy war dadurch bis zu 4 Std lang im eigenen Browser-Cache jedes/jeder
Besuchers unsichtbar. Cloudflare respektiert ein vom Origin gesetztes
Cache-Control -- jetzt explizit "no-cache" gesetzt (erzwingt Revalidierung
per ETag bei jedem Laden, kein Performance-Verlust durch schnelle
304-Antworten bei unveraendertem Inhalt).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 19:23:42 +02:00
DogFatherGitandClaude Opus 5 2c5e04e433 Fix: manifest-verwaltung.json wurde von der Zugangsschranke blockiert
Die site-weite Zugangsschranke (server/gate.js) laesst bislang nur den
exakten Pfad /manifest.json unauthentifiziert durch (fuer die
PWA-Installierbarkeit der Haupt-Website noetig) -- das neue
manifest-verwaltung.json fiel dadurch nicht unter die Ausnahme und wurde
zur Login-Seite umgeleitet statt als JSON ausgeliefert zu werden. Neuen
Pfad zur Ausnahmeliste hinzugefuegt.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 16:43:23 +02:00
DogFatherGitandClaude Opus 5 aecf6db509 Verwaltungsseite als eigene, separat installierbare App
Nutzer-Wunsch 20.08.2026: "ich will die Verwaltungsseite auch herunter
laden koennen, so dass ich die originale website und die verwaltungsseite
2 mal getrennt installieren kann, auf dem pc und handy."

- Eigenes manifest-verwaltung.json (eigene "id"/"scope" nur fuer
  verwaltung.html, eigener Name "DogiCrew-Verwaltung", eigenes
  Icon-Set) statt des site-weiten manifest.json (scope "/") -- macht sie
  zu einer technisch eigenstaendigen App-Identitaet, installierbar
  parallel zur Haupt-Website, auf Desktop und Handy.
- Neue Icons: bestehendes Husky-Logo mit Violett/Gold-Verlauf statt
  Babyblau (passend zum "Jewelen-Tresor"-Look der Verwaltungsseite),
  damit beide installierten Apps auch optisch klar unterscheidbar sind.
- Kein zweiter Service Worker noetig -- /sw.js laeuft bereits site-weit
  auf Scope "/" und deckt verwaltung.html automatisch mit ab.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 16:42:20 +02:00
DogFatherGitandClaude Opus 5 94182cc6ab Fix: Desktop-Nav lief ab ~1500px Fensterbreite ueber den Rand hinaus
Nutzer-Report per Screenshot: "DogiCrew"-Knopf/"BALD"-Badge rechts
abgeschnitten. Ursache: die Hamburger-Schwelle (1480px) und der
Nav-Bar-Breiten-Deckel (1440px) wurden schon zweimal knapp nachgezogen,
reichten aber nicht mehr, seit die Nav um den "DogiCrew"-Knopf + "BALD"-
Badge gewachsen ist (braucht jetzt real ~1491px). Ergebnis: bei JEDER
Fensterbreite ab ca. 1500px (nicht nur in einer schmalen Uebergangszone)
lief die Nav dauerhaft ~50px ueber den Rand.

Diesmal mit echtem Sicherheitsabstand statt wieder nur knapp behoben:
Schwelle 1480px -> 1650px, Deckel 1440px -> 1560px. Per Playwright ueber
den kompletten Bereich 1024-2560px nachgeprueft, keine Ueberlaeufe mehr.
Header ist site-weit eine gemeinsame Vorlage (main.js/main.css), Fix gilt
damit automatisch fuer alle Seiten.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 16:38:46 +02:00
DogFatherGitandClaude Opus 5 bda3198c76 Neue Funktion: Supporter-Abstimmungen (Verwaltung + DogiCrew-Bereich)
Naechste "Ausbaustufe" aus Dogfather_VanVan_Supporter_Abo.odt Abschnitt 20
(siehe Supporter-Abo-System.md), auf Nutzerwunsch "perfektioniere meine
Verwaltungsseite": Dogi/VanVan koennen in verwaltung.html eine Frage mit
2-6 Antwortoptionen auf Deutsch erstellen, automatische Uebersetzung beim
Speichern (gleiches Muster wie "Event des Jahres"). Jede aktive DogiCrew-
Person sieht die Abstimmung in ihrem Supporter-Bereich, stimmt genau einmal
ab (UNIQUE-Constraint in der DB, nicht nur Anwendungslogik), sieht danach
die Live-Ergebnisse. Admin-Seite zeigt Ergebnisbalken live, kann schliessen/
wiedereroeffnen/loeschen.

- Neue Migration 0005_supporter_polls.sql (supporter_polls,
  supporter_poll_votes), neue Berechtigung POLLS_MANAGE.
- server-internal/routes/polls.js: 30 End-to-End-Tests gegen eine
  Fake-DB bestanden (better-sqlite3 laesst sich lokal nicht kompilieren).
- verwaltung.html: neue "Abstimmungen"-Kiste im bestehenden
  vw-overview-box-Stil (violett/pink Ergebnisbalken).
- supporter.html: neue "Aktuelle Abstimmung"-Karte im bestehenden
  Gold-Look, Optionen -> Stimme -> Ergebnisbalken, alle 5 Sprachen.

Backend-Teil (server-internal/, cloudflare-worker/migrations/) noch ohne
Deploy-Zugriff -- Dogi muss ihn manuell auf dogiintern ausrollen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 16:07:51 +02:00
DogFatherGitandClaude Opus 5 189b72ec73 Vorschau-Umschalter (Theatermasken-Symbol) vor dem oeffentlichen Start deaktiviert
Nutzer-Wunsch 20.08.2026, vor morgigem oeffentlichem Start: "die leute
sollen das ja nicht selbst auswaehlen koennen". Der Schalter erkannte
Dogi/VanVan bisher nur ueber das rein client-seitig lesbare dogi_role-
Cookie (bewusst NICHT das echte HttpOnly-Sicherheits-Cookie) -- technisch
liesse sich dieses Cookie per Browser-Konsole selbst setzen. Echte Daten
waeren dadurch nie einsehbar (nur eigene Beispieldaten-Vorschau), aber
"soll weg fuer die Oeffentlichkeit" heisst hier bewusst ganz weg, nicht
nur sicherer.

Nur der Aufruf beim Seitenaufbau ist auskommentiert, renderVorschauSchalter()
selbst bleibt unangetastet fuer ein spaeteres internes Testen.

Per Playwright verifiziert: Widget erscheint weder bei normalem Besuch
noch mit manuell gesetztem dogi_role-Cookie.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 15:16:01 +02:00
DogFatherGitandClaude Opus 5 f86b96a22f "Event des Jahres" jetzt in der Verwaltung pflegbar (Bild, Text, Link)
Nutzer-Wunsch 20.08.2026: "will ich in der admin seite das selbst
gestalten können, mit bild und text und am besten auch einen link." Auf
Rueckfrage entschieden: Titel/Text nur auf Deutsch eingeben, die anderen
4 Sprachen werden beim Speichern automatisch uebersetzt (MyMemory, 0€,
kein API-Key) -- bewusste Ausnahme von der sonst geltenden "immer echte
Uebersetzung"-Regel, klar dokumentiert und im Admin-UI selbst als Hinweis
sichtbar. de-CH bekommt denselben deutschen Text (Dialekt ist keine von
Uebersetzungs-APIs unterstuetzte Zielsprache).

Backend (server-internal):
- routes/events.js: getEventsPublic (oeffentlich, keine Session),
  saveEvent + uploadEventImage (beide hinter neuer EVENTS_MANAGE-
  Berechtigung, Owner immer erlaubt). Speichert in der bereits
  bestehenden app_settings-Tabelle (2 feste Slots) statt einer neuen
  Tabelle -- es gibt nie mehr als genau 2 Events.
- lib/translate.js: MyMemory-Anbindung mit "fail closed auf Deutsch"
  pro Sprache, falls der Dienst mal nicht antwortet.
- Bild-Upload per multer, zufaelliger Dateiname (crypto.randomUUID,
  verhindert Path-Traversal ueber den Originalnamen komplett), 5 MB
  Limit, nur jpeg/png/webp, Ablage unter /var/lib/dogfather-internal/
  uploads/events (NICHT im Git-Ordner -- uebersteht Deploys), oeffentlich
  ausgeliefert unter /uploads.
- Neue Berechtigung EVENTS_MANAGE im Katalog (Gruppe "Startseite").

Frontend:
- index.html: laedt /events-of-year beim Aufruf, ueberschreibt pro Slot
  Datum/Titel/Text/Bild/Link NUR wenn dort tatsaechlich etwas gespeichert
  ist -- bleibt der Abruf aus oder ist ein Slot leer, bleibt der bisherige
  fest eingebaute Standardinhalt (dieselben zwei echten Events) stehen.
  Kein Blocker, kein sichtbarer Fehler bei Ausfall.
- verwaltung.html: neue Sektion "🏆 Event des Jahres" (nur mit
  EVENTS_MANAGE sichtbar), 2 Karten mit Bild-Upload+Vorschau, Datum,
  Titel, Text, Link, eigenem Speichern-Knopf pro Event.

Ausfuehrlich getestet, weil server-internal wegen fehlender Visual-
Studio-Build-Tools auf dieser Windows-Maschine nicht lokal mit echtem
better-sqlite3 laufen kann: routes/events.js komplett isoliert gegen eine
Fake-DB getestet (11 Szenarien: oeffentlicher Abruf, fehlende Session,
Session ohne Recht, Owner, Rolle MIT EVENTS_MANAGE, alle Validierungen,
Bild-Upload inkl. falscher Dateityp, Abruf des hochgeladenen Bilds).
index.html per Playwright mit echtem Netzwerk-Mocking gegen zwei
Szenarien getestet (API nicht erreichbar -> Standardinhalt bleibt; API
liefert echte Daten -> nur der befuellte Slot wird ueberschrieben, der
leere bleibt Standard). Dabei einen echten CSS-Bug gefunden und behoben
(display:block auf .jahres-event-bild überschrieb die [hidden]-Regel des
Browsers, leeres Bild waere immer sichtbar gewesen). verwaltung.html per
Playwright mit gemocktem Login+API end-to-end getestet: Formular wird
korrekt vorbefuellt, Bild-Upload + Speichern senden die richtigen Daten.

WICHTIG: server-internal laeuft unter einem eigenen Systembenutzer
(dogiintern), auf den ich (claudian) bewusst KEINEN Zugriff habe -- diese
Aenderung kann ich anders als sonst nicht selbst bis auf den Server
bringen. Dogi muss den Deploy-Schritt fuer server-internal selbst
ausfuehren (git pull + npm install + Neustart des dogiintern-Dienstes).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 15:09:54 +02:00
DogFatherGitandClaude Opus 5 3f3ba835a9 Patricks Zitat ergaenzt
Nutzer-Wunsch 20.08.2026: "Wege entstehen dadurch, dass man sie geht."
(alle 5 Sprachen). Ersetzt "Zitat folgt" auf der Scout-Karte.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 14:28:11 +02:00
DogFatherGitandClaude Opus 5 f8f3e65603 Streamer-gesucht-Seite: neues Hero-Bild (Spicy Media, rot)
Nutzer-Wunsch 20.08.2026: bg-bewerben-creator.jpg + -mobile.jpg (bisher
unscharfes Selfie mit pinker Sonnenbrille) ersetzt durch das vom Nutzer
gelieferte Motiv (Spicy-Media-Look, rot, Silhouette + Logo). Alte Version
lokal gesichert (*.jpg.bak-20-08-2026, nicht eingecheckt).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 14:22:01 +02:00
DogFatherGitandClaude Opus 5 f6cbaec132 Zeitreise: Karte "Das Manager-Team wächst" entfernt
Nutzer-Wunsch 20.08.2026. Karte (Juli 2026) samt zugehoeriger i18n-Keys
(zr_ev8_date/_h3/_p, alle 5 Sprachen) entfernt. 16 -> 15 Karten.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 13:16:32 +02:00
DogFatherGitandClaude Opus 5 5c5accd12c Neues Links-Hero-Bild + echtes Community-Foto statt Platzhalter
Nutzer-Wunsch 20.08.2026:
- links.html: neues, vom Nutzer geliefertes "aus dem Universum"-Motiv
  (Weltraum-Szene mit TikTok/Instagram/Snapchat/Discord/Merch-Icons um ein
  "LINKS"-Portal) ersetzt bg-links.jpg + -mobile.jpg. Alte Version lokal
  gesichert (*.jpg.bak-20-08-2026, nicht eingecheckt).
- community.html, Abschnitt "Mehr als Follower": echtes Team-Foto
  (Diene, Ghost, VanVan, Dogi bei einem gemeinsamen Treffen, "TEAM DOGI")
  ersetzt den "Foto folgt"-Platzhalter. Gleiches Muster wie vanvan.html
  (.collage-photo, aspect-ratio:3/4 statt der alten 4/3-Platzhalterbox,
  da das echte Foto Hochformat ist). Jetzt ungenutzte i18n-Keys
  com_bild_platzhalter/com_foto_folgt entfernt.

Per Playwright verifiziert: beide Seiten laden fehlerfrei, keine
fehlgeschlagenen Requests, keine Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 12:54:53 +02:00
DogFatherGitandClaude Opus 5 071f3d0382 Startseite: neue "Event des Jahres"-Kachel unter den 3 Welt-Kacheln
Nutzer-Wunsch 20.08.2026: volle Breite wie die 3 Welt-Kacheln zusammen,
zeigt immer genau zwei Events. Erste Belegung mit zwei bereits auf
zeitreise.html dokumentierten echten Events (28-Stunden-Stream 7. März
2026, Creator Cup 27. Juli - 2. August 2026) statt Platzhaltertext --
Texte sind gekuerzte, inhaltlich unveraenderte Fassungen der bestehenden
Zeitreise-Beschreibungen, beide Karten verlinken auf zeitreise.html. Alle
5 Sprachen gepflegt. Gleiche Karten-/Tag-Optik wie die Welt-Kacheln, damit
es wie ein natuerlicher vierter Baustein wirkt statt wie ein fremdes
Element.

Per Playwright verifiziert: exakt gleiche Breite wie .hero-grid (1180px),
2 Karten, kein horizontales Scrollen auf Mobil, keine Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 12:51:17 +02:00
DogFatherGitandClaude Opus 5 9adebd9bc4 KRITISCH: Server-Quellcode + kompletter Git-Verlauf waren oeffentlich abrufbar
Sicherheits-Audit vor dem geplanten oeffentlichen Start morgen (21.08.2026,
Nutzer-Anfrage: "duerfen die leute keinen zugriff auf veraenderungen haben").
SITE_DIR ist der GESAMTE Repo-Ordner (join(__dirname, "..")), express.static
lieferte daher nicht nur die Website aus, sondern auch:
- server/ (inkl. gate.js, das komplette Sicherheitskonzept im Klartext)
- server-internal/ (Admin-/Supporter-Backend-Quellcode)
- cloudflare-worker/ (altes Backend)
- .git/ (VOLLSTAENDIGE Commit-Historie, rekonstruierbar per Git-Dump)
- CLAUDE.md, DEPLOY.md, wrangler.toml, netlify.toml, gate-worker.js,
  gate.html.bak-07-08-2026 (Alt-Backup-Datei einer frueheren Session)

Live nachgewiesen (mit gueltigem Zugangscode -- morgen faellt die Schranke
fuer ALLE weg): /server/gate.js und /.git/config lieferten HTTP 200.
Ursache: serve-static blockt per Default nur Dateien, deren EIGENER Name
mit einem Punkt beginnt (server/.env -> zufaellig schon 404), aber NICHT
rekursiv -- .git/config wird trotzdem ausgeliefert, weil "config" selbst
nicht mit einem Punkt beginnt, nur der Ordner davor.

Fix: eigene Sperr-Middleware VOR express.static, unabhaengig von
gateMiddleware (bleibt also auch nach dem Entfernen der Zugangsschranke
wirksam). Blockt ganze Ordner (server/, server-internal/,
cloudflare-worker/) + versteckte Ordner/Dateien rekursiv (jedes
Pfadsegment, das mit "." beginnt, ausser .well-known) + eine feste Liste
an Alt-Dateien + jedes *.bak-Muster, damit auch kuenftige Backup-Reste
automatisch mitgeschuetzt sind.

Lokal mit echtem Express-Server verifiziert (gateMiddleware absichtlich
deaktiviert, um exakt den morgigen "oeffentlich"-Zustand zu simulieren):
alle vorher gefundenen Luecken jetzt 404, alle echten Seiten/Assets
(index.html, main.css, main.js, manifest.json, robots.txt, favicon)
weiterhin 200.

Getrennt prooft: server-internal/ (eigener Dienst unter
postfach.dogfather-universe.com, Port 4200) hat sein EIGENES,
unabhaengiges Session-System -- jede /admin/*-Route ist einzeln per
requireTeamSession-Middleware abgesichert (in index.js durchgezaehlt,
keine Ausnahme gefunden), live mit einer unauthentifizierten Anfrage
gegen /admin/users/list bestaetigt (401). Dieser Dienst war nie vom
Website-Gate abhaengig und ist von diesem Fund nicht betroffen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 12:47:36 +02:00
DogFatherGitandClaude Opus 5 1d28d8db2b Kooperation-Seite: Hero-Bild nochmal ausgetauscht (verfeinerte Version)
Nutzer-Wunsch 20.08.2026 (zweite Runde): neues, verfeinertes Motiv (DogFather-
Logo oben, Filipe sitzend mit HasiDog + Husky, "INTERESSE AN EINER
KOOPERATION? JETZT KONTAKT AUFNEHMEN") ersetzt die Version vom selben Tag.
Theme bleibt dogfather/babyblau (passt weiterhin). Alte Version lokal
gesichert (*.jpg.bak-20-08-2026-v2, nicht eingecheckt).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 12:26:59 +02:00
DogFatherGitandClaude Opus 5 7a69f896bc Dienes Profiltext: Alter durch Geburtsmonat ersetzt
Nutzer-Wunsch 20.08.2026: "Ich bin 37 Jahre alt" -> "Ich bin im August 1988
geboren" (und analog in allen 5 Sprachen), Rest des Satzes (verheiratet,
Mutter von zwei Kindern) unveraendert. Betrifft nur bioHtml.de/de-CH/en/
fr/pt in data-modis.js, keine anderen Vorkommen von "37" im Text.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 12:06:48 +02:00
DogFatherGitandClaude Opus 5 bfcf35aafa Kooperation-Seite: neues Hero-Bild + Theme Rot -> Blau
Nutzer-Wunsch 20.08.2026: Hintergrundbild von bewerben-kooperation.html
(bg-bewerben.jpg + -mobile.jpg, beide bisher identisch zum alten roten
Spicy-Media-Motiv "WERDE TEIL VON SPICY MEDIA") ersetzt durch das vom
Nutzer geschickte neue Motiv (HasiDog/DogFather/Husky, "EINE KOOPERATION
WOLLEN? DANN HIER MELDEN", bereits babyblau statt rot). Passend dazu
data-theme von "spicymedia" (Rot/Orange) auf "dogfather" (Babyblau/Lila)
umgestellt -- keine hartkodierten Rot-Werte im HTML, daher genuegt die
Theme-Umstellung fuer Knopf-/Link-/Eyebrow-Farben komplett.

Bild aus PNG-Quelle konvertiert (quality=90, optimize+progressive), Seiten-
verhaeltnis/Aufloesung unveraendert uebernommen (kein Hochskalieren). Alte
Bilder lokal gesichert (*.jpg.bak-20-08-2026, nicht eingecheckt).

Per Playwright verifiziert: data-theme=dogfather, --accent=#8fd9ea, keine
fehlgeschlagenen Requests, keine Konsolenfehler, Formular/Footer sehen im
echten (nicht-fullPage-)Screenshot nach dem Scrollen korrekt dunkel aus --
ein zunaechst gefundener "weisser Kasten" war ein reines Playwright-
fullPage-Screenshot-Artefakt (position:fixed-Hintergrund kachelt beim
kuenstlich verlaengerten Full-Page-Screenshot nicht mit), kein echter Bug,
per echtem gescrolltem Viewport-Screenshot gegengeprueft und ausgeschlossen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 12:04:05 +02:00
DogFatherGitandClaude Opus 5 02f9509042 Zeitreise-Finale-Punkt-Bug + Preis aus dem Abonnieren-Hero-Bild entfernt
1) Nutzer-Report 20.08.2026 ("wieso dieser Punkt da in der Mitte"):
.zeit-node (der leuchtende Zeitleisten-Punkt) ist bei normalen Karten
position:absolute; left:50%; top:50% relativ zu .zeit-ast -- korrekt, weil
die Karte dort nur die halbe Breite einnimmt. Die Finale-Karte
(.zeit-final) wird aber auf fast volle Breite gestreckt und zentriert,
der Knoten landete dadurch mitten im Zitat-Text. Die Verbindungslinie
(::before) wurde dafuer schon frueher ausgeblendet, der Knoten selbst
wurde dabei uebersehen -- jetzt nachgezogen (display:none fuer
.zeit-final .zeit-node).

2) Nutzer-Wunsch 20.08.2026: der Preis "FÜR 4,99 € MONATLICH" stand fest
ins Hero-Bild von abonnieren.html eingebrannt (bg-abonnieren.jpg +
-mobile.jpg, beide Dateien waren identisch) -- per CSS/Text nicht
erreichbar, siehe bereits dokumentierter Fund vom selben Tag. Per Pillow
sauber herausretuschiert (Clone-Stamp aus einem textfreien Bereich
derselben Schaltflaeche, exakt auf die Zeilenhoehe inkl. Ü-Umlautpunkte
skaliert, Nahtstellen weich gezeichnet, mit numpy-Helligkeitsanalyse
zeilenweise gegengeprueft bis keine Text-Reste mehr uebrig waren) --
"ABONNIEREN" bleibt als eigenstaendiger Button stehen, keine sichtbare
Lücke/Leerstelle. Qualitaet/Dateigroesse an das Original angepasst
(quality=90, optimize+progressive, exakt vergleichbare Groesse). Original
lokal gesichert (bg-abonnieren.jpg.bak-20-08-2026, nicht eingecheckt).

Beide Fixes per Playwright verifiziert: Knoten-Punkt display:none bei der
Finale-Karte, Hero-Bild laedt fehlerfrei ohne fehlgeschlagene Requests,
keine Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 11:49:26 +02:00
DogFatherGitandClaude Opus 5 7e369c72ae Zeitreise: echte Daten fuer "Team Dogi entsteht" + "geht online"
Nutzer-Wunsch 20.08.2026:
- "Team Dogi entsteht": "Datum folgt" -> "Anfang 2025" (neuer Key
  zr_ev2_date, alle 5 Sprachen). Bleibt an ihrer Stelle -- liegt weiterhin
  chronologisch vor "Ostern 2025".
- "DOGFATHER UNIVERSE geht online": "2026 - Datum folgt" -> "21. August
  2026, 21 Uhr" (morgen, live). Bleibt ebenfalls an ihrer Stelle.

Per Playwright komplette Reihenfolge erneut gegengeprueft: weiterhin
chronologisch korrekt, keine Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 11:36:47 +02:00
DogFatherGitandClaude Opus 5 895f04c524 Zeitreise: echtes Datum fuer "Start als Content Creator" (24.10.2023)
Nutzer-Wunsch 20.08.2026. Neuer i18n-Key zr_ev1_date (alle 5 Sprachen),
Karte bleibt weiterhin an erster Stelle, da 2023 das fruehste Datum der
gesamten Zeitleiste ist. Per Playwright die komplette Reihenfolge erneut
durchgezaehlt: weiterhin korrekt, keine Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 11:33:55 +02:00
DogFatherGitandClaude Opus 5 f750fe9dfd Zeitreise: echte Daten fuer Buch + Spotify, chronologisch einsortiert
Nutzer-Wunsch 20.08.2026: "Das Buch HasiDog & Casper" (November 2025) und
"HasiDog startet auf Spotify" (1. Januar 2026) hatten bisher "Datum folgt"
und lagen dadurch weit hinten in der Zeitleiste, obwohl beide zeitlich vor
dem 28-Stunden-Stream (7. März 2026) liegen. Neue i18n-Keys zr_ev13_date/
zr_ev14_date (alle 5 Sprachen) ergaenzt, beide Karten an die chronologisch
richtige Stelle verschoben: Ostern 2025 -> November 2025 (Buch) ->
1. Januar 2026 (Spotify) -> 7. März 2026 (28-Stunden-Stream) -> ...

"Kooperation mit VanVan Teddys" bleibt bewusst unveraendert bei "Datum
folgt", da dafuer noch kein Datum genannt wurde.

Per Playwright die komplette Reihenfolge alle 16 Karten durchgezaehlt und
gegengeprueft: chronologisch korrekt, keine Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 11:26:19 +02:00
DogFatherGitandClaude Opus 5 9cb4e177ee Zeitreise: 4 Ereignisse entfernt + einladender "wächst noch"-Hinweis
Nutzer-Wunsch 20.08.2026: vier Zeitleisten-Karten sollten komplett weg
("Ein neues Kapitel beginnt" / Mai 2026, "Die ersten betreuten Creator",
"Weitere Scouts und Manager kommen hinzu", "Das erste Community-Treffen") --
entfernt aus zeitreise.html samt der zugehoerigen, jetzt ungenutzten
i18n-Keys (zr_ev5_*, zr_ev16_*, zr_ev17_*, zr_ev18_*). 20 -> 16 Karten.

Zusaetzlich: gleich am Seitenanfang soll auffallen, dass diese Zeitreise
noch nicht fertig ist. Bewusst NICHT die alte .todo-note-Optik reaktiviert
(die war fuer Entwickler gedacht und wurde am 31.07.2026 sitewide bewusst
unsichtbar gemacht) -- stattdessen ein neuer, fuer Besucher gestalteter
Hinweis (.zeit-baustelle), der zum Wachstums-Baum-Thema der Seite passt
(🌱 "Der Anfang" oben, 🌳 "...wächst weiter" unten): "Diese Zeitreise
wächst noch", babyblauer Glow-Rahmen, leicht wiegendes Setzlings-Icon.
Alle 5 Sprachen gepflegt.

Per Playwright verifiziert: alle 4 Karten wirklich weg (Text-Suche),
16 statt 20 .zeit-ast-Elemente, Hinweisbox sichtbar mit korrektem Text,
keine Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 11:23:38 +02:00
DogFatherGitandClaude Opus 5 c4fa132244 DogiCrew-Sperre: auch die Registrierungsfelder selbst nicht mehr eintippbar
Nutzer-Feedback 20.08.2026 (direkte Nachpruefung): "ich kann immer nur was
rein tippen, soll garnicht möglich sein bitte" -- der Registrieren-Knopf
war gesperrt, aber Name/TikTok/E-Mail-Feld ließen sich weiterhin normal
beschreiben. Jetzt disabled, exakt wie das E-Mail-Feld im Login-Panel
(voriger Commit). Per Playwright verifiziert: echter Tippversuch in allen
drei Feldern hinterlässt keinen Wert mehr.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 11:10:26 +02:00
DogFatherGitandClaude Opus 5 d98133faba DogiCrew-Sperre vervollstaendigt: Login-Einstieg, E-Mail-Feld, Preis
Nutzer-Feedback 20.08.2026 (Nachpruefung des vorherigen Commits): der
Registrieren-Knopf war gesperrt, aber drei Luecken blieben offen --

1. "Schon Supporter? Hier einloggen" fuehrte zu einem VOLL FUNKTIONSFAEHIGEN
   Login-Code-Anfordern-Knopf. E-Mail-Feld jetzt disabled (nicht eintippbar,
   wie gewuenscht) und btn-login-request auf denselben .btn-gold-locked-Stil
   wie der Registrieren-Knopf umgestellt (natives disabled-Attribut).
2. Dritte, bislang uebersehene Luecke: "Mit Google anmelden" waere ein
   weiterer Weg gewesen, trotz gesperrter Knoepfe ein echtes Konto anzulegen,
   falls GOOGLE_CLIENT_ID serverseitig konfiguriert ist. initGoogleConsent()
   wird jetzt nicht mehr aufgerufen, solange die Registrierung gesperrt ist --
   ein Kommentar markiert genau die Stelle zum spaeteren Reaktivieren.
3. Der Preis (4,99 €) durfte laut Nutzer noch nicht sichtbar sein -- war aber
   an zwei Stellen zu sehen: der grossen Preis-Zahl in der Box (jetzt
   "Coming soon…" im selben Gold-Schimmer-Stil wie der gesperrte Knopf) und
   im Fliesstext (ab_text2, alle 5 Sprachen: "Für 4,99 € im Monat" ->
   "Mit deinem/dim/your/ta/sua monatlichen Beitrag", nur die erste Teilphrase
   geaendert, Rest jeder Uebersetzung unangetastet).

Betrifft ausschliesslich den oeffentlichen Anmelde-Einstieg -- bereits
eingeloggte Supporter (Dogi/VanVan als Test-Accounts) sind ueber ihren
gespeicherten Token/supporter.html unveraendert erreichbar.

Bekannter, NICHT in diesem Commit geloester Rest: der grosse Hero-Banner
oben auf der Seite (assets/img/bg-abonnieren(-mobile).jpg) zeigt den Preis
ebenfalls fest ins Bild eingebrannt ("ABONNIEREN FÜR 4,99 € MONATLICH") --
das ist Bildmaterial, keine Text-/CSS-Aenderung, braucht Ruecksprache mit
Dogi bevor daran gearbeitet wird.

Vor dem Commit per Playwright verifiziert: Preis-Box zeigt nur noch
"Coming soon…", Login-E-Mail-Feld nimmt keine Eingabe an, Klick auf den
gesperrten Login-Knopf loest keinen /supporter/login-request-Aufruf aus,
Google-Bereich bleibt hidden, keine Konsolenfehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 11:07:24 +02:00
DogFatherGitandClaude Opus 5 5cd473e71a DogiCrew-Registrierung: "Coming Soon"-Sperre + Bald-Badge im Nav-Knopf
Nutzer-Wunsch 20.08.2026: DogiCrew ist "das geilste Bonus" -- der Registrieren-
Knopf soll noch nicht bedienbar sein (Dogis PayPal-Business-Zugangsdaten fehlen
noch, siehe DogFather Website - Offene Punkte.md), aber Besucher sollen ueber
den Nav-Knopf trotzdem ganz normal auf die Seite kommen und die Vorschau sehen
duerfen -- "soll alles bleiben" ausser dem einen Knopf.

- Nav-Knopf (DogiCrew/Abo aktivieren, main.js renderAboButton): neues kleines
  Schimmer-Badge "Bald" direkt in der Pille (main.css .nav-cta-soon, reine
  Wiederverwendung von .btn-silver-text/-spark in kleinerem Massstab). Nur auf
  den drei Zielen, die zur (noch gesperrten) Registrierung fuehren -- NICHT auf
  supporter.html, das ist der echte, bereits funktionierende Bereich fuer
  Dogi/VanVan als Test-Supporter.
- abonnieren.html: Registrieren-Knopf ersetzt durch neue .btn-gold-locked-
  Komponente -- eigener Gold/Schloss-Stil (nicht das schon anderswo auf dieser
  Seite vergebene .btn-silver), natives disabled-Attribut (kein JS noetig,
  disabled-Buttons feuern keine Click-Events -- der bestehende Listener bleibt
  unveraendert und inert). Formularfelder bleiben normal ausfuellbar (Teaser),
  nur der Absende-Knopf ist gesperrt.

Bug waehrend der Umsetzung gefunden UND behoben, nicht nur uebersehen: die
rotierende Conic-Gradient-Randmaske von .btn-silver/.btn-legendary (copy-paste
als erster Versuch) verzieht sich auf diesem sehr langgestreckten 100%-Breite-
Knopf zu einer krummen Schlaufe -- exakt die dokumentierte Lektion in
Projektregeln.md Punkt 14 (03.08.2026, TikTok-Button-Bug), die beim ersten
Entwurf übersehen wurde. Per Playwright-Screenshots über mehrere Animations-
Frames nachgewiesen (nicht nur vermutet) und auch am bereits LIVE laufenden
.btn-silver auf dieser Seite reproduziert, um auszuschliessen, dass es an der
neuen Komponente statt an der Technik selbst liegt. Fix: derselbe sichere
Zwei-Layer-Background-Trick wie bei .abo-google-frame (gleiche Datei) --
Bewegung nur ueber background-position, bleibt geometrieunabhaengig exakt an
der Kontur. Nach dem Fix erneut ueber mehrere Frames verifiziert, sauber.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 10:51:34 +02:00
DogFatherGitandClaude Opus 5 0223fcf9cd Mobil-Menü: schwebende Eck-Knöpfe blockierten Sprachschalter + Bewerben-Button
Nutzer-Report 20.08.2026: "auf dem Handy... passt nicht immer alles". Per echter
Mobil-Emulation (iPhone 15 Pro + Pixel 8, Playwright) nachgestellt statt geraten:
Die schwebenden Eck-Knöpfe (Musik-Widget unten rechts, Vorschau-Umschalter fuer
Dogi/VanVan unten links) sind absichtlich immer sichtbar (position:fixed, direkte
<body>-Kinder). Beim aufgeklappten Mobil-Hamburger-Menü lagen sie dadurch sichtbar
UEBER den untersten Menuepunkten (Sprachschalter "DE" + "Bewerben"-Knopf) und
verdeckten sie -- auf Screenshots klar zu sehen, betrifft jede der ueber 30 Seiten,
da das Menü ueberall gleich ist. Genau die Art Bug, die im Browser (Adressleiste
noch da, man kann drumrum navigieren) kaum auffaellt, aber in der installierten
Vollbild-App voll durchschlaegt.

Fix: :has()-Selektor blendet beide Widgets aus, solange .nav-links.open ist --
kein gemeinsamer Elternknoten mit dem Menue vorhanden, daher CSS statt weiterer
main.js-Logik. Nach dem Fix per Regressionstest ueber 6 Seiten x 2 Geraete erneut
verifiziert: Widgets erscheinen normal wieder, sobald das Menue schliesst, keine
neuen Konsolenfehler, kein horizontales Scrollen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-20 10:28:22 +02:00
128 changed files with 6107 additions and 659 deletions
+6 -6
View File
@@ -18,10 +18,10 @@
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex" />
<title>Seite nicht gefunden — DOGFATHER UNIVERSE</title> <title>Seite nicht gefunden — DOGFATHER UNIVERSE</title>
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body> <body>
<div id="site-header"></div> <div id="site-header"></div>
@@ -41,7 +41,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-404.js"></script> <script src="assets/js/i18n-404.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+303 -47
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Unterstütze Dogfather dauerhaft mit der DogiCrew 🩵 und sammle exklusive Treueprämien — die Teddy-Kollektion entsteht in Kooperation mit VanVan." /> <meta name="twitter:description" content="Unterstütze Dogfather dauerhaft mit der DogiCrew 🩵 und sammle exklusive Treueprämien — die Teddy-Kollektion entsteht in Kooperation mit VanVan." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
<style> <style>
/* ===================================================================== /* =====================================================================
Preis-/Anmelde-Kiste — komplett neu gestaltet (Nutzer-Feedback Preis-/Anmelde-Kiste — komplett neu gestaltet (Nutzer-Feedback
@@ -216,6 +216,104 @@
.abo-switch a { color: var(--accent); cursor: pointer; font-weight: 600; } .abo-switch a { color: var(--accent); cursor: pointer; font-weight: 600; }
.abo-switch a:hover { text-decoration: underline; } .abo-switch a:hover { text-decoration: underline; }
/* Registrieren/Einloggen-Umschalter (21.08.2026, Nutzer-Wunsch: "die
sollen dann sofort immer den anmelde knopf auswählen können") — zwei
Segmente in einer Pille, aktiver Tab in Gold hervorgehoben, damit die
Wahl sofort ins Auge fällt, noch bevor überhaupt ein Formularfeld zu
sehen ist. Augenschonend: kein grelles Weiß, ruhiger Farbwechsel. */
/* =====================================================================
Aktionsleiste (21.08.2026) — EIN Element für beide Wege, statt vorher
doppeltem "Registrieren" (Umschalter oben + Knopf unten). Links die
eigentliche Aktion in Gold (unverändert im bestehenden Marken-Look),
rechts der Wechsel in Babyblau — der zweiten Hausfarbe der Seite
(theme-dogfather.css). Beide Hälften nutzen dieselbe Technik wie die
bestehenden Gold-Knöpfe (wandernder Verlauf per background-position +
ein durchlaufender Glanzstreifen als ::after) — dadurch wirken sie wie
ein zusammengehöriges Paar statt wie zwei fremde Knöpfe.
Augenschonend (Projektregel): kein Blinken/Flackern, nur ruhig
wandernde Verläufe, und bei prefers-reduced-motion steht alles still.
===================================================================== */
.abo-aktionsleiste {
display: flex; gap: .6rem; margin-top: .4rem;
}
/* flex-basis "auto" statt "0": jede Hälfte startet bei ihrer eigenen
Textbreite und wächst dann anteilig mit. Dadurch bekommt automatisch
die Seite mit dem längeren Text mehr Platz — egal ob das gerade links
("Login-Code anfordern") oder rechts ("Schon registriert? Einloggen")
ist. Mit fester 0-Basis wären beide exakt gleich breit gewesen und der
jeweils längere Text wäre auf zwei Zeilen umgebrochen. */
.abo-aktionsleiste > button {
position: relative; isolation: isolate; overflow: hidden;
flex: 1 1 auto; min-width: 0;
display: flex; align-items: center; justify-content: center; text-align: center;
padding: 1rem .95rem; border-radius: 14px;
font-weight: 800; font-size: .92rem; line-height: 1.25;
white-space: nowrap;
cursor: pointer;
transition: transform .2s ease, box-shadow .2s ease;
}
.abo-aktionsleiste > button::after {
content: ""; position: absolute; inset: 0; z-index: 1;
background: linear-gradient(100deg, transparent 30%, rgba(255,255,255,.85) 48%, transparent 66%);
background-size: 240% 100%;
mix-blend-mode: overlay;
animation: abo-btn-glare 3.2s ease-in-out infinite;
pointer-events: none;
}
.abo-aktionsleiste > button:hover { transform: translateY(-3px) scale(1.015); }
/* Links: Gold — exakt die bestehende Knopf-Optik der Seite. */
.abo-aktion-haupt {
color: #1a1408;
border: 1.5px solid rgba(255,233,168,.65);
background: linear-gradient(120deg, #ffe9a8 0%, #d4af37 30%, #ffe9a8 55%, #fff2c9 78%, #d4af37 100%);
background-size: 280% 100%;
animation: abo-btn-shine 6s ease-in-out infinite;
box-shadow: 0 10px 26px -12px rgba(212,175,55,.55);
}
.abo-aktion-haupt:hover {
box-shadow: 0 16px 34px -10px rgba(212,175,55,.65), 0 0 26px rgba(212,175,55,.4);
}
/* Babyblau — gleiche Machart, kräftigeres Sky-Blau statt des sehr
blassen Standard-Babyblaus, damit es neben dem Gold nicht untergeht
(dieselbe Lektion wie auf der Verwaltungsseite). Dunkler Tiefsee-Text
darauf für klaren Kontrast; zusätzlich ein sanft atmender Ring, der die
Kachel leicht leuchten lässt, ohne zu blenden.
Zwei Namen, eine Optik: ".abo-aktion-neben" ist der Wechsel-Knopf neben
einer Gold-Aktion (Registrierungs-Seite), ".abo-aktion-blau" ist ein
Knopf, der selbst DIE Hauptaktion ist und trotzdem blau sein soll
(Einloggen-Seite, Nutzer-Wunsch 21.08.2026). Bewusst getrennte Namen,
damit später klar bleibt, warum ein Knopf blau ist. */
.abo-aktion-neben,
.abo-aktion-blau {
color: #05202b;
border: 1.5px solid rgba(224,247,254,.7);
background: linear-gradient(120deg, #e0f7fe 0%, #38bdf8 30%, #7dd3fc 55%, #f0fbff 78%, #38bdf8 100%);
background-size: 280% 100%;
animation: abo-btn-shine 6s ease-in-out infinite, abo-blau-atem 4.5s ease-in-out infinite;
box-shadow: 0 10px 26px -12px rgba(56,189,248,.6), 0 0 0 0 rgba(125,211,252,.5);
}
.abo-aktion-neben:hover,
.abo-aktion-blau:hover {
box-shadow: 0 16px 34px -10px rgba(56,189,248,.75), 0 0 30px rgba(125,211,252,.55);
}
@keyframes abo-blau-atem {
0%, 100% { box-shadow: 0 10px 26px -12px rgba(56,189,248,.55), 0 0 14px rgba(125,211,252,.25); }
50% { box-shadow: 0 12px 30px -12px rgba(56,189,248,.75), 0 0 26px rgba(125,211,252,.5); }
}
@media (prefers-reduced-motion: reduce) {
.abo-aktionsleiste > button,
.abo-aktionsleiste > button::after { animation: none; }
}
/* Auf schmalen Handys untereinander statt gequetscht nebeneinander. */
@media (max-width: 430px) {
.abo-aktionsleiste { flex-direction: column; gap: .55rem; }
.abo-aktionsleiste > button { font-size: .92rem; }
}
.abo-oder { .abo-oder {
display: flex; align-items: center; gap: .8rem; display: flex; align-items: center; gap: .8rem;
color: var(--text-muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .08em;
@@ -265,7 +363,12 @@
<div class="abo-logo-glut" aria-hidden="true"></div> <div class="abo-logo-glut" aria-hidden="true"></div>
<div class="abo-logo-wz" aria-hidden="true"></div> <div class="abo-logo-wz" aria-hidden="true"></div>
<span class="eyebrow" data-i18n="ab_preis_eyebrow">DogiCrew 🩵</span> <span class="eyebrow" data-i18n="ab_preis_eyebrow">DogiCrew 🩵</span>
<div class="abo-preis-zahl">4,99 € <small data-i18n="ab_preis_pro_monat">/ Monat</small></div> <!-- Bugfix/Nutzer-Wunsch 20.08.2026: Preis noch NICHT zeigen, solange die
Registrierung gesperrt ist (siehe .btn-gold-locked weiter unten) — sonst
wäre der Preis schon öffentlich, bevor überhaupt jemand abschließen kann.
Gleicher Chrome-Gold-Schimmer-Text wie beim gesperrten Knopf
(.btn-gold-locked-text), hier freistehend statt in einem Knopf. -->
<div class="abo-preis-zahl"><span class="btn-gold-locked-text" style="font-size:1em;" data-i18n="ab_kollektion_coming_soon">Coming soon…</span></div>
<p class="small muted" style="margin-top:-.6rem;margin-bottom:1rem;">Keine gesondert ausgewiesene MwSt. gemäß Kleinunternehmerregelung (Art. 57 des luxemburgischen TVA-Gesetzes) — keine weiteren Kosten.</p> <p class="small muted" style="margin-top:-.6rem;margin-bottom:1rem;">Keine gesondert ausgewiesene MwSt. gemäß Kleinunternehmerregelung (Art. 57 des luxemburgischen TVA-Gesetzes) — keine weiteren Kosten.</p>
<ul class="abo-bedingungen"> <ul class="abo-bedingungen">
<li data-i18n="ab_bed1">Monatliche automatische Zahlung über PayPal</li> <li data-i18n="ab_bed1">Monatliche automatische Zahlung über PayPal</li>
@@ -308,6 +411,12 @@
<!-- Zustand 2: Registrierung --> <!-- Zustand 2: Registrierung -->
<div class="abo-panel" id="panel-register" hidden> <div class="abo-panel" id="panel-register" hidden>
<!-- Bugfix/Nutzer-Wunsch 20.08.2026 (Nachpruefung): Nur der Knopf war
gesperrt, die drei Felder ließen sich trotzdem antippen und
beschreiben ("ich kann immer nur was rein tippen, soll garnicht
möglich sein"). Jetzt wie das E-Mail-Feld im Login-Panel: disabled
statt nur readonly, damit auch Tastatur-/Screenreader-Nutzung
korrekt "nicht bedienbar" meldet. -->
<div class="abo-field"> <div class="abo-field">
<label for="reg-name" data-i18n="ab_feld_name">Name oder Anzeigename</label> <label for="reg-name" data-i18n="ab_feld_name">Name oder Anzeigename</label>
<input type="text" id="reg-name" maxlength="60" /> <input type="text" id="reg-name" maxlength="60" />
@@ -320,23 +429,68 @@
<label for="reg-email" data-i18n="ab_feld_email">E-Mail-Adresse</label> <label for="reg-email" data-i18n="ab_feld_email">E-Mail-Adresse</label>
<input type="email" id="reg-email" maxlength="120" /> <input type="email" id="reg-email" maxlength="120" />
</div> </div>
<button type="button" class="btn btn-primary" style="width:100%;" id="btn-register" data-i18n="ab_btn_register">Registrieren</button> <!-- Das Zugangscode-Feld stand bis 21.08.2026 hier, direkt neben den
Stammdaten. Es ist bewusst in einen eigenen, SPÄTEREN Schritt
gewandert (Nutzer-Wunsch: "bei der ersten registrierung sollen
die eine email bekommen mit einem einmaligen code damit wir auch
wissen dass email stimmt und danach wenn sie den code eingegeben
haben sollen die erst ihren zugangscode auswählen/eintippen
können"). Ablauf jetzt: hier Stammdaten -> Einmalcode per Mail
(#panel-code) -> Zugangscode festlegen (#panel-neuer-code). -->
<p class="small muted" style="margin:-.2rem 0 .2rem;" data-i18n="ab_reg_hinweis_bestaetigung">Wir schicken dir gleich einen 6-stelligen Code per E-Mail. Danach legst du deinen eigenen Zugangscode fest.</p>
<!-- Nutzer-Wunsch 21.08.2026: "die leute sollen sich auch schon
registrieren können, aber der abo soll noch zu sein, da soll
dann coming soon stehen." Registrierung/Login sind ab jetzt
offen -- Konten können angelegt werden. Gesperrt bleibt NUR
noch der eigentliche Zahlungsschritt danach (siehe
panel-subscribe weiter unten), weil ohne Dogis echte PayPal-
Business-Zugangsdaten niemand wirklich zahlen kann. Wieder
normaler .btn-primary-Stil statt .btn-gold-locked. -->
<!-- Aktionsleiste (21.08.2026, Nutzer-Wunsch: "bringt nichts 2 mal
registrieren da zu haben"). EIN Element statt vorher Umschalter
oben + Knopf unten: links die eigentliche Aktion in Gold,
rechts der Wechsel zum Einloggen in Babyblau. Beide Hälften
sind immer aktiv (kein Tab-Zustand) — links macht etwas,
rechts wechselt die Ansicht. -->
<div class="abo-aktionsleiste">
<button type="button" class="abo-aktion-haupt" id="btn-register" data-i18n="ab_btn_register">Registrieren</button>
<button type="button" class="abo-aktion-neben" id="tab-login" data-i18n="ab_tab_login">Schon registriert? Einloggen</button>
</div>
<p class="abo-fehler" id="register-fehler"></p> <p class="abo-fehler" id="register-fehler"></p>
<p class="abo-switch"><a id="switch-to-login" data-i18n="ab_switch_login">Schon Supporter? Hier einloggen</a></p>
</div> </div>
<!-- Zustand 3: Login (E-Mail-Code anfordern) --> <!-- Zustand 3: Einloggen mit E-Mail + eigenem Zugangscode.
Umgestellt 21.08.2026 (Nutzer-Wunsch: "wieso sollte ich wieder
ein code bekommen, mach die anmeldung einfach"): vorher wurde
auch hier bei JEDEM Login erst ein Einmalcode per E-Mail
verschickt — für längst registrierte Leute ein völlig
unnötiger Umweg. Der E-Mail-Weg existiert weiterhin, aber nur
noch als "Code vergessen?" (siehe Link unten). -->
<div class="abo-panel" id="panel-login" hidden> <div class="abo-panel" id="panel-login" hidden>
<div class="abo-field"> <div class="abo-field">
<label for="login-email" data-i18n="ab_feld_email">E-Mail-Adresse</label> <label for="login-email" data-i18n="ab_feld_email">E-Mail-Adresse</label>
<input type="email" id="login-email" maxlength="120" /> <input type="email" id="login-email" maxlength="120" autocomplete="email" />
</div>
<div class="abo-field">
<label for="login-code" data-i18n="ab_feld_code_eigen">Dein Zugangscode</label>
<input type="password" id="login-code" maxlength="200" autocomplete="current-password" />
</div>
<!-- Nutzer-Wunsch 21.08.2026: "da soll nur noch einloggen sein und
der knopf soll auch blau sein, der neu registriert soll weg."
Hier steht deshalb nur EIN Knopf, in Babyblau statt Gold —
wer auf dieser Ansicht gelandet ist, will einloggen; der Weg
zurück zur Registrierung führt über den Zurück-Knopf des
Browsers bzw. den blauen Knopf auf der Registrierungs-Seite. -->
<div class="abo-aktionsleiste">
<button type="button" class="abo-aktion-blau" id="btn-login" data-i18n="ab_btn_login">Einloggen</button>
</div> </div>
<button type="button" class="btn btn-primary" style="width:100%;" id="btn-login-request" data-i18n="ab_btn_login_code">Login-Code anfordern</button>
<p class="abo-fehler" id="login-fehler"></p> <p class="abo-fehler" id="login-fehler"></p>
<p class="abo-switch"><a id="switch-to-register" data-i18n="ab_switch_register">Noch kein Konto? Jetzt registrieren</a></p> <p class="abo-switch"><a id="code-vergessen" data-i18n="ab_code_vergessen">Code vergessen?</a></p>
</div> </div>
<!-- Zustand 4: Code-Eingabe (nach Registrierung ODER Login-Anfrage) --> <!-- Zustand 4: Einmalcode aus der E-Mail eingeben. Kommt seit
21.08.2026 NUR noch im "Code vergessen?"-Weg vor — die normale
Registrierung/Anmeldung braucht ihn nicht mehr. -->
<div class="abo-panel" id="panel-code" hidden> <div class="abo-panel" id="panel-code" hidden>
<p class="small" id="code-hinweis" data-i18n="ab_code_hinweis">Wir haben dir einen 6-stelligen Code per E-Mail geschickt.</p> <p class="small" id="code-hinweis" data-i18n="ab_code_hinweis">Wir haben dir einen 6-stelligen Code per E-Mail geschickt.</p>
<div class="abo-field"> <div class="abo-field">
@@ -347,6 +501,20 @@
<p class="abo-fehler" id="code-fehler"></p> <p class="abo-fehler" id="code-fehler"></p>
</div> </div>
<!-- Zustand 4b: neuen dauerhaften Zugangscode festlegen. Letzter
Schritt des "Code vergessen?"-Weges — an dieser Stelle ist man
durch den Einmalcode bereits eingeloggt, es fehlt nur noch der
neue Code für künftige Anmeldungen. -->
<div class="abo-panel" id="panel-neuer-code" hidden>
<p class="small" data-i18n="ab_neuer_code_hinweis">Fast fertig — leg jetzt deinen neuen Zugangscode fest.</p>
<div class="abo-field">
<label for="neuer-code" data-i18n="ab_feld_code_neu">Dein Zugangscode (mindestens 6 Zeichen)</label>
<input type="password" id="neuer-code" maxlength="200" autocomplete="new-password" />
</div>
<button type="button" class="btn btn-primary" style="width:100%;" id="btn-neuer-code" data-i18n="ab_btn_code_speichern">Zugangscode speichern</button>
<p class="abo-fehler" id="neuer-code-fehler"></p>
</div>
<!-- Zustand 5: eingeloggt, noch kein Abo -> PayPal Smart Buttons. <!-- Zustand 5: eingeloggt, noch kein Abo -> PayPal Smart Buttons.
Zeigt automatisch alle für die Käuferin/den Käufer verfügbaren Zeigt automatisch alle für die Käuferin/den Käufer verfügbaren
Zahlarten (PayPal-Konto, Kreditkarte als Gast, je nach Land) — Zahlarten (PayPal-Konto, Kreditkarte als Gast, je nach Land) —
@@ -354,24 +522,45 @@
<div class="abo-panel" id="panel-subscribe" hidden> <div class="abo-panel" id="panel-subscribe" hidden>
<p class="abo-erfolg zeige" data-i18n="ab_eingeloggt_als">Eingeloggt.</p> <p class="abo-erfolg zeige" data-i18n="ab_eingeloggt_als">Eingeloggt.</p>
<!-- Button-Lösung (§ 312j BGB / entspr. Regelung): Preis- und <!-- Nutzer-Wunsch 21.08.2026: "die leute sollen sich schon registrieren
Zahlungspflicht-Hinweis unmittelbar vor der Zahlung. --> können, aber der abo soll noch zu sein, da soll dann coming soon
<p class="small" style="text-align:center;font-weight:700;margin-bottom:.8rem;">4,99 € / Monat · zahlungspflichtiges Abo · monatlich kündbar</p> stehen." Konto ist angelegt/Login hat geklappt -- aber die eigent-
liche Zahlung (PayPal) bleibt gesperrt, bis Dogis echte PayPal-
Business-Zugangsdaten stehen. Die echten Zahlungs-Elemente (Preis-
Hinweis, Widerrufs-Checkbox, PayPal-Container) bleiben unten UNVER-
ÄNDERT im DOM (nur per hidden versteckt), damit das Skript weiter
unten (Checkbox-Listener, ladePaypalButtons() usw.) unangetastet
funktioniert -- einfach das hidden am Wrapper unten entfernen,
sobald PayPal bereitsteht. -->
<div class="btn-gold-locked" style="width:100%;margin-top:.4rem;cursor:default;">
<span class="btn-gold-locked-lock" aria-hidden="true">🔒</span>
<span class="btn-gold-locked-text" data-i18n="ab_kollektion_coming_soon">Coming soon…</span>
</div>
<p class="small muted" style="text-align:center;margin-top:.9rem;" data-i18n="ab_subscribe_locked_hinweis">Dein Konto ist bereit — die Bezahlung startet, sobald das Abo offiziell live geht. Schau einfach bald wieder vorbei.</p>
<!-- Widerrufsrecht bei digitalen Inhalten (§ 356 Abs. 5 BGB / <!-- Nutzer-Wunsch 21.08.2026: "ich will dass jetzt alles verbunden
entspr. Regelung, siehe agb.html Ziffer 6): ohne aktives ist so dass man mit den klicks pro klick weiter kommt" — vorher
Ankreuzen bleibt der Zahlungsbereich gesperrt, es gibt keine war hier eine Sackgasse (kein Link/Knopf mehr nach dem Login).
Zahlung ohne diese ausdrückliche, nicht vorausgewählte Jetzt zwei klare Wege weiter: zurück zur Startseite, oder
Zustimmung. --> direkt zum Streamplan (naheliegendster nächster Klick für
<label class="abo-widerruf-check" for="widerruf-consent"> jemanden, der sich gerade für Dogi interessiert). -->
<input type="checkbox" id="widerruf-consent" /> <div class="btn-row" style="justify-content:center;margin-top:1.4rem;">
<span>Ich wünsche ausdrücklich, dass der Zugang zum Supporter-Bereich sofort mit Vertragsschluss beginnt, und weiß, dass mein 14-tägiges Widerrufsrecht dadurch erlischt, sobald der Zugang vollständig bereitgestellt ist (Details in den <a href="agb.html" target="_blank" rel="noopener">AGB</a>, Ziffer 6).</span> <a class="btn btn-outline" href="index.html" data-i18n="ab_weiter_startseite">Zur Startseite →</a>
</label> <a class="btn btn-outline" href="streamplan.html" data-i18n="ab_weiter_streamplan">Zum Streamplan →</a>
</div>
<div id="paypal-button-container" class="paypal-gesperrt"></div> <div hidden>
<p class="small muted" id="paypal-gesperrt-hinweis">Bitte bestätige oben die Checkbox, um die Zahlungsoptionen freizuschalten.</p> <p class="small" style="text-align:center;font-weight:700;margin-bottom:.8rem;">4,99 € / Monat · zahlungspflichtiges Abo · monatlich kündbar</p>
<p class="small" id="paypal-laedt" data-i18n="ab_paypal_laedt" hidden>Zahlungsoptionen werden geladen …</p> <label class="abo-widerruf-check" for="widerruf-consent">
<p class="abo-fehler" id="subscribe-fehler"></p> <input type="checkbox" id="widerruf-consent" />
<span>Ich wünsche ausdrücklich, dass der Zugang zum Supporter-Bereich sofort mit Vertragsschluss beginnt, und weiß, dass mein 14-tägiges Widerrufsrecht dadurch erlischt, sobald der Zugang vollständig bereitgestellt ist (Details in den <a href="agb.html" target="_blank" rel="noopener">AGB</a>, Ziffer 6).</span>
</label>
<div id="paypal-button-container" class="paypal-gesperrt"></div>
<p class="small muted" id="paypal-gesperrt-hinweis">Bitte bestätige oben die Checkbox, um die Zahlungsoptionen freizuschalten.</p>
<p class="small" id="paypal-laedt" data-i18n="ab_paypal_laedt" hidden>Zahlungsoptionen werden geladen …</p>
<p class="abo-fehler" id="subscribe-fehler"></p>
</div>
</div> </div>
<!-- Zustand 6: bereits Supporter --> <!-- Zustand 6: bereits Supporter -->
@@ -387,7 +576,10 @@
<div class="container grid grid-2"> <div class="container grid grid-2">
<div class="card"> <div class="card">
<p data-i18n="ab_text1">Dieses Abonnement ist kein gewöhnlicher Shop und kein einfacher Kauf. Es ist für die Menschen gedacht, die Dogfather dauerhaft unterstützen und ein besonderer Teil dieser gemeinsamen Reise sein möchten.</p> <p data-i18n="ab_text1">Dieses Abonnement ist kein gewöhnlicher Shop und kein einfacher Kauf. Es ist für die Menschen gedacht, die Dogfather dauerhaft unterstützen und ein besonderer Teil dieser gemeinsamen Reise sein möchten.</p>
<p data-i18n="ab_text2">Für 4,99 € im Monat erhältst du Zugang zu einem exklusiven Supporter-Bereich, besonderen Inhalten und einer persönlichen Treuekollektion. Mit jedem erfolgreich bezahlten Meilenstein wächst deine Sammlung: vom kleinen Hasen-Teddy über besondere Jubiläumsprämien bis zum exklusiven Hoodie nach 30 Monaten. Danach beginnt für dich eine neue Kollektion — mit neuen Designs, neuen Erinnerungen und derselben Bedeutung.</p> <!-- Bugfix/Nutzer-Wunsch 20.08.2026: "4,99 €" hier entfernt (nur die Zahl, Rest
des Satzes unverändert) — sonst hätte der eigentlich gesperrte Preis
weiter oben in der Preis-Box hier im Fließtext trotzdem gestanden. -->
<p data-i18n="ab_text2">Mit deinem monatlichen Beitrag erhältst du Zugang zu einem exklusiven Supporter-Bereich, besonderen Inhalten und einer persönlichen Treuekollektion. Mit jedem erfolgreich bezahlten Meilenstein wächst deine Sammlung: vom kleinen Hasen-Teddy über besondere Jubiläumsprämien bis zum exklusiven Hoodie nach 30 Monaten. Danach beginnt für dich eine neue Kollektion — mit neuen Designs, neuen Erinnerungen und derselben Bedeutung.</p>
<ul class="abo-vorteile"> <ul class="abo-vorteile">
<li data-i18n="ab_vorteil1">Exklusiver Supporter-Bereich</li> <li data-i18n="ab_vorteil1">Exklusiver Supporter-Bereich</li>
<li data-i18n="ab_vorteil2">Kleiner Hasen-Teddy nach 6 bezahlten Monaten</li> <li data-i18n="ab_vorteil2">Kleiner Hasen-Teddy nach 6 bezahlten Monaten</li>
@@ -411,13 +603,13 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-abonnieren.js"></script> <script src="assets/js/i18n-abonnieren.js?v=20260821o"></script>
<script src="assets/js/supporter.js"></script> <script src="assets/js/supporter.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
(function () { (function () {
const S = window.DogiSupporter; const S = window.DogiSupporter;
const panels = ["panel-lade", "panel-register", "panel-login", "panel-code", "panel-subscribe", "panel-already"]; const panels = ["panel-lade", "panel-register", "panel-login", "panel-code", "panel-neuer-code", "panel-subscribe", "panel-already"];
// panel-social-login (Google-Button) hängt sich an register/login dran, // panel-social-login (Google-Button) hängt sich an register/login dran,
// ist aber kein "richtiger" exklusiver Zustand — separat gesteuert. // ist aber kein "richtiger" exklusiver Zustand — separat gesteuert.
const SOCIAL_LOGIN_PANELS = ["panel-register", "panel-login"]; const SOCIAL_LOGIN_PANELS = ["panel-register", "panel-login"];
@@ -455,8 +647,14 @@
el.classList.remove("zeige"); el.classList.remove("zeige");
} }
let codePurpose = "verify_email"; // oder "login" // Merkt sich die E-Mail-Adresse zwischen "Code vergessen?" und der
// Eingabe des Einmalcodes (der frühere codePurpose-Schalter entfiel mit
// der Umstellung 21.08.2026 — es gibt nur noch diesen einen Code-Weg).
let codeEmail = ""; let codeEmail = "";
// Merkt sich, WARUM gerade ein Einmalcode eingegeben wird (21.08.2026):
// "register" = Erstbestätigung der E-Mail, "login" = "Code vergessen?".
// Steuert in btn-code-verify, welcher Endpunkt aufgerufen wird.
let codeZweck = "login";
async function pruefeStatus() { async function pruefeStatus() {
if (!S.getToken()) { if (!S.getToken()) {
@@ -468,11 +666,24 @@
const status = data.subscription?.status || "none"; const status = data.subscription?.status || "none";
S.cacheSubStatus(status); S.cacheSubStatus(status);
S.cacheDisplayName(data.profile?.displayName || ""); S.cacheDisplayName(data.profile?.displayName || "");
if (["active", "pending", "suspended", "cancelled"].includes(status)) { /* Nutzer-Wunsch 21.08.2026: "anstatt das was du jetzt auf dem screen
zeige("panel-already"); siehst, sollen die leute nicht mehr sehen wenn sie angemeldet sind.
} else { sie sollen die richtige seite sehen die wir schon mal vorbereitet
zeige("panel-subscribe"); hatten, wo sie ihre persönliche daten sehen und so."
}
Wer eingeloggt ist, gehört nicht mehr auf diese Anmeldeseite —
egal ob mit oder ohne bezahltes Abo. supporter.html zeigt in BEIDEN
Fällen das Richtige: persönliche Daten, Fortschritt mit allen
Prämien-Meilensteinen, Sammlungsarchiv, Profil bearbeiten,
Ausloggen — bei "kein Abo" eben mit entsprechendem Hinweis statt
echter Laufzeit. Vorher landete man hier stattdessen in einer
Sackgasse ("Coming soon…"), ohne je die eigentliche Seite zu sehen.
replace() statt href: die Anmeldeseite verschwindet damit aus dem
Verlauf — ein "Zurück" führt sonst sofort wieder hierher und von
hier wieder weiter, was sich wie eine Schleife anfühlt. */
location.replace("supporter.html");
return;
} catch (err) { } catch (err) {
// Token ungültig/abgelaufen -> zurück zur Registrierung/Login // Token ungültig/abgelaufen -> zurück zur Registrierung/Login
S.clearToken(); S.clearToken();
@@ -585,8 +796,10 @@
ladeHinweis.hidden = true; ladeHinweis.hidden = true;
} }
document.getElementById("switch-to-login").addEventListener("click", () => zeige("panel-login")); // Der frühere Gegenstück-Knopf "Neu registrieren" auf der Einloggen-Seite
document.getElementById("switch-to-register").addEventListener("click", () => zeige("panel-register")); // wurde am 21.08.2026 auf Wunsch entfernt — deshalb hier nur noch der eine
// Wechsel von der Registrierung zum Einloggen.
document.getElementById("tab-login").addEventListener("click", () => zeige("panel-login"));
// Widerrufsrecht-Checkbox: erst nach aktivem Ankreuzen werden die // Widerrufsrecht-Checkbox: erst nach aktivem Ankreuzen werden die
// PayPal-Zahlungsoptionen überhaupt geladen/klickbar (siehe agb.html // PayPal-Zahlungsoptionen überhaupt geladen/klickbar (siehe agb.html
@@ -615,24 +828,44 @@
const email = document.getElementById("reg-email").value.trim(); const email = document.getElementById("reg-email").value.trim();
if (!displayName || !email) return fehlerZeigen(el, "Bitte Name und E-Mail-Adresse angeben."); if (!displayName || !email) return fehlerZeigen(el, "Bitte Name und E-Mail-Adresse angeben.");
try { try {
// Schritt 1 von 3 (21.08.2026): /supporter/register legt nur das noch
// unbestätigte Konto an und verschickt einen Einmalcode -- es kommt
// bewusst KEIN Token zurück, eingeloggt ist man erst nach Schritt 2.
await S.apiFetch("/supporter/register", { displayName, tiktokUsername, email }); await S.apiFetch("/supporter/register", { displayName, tiktokUsername, email });
codePurpose = "verify_email";
codeEmail = email; codeEmail = email;
codeZweck = "register";
zeige("panel-code"); zeige("panel-code");
} catch (err) { } catch (err) {
fehlerZeigen(el, err.message); fehlerZeigen(el, err.message);
} }
}); });
document.getElementById("btn-login-request").addEventListener("click", async () => { // Normale Anmeldung: E-Mail + eigener Zugangscode, ohne Umweg über E-Mail.
document.getElementById("btn-login").addEventListener("click", async () => {
const el = document.getElementById("login-fehler"); const el = document.getElementById("login-fehler");
fehlerVerstecken(el); fehlerVerstecken(el);
const email = document.getElementById("login-email").value.trim(); const email = document.getElementById("login-email").value.trim();
if (!email) return fehlerZeigen(el, "Bitte E-Mail-Adresse angeben."); const accessCode = document.getElementById("login-code").value;
if (!email || !accessCode) return fehlerZeigen(el, "Bitte E-Mail-Adresse und Zugangscode eingeben.");
try {
const data = await S.apiFetch("/supporter/login-code", { email, accessCode });
S.setToken(data.token);
await pruefeStatus();
} catch (err) {
fehlerZeigen(el, err.message);
}
});
// "Code vergessen?" — hier lebt der alte E-Mail-Einmalcode-Weg weiter.
document.getElementById("code-vergessen").addEventListener("click", async () => {
const el = document.getElementById("login-fehler");
fehlerVerstecken(el);
const email = document.getElementById("login-email").value.trim();
if (!email) return fehlerZeigen(el, "Bitte zuerst deine E-Mail-Adresse eingeben.");
try { try {
await S.apiFetch("/supporter/login-request", { email }); await S.apiFetch("/supporter/login-request", { email });
codePurpose = "login";
codeEmail = email; codeEmail = email;
codeZweck = "login";
zeige("panel-code"); zeige("panel-code");
} catch (err) { } catch (err) {
fehlerZeigen(el, err.message); fehlerZeigen(el, err.message);
@@ -645,9 +878,26 @@
const code = document.getElementById("code-input").value.trim(); const code = document.getElementById("code-input").value.trim();
if (!code) return fehlerZeigen(el, "Bitte Code eingeben."); if (!code) return fehlerZeigen(el, "Bitte Code eingeben.");
try { try {
const path = codePurpose === "login" ? "/supporter/verify-login" : "/supporter/verify-email"; // Dieselbe Code-Eingabe bedient zwei Wege (21.08.2026):
const data = await S.apiFetch(path, { email: codeEmail, code }); // "register" -> Erstbestätigung der E-Mail bei der Registrierung
// "login" -> "Code vergessen?"-Weg für bestehende Konten
// Beide enden gleich: eingeloggt, danach Zugangscode festlegen.
const endpunkt = codeZweck === "register" ? "/supporter/verify-email" : "/supporter/verify-login";
const data = await S.apiFetch(endpunkt, { email: codeEmail, code });
S.setToken(data.token); S.setToken(data.token);
zeige("panel-neuer-code");
} catch (err) {
fehlerZeigen(el, err.message);
}
});
document.getElementById("btn-neuer-code").addEventListener("click", async () => {
const el = document.getElementById("neuer-code-fehler");
fehlerVerstecken(el);
const accessCode = document.getElementById("neuer-code").value;
if (!accessCode) return fehlerZeigen(el, "Bitte leg einen Zugangscode fest.");
try {
await S.apiFetch("/supporter/set-access-code", { accessCode }, true);
await pruefeStatus(); await pruefeStatus();
} catch (err) { } catch (err) {
fehlerZeigen(el, err.message); fehlerZeigen(el, err.message);
@@ -662,6 +912,12 @@
} catch { } catch {
/* Konfiguration konnte nicht geladen werden — Google-Button/PayPal-Buttons bleiben dann aus. */ /* Konfiguration konnte nicht geladen werden — Google-Button/PayPal-Buttons bleiben dann aus. */
} }
/* Bugfix 20.08.2026, wieder aktiviert 21.08.2026: "Mit Google anmelden" war ein
dritter, bisher übersehener Weg, trotz gesperrtem Registrieren-/Login-Knopf
doch ein echtes Konto anzulegen bzw. sich einzuloggen. War deshalb deaktiviert,
solange die Registrierung selbst gesperrt war. Registrierung ist jetzt (Nutzer-
Wunsch 21.08.2026) bewusst offen -- Google-Login ist nur ein weiterer,
gleichwertiger Weg zum selben jetzt erlaubten Ziel, kein Schlupfloch mehr. */
initGoogleConsent(); initGoogleConsent();
await pruefeStatus(); await pruefeStatus();
} }
+5 -5
View File
@@ -20,10 +20,10 @@
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body> <body>
<div id="site-header"></div> <div id="site-header"></div>
@@ -134,6 +134,6 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+7 -7
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Detailansicht einer Aktion oder eines Projekts von Team Dogi." /> <meta name="twitter:description" content="Detailansicht einer Aktion oder eines Projekts von Team Dogi." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body> <body>
<div id="site-header"></div> <div id="site-header"></div>
@@ -51,9 +51,9 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-aktionen.js"></script> <script src="assets/js/data-aktionen.js?v=20260821o"></script>
<script src="assets/js/i18n-aktion-detail.js"></script> <script src="assets/js/i18n-aktion-detail.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
+808 -26
View File
@@ -121,17 +121,14 @@ header.site-header {
braucht durch größere Schrift + mehr Abstand + Polarlicht-Padding braucht durch größere Schrift + mehr Abstand + Polarlicht-Padding
spürbar mehr Platz, ohne dass der restliche Seiteninhalt breiter spürbar mehr Platz, ohne dass der restliche Seiteninhalt breiter
werden soll. werden soll.
Bugfix 04.08.2026: Der feste 1400px-Deckel war zu knapp — der Bugfix 04.08.2026: 1400px -> 1440px angehoben (siehe damalige Analyse).
tatsächliche Inhalt (Logo+Menü+Sprachschalter+DogiCrew-Knopf+ Bugfix 20.08.2026: reichte seither wieder nicht mehr — die Nav braucht
Bewerben-Knopf) braucht bei aufgeklapptem Desktop-Menü (ab 1480px inzwischen (Nav-Punkt "DogiCrew" + "BALD"-Badge dazugekommen) real ca.
Breite, siehe Media Query unten) ca. 1417px und lief dadurch IMMER 1491px, der Deckel blieb bei 1440px. 1440px -> 1560px, zusammen mit der
um ca. 17px über den Rahmen hinaus — auf breiten Bildschirmen ohne auf 1650px angehobenen Hamburger-Schwelle weiter unten (siehe Kommentar
sichtbaren Schaden (genug Luft drumherum), aber bei schmaleren dort) — diesmal mit echtem Sicherheitsabstand statt wieder nur knapp
Fenstern knapp über der 1480px-Schwelle (z.B. Laptop-Fenster nicht genug für den aktuellen Stand. */
maximiert, oder Browser-Zoom >100%) wurde dadurch der rechte Rand width: min(1560px, 94vw);
(der "Bewerben"-Knopf) sichtbar abgeschnitten. 1400px -> 1440px gibt
ausreichend Puffer, ohne auf breiten Screens spürbar größer zu wirken. */
width: min(1440px, 94vw);
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -414,11 +411,21 @@ header.site-header {
0% { background-position: 160% 0; } 0% { background-position: 160% 0; }
100% { background-position: -60% 0; } 100% { background-position: -60% 0; }
} }
/* Bugfix 20.08.2026 (Nutzer-Report per Screenshot-Vergleich: Kategorie-
Menüs wie "Medien" sind mittig unter dem Knopf, der Sprachschalter aber
rechtsbündig) — war bewusst rechtsbündig, seit der Sprachschalter aber
nicht mehr ganz am rechten Rand sitzt (DogiCrew-/Bewerben-Knopf daneben),
ist genug Platz für dieselbe zentrierte Ausrichtung wie überall sonst.
left/transform hier deshalb NICHT mehr überschrieben (erbt jetzt
left:50%/translateX(-50%) von der allgemeinen .nav-dropdown-Regel,
genau wie der bereits zentrierte Verbindungs-Pfeil (::before) es die
ganze Zeit schon war — vorher zeigte der Pfeil mittig, während die Box
selbst daneben saß). Eigene Animation bleibt bestehen (siehe Kommentar
der allgemeinen .nav-dropdown-Animation), jetzt aber ebenfalls mit
translateX(-50%) in jedem Frame, sonst würde sie die Zentrierung wieder
aufheben. */
.sprach-schalter .nav-dropdown { .sprach-schalter .nav-dropdown {
min-width: 190px; min-width: 190px;
left: auto;
right: 0;
transform: none;
padding: .4rem; padding: .4rem;
border-radius: 14px; border-radius: 14px;
border: 1px solid rgba(210, 216, 224, .55); border: 1px solid rgba(210, 216, 224, .55);
@@ -427,12 +434,11 @@ header.site-header {
0 14px 34px rgba(0, 0, 0, .6), 0 14px 34px rgba(0, 0, 0, .6),
0 0 16px rgba(220, 226, 235, .35), 0 0 16px rgba(220, 226, 235, .35),
0 0 34px rgba(190, 200, 215, .18); 0 0 34px rgba(190, 200, 215, .18);
transform-origin: top right;
animation: sprach-dropdown-in .18s ease both, sprach-silber-puls 3.6s ease-in-out infinite; animation: sprach-dropdown-in .18s ease both, sprach-silber-puls 3.6s ease-in-out infinite;
} }
@keyframes sprach-dropdown-in { @keyframes sprach-dropdown-in {
from { opacity: 0; transform: translateY(-6px) scale(.96); } from { opacity: 0; transform: translateX(-50%) translateY(-6px) scale(.96); }
to { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
} }
/* Silberner Polarlicht-Schein um den Dropdown-Rand — bewusst neutral /* Silberner Polarlicht-Schein um den Dropdown-Rand — bewusst neutral
silber statt farbig, damit sich der schwarze Kasten von der bunten silber statt farbig, damit sich der schwarze Kasten von der bunten
@@ -613,6 +619,34 @@ header.site-header {
0 0 22px rgba(143, 217, 234, .35), 0 0 22px rgba(143, 217, 234, .35),
0 0 22px rgba(139, 92, 246, .3); 0 0 22px rgba(139, 92, 246, .3);
} }
/* "Bald"-Badge auf dem Abo-Knopf, solange die Registrierung auf
abonnieren.html gesperrt ist (20.08.2026, siehe .btn-gold-locked weiter
unten + main.js renderAboButton/soonBadge). Sitzt als zweites Kind IN
der Pille (nicht als eigenes Nav-Element), bleibt dadurch Teil desselben
Klicks — der Knopf führt bewusst weiterhin ganz normal zu abonnieren.html,
nur die Registrierung selbst dort ist gesperrt. Bewusst neutral-silbern
(Wiederverwendung von .btn-silver-text/-spark, nur kleiner skaliert)
statt Gold, damit es nicht mit der babyblau/lila-Aurora dieser Pille
konkurriert — reine Status-Markierung, kein zweiter Marken-Akzent. */
.nav-cta-soon {
position: relative;
z-index: 2;
display: inline-flex;
align-items: center;
gap: .22rem;
margin-left: .5rem;
padding: .15rem .5rem;
border-radius: 999px;
font-size: .64rem;
font-weight: 800;
letter-spacing: .04em;
text-transform: uppercase;
background: rgba(255, 255, 255, .07);
border: 1px solid rgba(255, 255, 255, .3);
box-shadow: 0 0 10px rgba(230, 235, 242, .3);
flex-shrink: 0;
}
.nav-cta-soon .btn-silver-spark { font-size: .85em; }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.nav-cta::before, .nav-cta::after { animation: none; } .nav-cta::before, .nav-cta::after { animation: none; }
} }
@@ -937,6 +971,108 @@ header.site-header {
} }
} }
/* =====================================================================
"Gesperrter Tresor"-Knopf für den DogiCrew-Registrieren-Knopf
(abonnieren.html, 20.08.2026, Nutzer-Wunsch: "richtig krank übertrieben
geiles... weil es wird ja das geilste Bonus und überhaupt so von mir").
Bewusst KEINE Wiederverwendung von .btn-silver — der ist neutral-grau und
auf DIESER Seite bereits für die Teddy-Kollektion (Nebensache) vergeben.
Chrome-Text-Clip-Schimmer + Schimmer-Sweep + Halo-Puls wie .btn-silver/
.btn-legendary, aber in den ECHTEN Gold-Tönen dieser Seite
(#ffe9a8/#d4af37, siehe .abo-panel .btn) und mit einem pulsierenden
Schloss statt Sternen, damit die Haupt-CTA sichtbar "das Besondere,
noch verschlossene" bleibt statt nur "irgendwann verfügbar".
WICHTIG: Für den Rand bewusst NICHT die rotierende Conic-Gradient-Maske
von .btn-silver/.btn-legendary kopiert — dieser Knopf läuft über
`width:100%` auf voller Kartenbreite (~440px) bei nur ~62px Höhe, also
ein sehr langgestrecktes Rechteck. Genau für diesen Fall ist die
dokumentierte Lektion in Projektregeln.md Punkt 14 gedacht (03.08.2026,
"TikTok-Button-Bug"): ein rotierendes maskiertes Pseudo-Element verzieht
sich auf länglichen Formen zu einer krummen Schlaufe, weil ein Rechteck
beim Rotieren nicht rotationssymmetrisch ist — live mit Playwright-
Screenshots über mehrere Frames nachgewiesen (die Schlaufe ist der
rotierende Rand bei verschiedenen Winkeln). Stattdessen derselbe sichere
Zwei-Layer-Background-Trick wie bei .abo-google-frame weiter oben in
dieser Datei (Füllung padding-box + Verlauf border-box, Bewegung nur
über background-position) — bleibt geometrisch exakt an der Kontur,
unabhängig vom Seitenverhältnis. Reine Deko/Statusanzeige — echte
Deaktivierung läuft über das native disabled-Attribut am <button>
(siehe abonnieren.html), nicht über pointer-events, damit auch
Tastatur-/Screenreader-Nutzung korrekt "nicht bedienbar" meldet. Bewusst
KEIN :hover-Zustand (keine Lift-Animation wie bei .btn-silver/
.btn-legendary) — soll nicht so wirken, als wäre er doch klickbar. */
.btn-gold-locked {
position: relative;
isolation: isolate;
overflow: hidden;
display: inline-flex;
align-items: center;
justify-content: center;
gap: .65rem;
padding: 1rem 1.4rem;
border-radius: 14px;
font-weight: 800;
font-size: 1.04rem;
letter-spacing: .02em;
border: 1.5px solid transparent;
background:
linear-gradient(160deg, #1c1710, #0b0e14 55%, #211a0d) padding-box,
linear-gradient(120deg, #d4af37, #ffe9a8, #b8912b, #fff2c9, #d4af37) border-box;
background-size: 100% 100%, 260% 100%;
cursor: not-allowed;
animation: btn-gold-locked-border-shine 6s ease-in-out infinite, btn-gold-locked-halo 3s ease-in-out infinite;
box-shadow: 0 8px 22px rgba(0, 0, 0, .45), 0 0 0 1px rgba(212, 175, 55, .08) inset;
}
.btn-gold-locked-lock {
position: relative;
z-index: 2;
display: inline-block;
font-size: 1.05em;
filter: drop-shadow(0 0 6px rgba(255, 214, 102, .85));
animation: btn-gold-locked-pulse 2.2s ease-in-out infinite;
}
.btn-gold-locked-text {
position: relative;
z-index: 2;
background: linear-gradient(100deg, #ffe9a8 0%, #ffffff 24%, #d4af37 42%, #ffe9a8 60%, #ffffff 78%, #d4af37 100%);
background-size: 260% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .6));
animation: btn-silver-text-shine 3.6s linear infinite; /* bestehendes Keyframe wiederverwendet */
}
.btn-gold-locked::after {
content: "";
position: absolute;
inset: 0;
z-index: 1;
border-radius: inherit;
background: linear-gradient(115deg, transparent 32%, rgba(255, 233, 168, .5) 49%, transparent 66%);
background-size: 240% 100%;
mix-blend-mode: overlay;
animation: nav-cta-shimmer 4s linear infinite; /* bestehendes Keyframe wiederverwendet */
pointer-events: none;
}
@keyframes btn-gold-locked-border-shine {
0%, 100% { background-position: 0 0, 0% 50%; }
50% { background-position: 0 0, 100% 50%; }
}
@keyframes btn-gold-locked-pulse {
0%, 100% { opacity: .7; transform: scale(.92) rotate(-4deg); }
50% { opacity: 1; transform: scale(1.12) rotate(4deg); }
}
@keyframes btn-gold-locked-halo {
0%, 100% { box-shadow: 0 8px 22px rgba(0, 0, 0, .45), 0 0 16px rgba(212, 175, 55, .22); }
50% { box-shadow: 0 8px 22px rgba(0, 0, 0, .45), 0 0 32px rgba(255, 214, 102, .45); }
}
@media (prefers-reduced-motion: reduce) {
.btn-gold-locked, .btn-gold-locked::after,
.btn-gold-locked-text, .btn-gold-locked-lock {
animation: none;
}
}
/* ===================================================================== /* =====================================================================
.btn-holo — zweiter "Mega"-Button-Stil, bewusst MECHANISCH anders als .btn-holo — zweiter "Mega"-Button-Stil, bewusst MECHANISCH anders als
.btn-legendary/.btn-silver/.btn-tiktok (die alle nach demselben Muster .btn-legendary/.btn-silver/.btn-tiktok (die alle nach demselben Muster
@@ -1308,14 +1444,21 @@ header.site-header {
cursor: pointer; cursor: pointer;
} }
/* Schwelle von ursprünglich 880px auf 1480px angehoben: nach mehreren /* Bugfix 20.08.2026 (Nutzer-Report per Screenshot: "DogiCrew"-Knopf/Badge
Vergrößerungsrunden (Logo/Nav-Schrift/Sprachschalter/Bewerben-Button, rechts abgeschnitten): die Schwelle war schon zweimal angehoben (880 ->
mehr Abstand zwischen den Nav-Punkten, größerer eigener Nav-Container, 1480px, dazu der .nav-bar-Deckel 1400 -> 1440px), reichte aber seither
Polarlicht-Padding je Kategorie) braucht die Desktop-Nav deutlich mehr NICHT mehr aus — seit den neu dazugekommenen Nav-Punkten "DogiCrew" +
Platz als früher. Live bei mehreren Breiten nachgeprüft (siehe Commits "BALD"-Badge braucht die aufgeklappte Desktop-Nav real ca. 1491px
vom 02.08.2026) — Schwelle hier bewusst großzügig, damit nie wieder Breite (per Playwright über mehrere Breiten nachgemessen), der Deckel
Elemente sichtbar aus der Leiste verschwinden. */ blieb aber bei 1440px hart begrenzt. Ergebnis: bei JEDER Fensterbreite
@media (max-width: 1480px) { ab ca. 1500px (nicht nur in einer schmalen Übergangszone) lief die Nav
permanent ca. 50px über den Rand hinaus. Diesmal mit echtem Sicherheits-
abstand behoben statt wieder nur knapp: Schwelle auf 1650px, Deckel auf
1560px — bei exakt 1650px ergibt min(1560px, 94vw) ≈ 1551px, spürbar
mehr als die gemessenen ~1491px nötigen Breite. Bei jeder Fensterbreite
NACH der Schwelle reicht 94vw automatisch weiter aus (nur ganz nah an
der Schwelle war die Lücke kritisch). */
@media (max-width: 1650px) {
.nav-toggle { display: block; } .nav-toggle { display: block; }
.nav-links { .nav-links {
position: fixed; position: fixed;
@@ -1336,6 +1479,22 @@ header.site-header {
.nav-dropdown::before { display: none; } .nav-dropdown::before { display: none; }
.nav-group.open .nav-dropdown { display: flex; } .nav-group.open .nav-dropdown { display: flex; }
.nav-cta { align-self: flex-start; margin: 1rem 0 .6rem; } .nav-cta { align-self: flex-start; margin: 1rem 0 .6rem; }
/* Bugfix 20.08.2026 (Nutzer-Report: "auf dem Handy passt nicht immer alles").
Per echter Mobil-Emulation (iPhone 15 Pro + Pixel 8, Screenshots) nachgestellt:
Die schwebenden Eck-Knöpfe (Musik unten rechts, Vorschau-Umschalter unten links
für Dogi/VanVan — siehe .music-widget/.vorschau-widget weiter unten) sind
absichtlich IMMER sichtbar (position:fixed, direkte <body>-Kinder, eigener
Stacking-Kontext, unabhängig vom Menü). Bei aufgeklapptem Mobil-Menü liegen sie
dadurch ÜBER den untersten Menüpunkten (Sprachschalter + "Bewerben"-Knopf) und
verdecken/blockieren sie sichtbar. :has() statt einer weiteren main.js-Änderung,
weil beide Widgets keinen gemeinsamen Elternknoten mit .nav-links haben (beide
hängen direkt unter <body>, .nav-links steckt im Header) — reiner CSS-Fix bleibt
an der Stelle, an der auch die restliche Menü-Overlay-Logik dieses Blocks steht. */
body:has(.nav-links.open) .music-widget,
body:has(.nav-links.open) .vorschau-widget {
display: none;
}
} }
/* ---------- Buttons ---------- /* ---------- Buttons ----------
@@ -1407,6 +1566,12 @@ header.site-header {
.btn-outline:hover { border-color: var(--accent); color: var(--accent); } .btn-outline:hover { border-color: var(--accent); color: var(--accent); }
.btn-row { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 1.5rem; } .btn-row { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 1.5rem; }
/* Kurzes "✓ Link kopiert"-Feedback für dogiTeilen()-Knöpfe (Zwischenablage-
Fallback ohne Web-Share-API) -- 21.08.2026. Gleiche Erfolgsfarbe wie
.vw-copy-btn.kopiert in der Verwaltung, für ein konsistentes Gefühl
zwischen öffentlicher Seite und Admin-Bereich. */
.dogi-teilen-kopiert { background: rgba(52,211,153,.15) !important; border-color: rgba(52,211,153,.5) !important; color: #5eeaa7 !important; }
/* ---------- Hero ---------- */ /* ---------- Hero ---------- */
.hero { .hero {
position: relative; position: relative;
@@ -1444,6 +1609,185 @@ header.site-header {
} }
@media (max-width: 760px) { .hero-grid { grid-template-columns: 1fr; } } @media (max-width: 760px) { .hero-grid { grid-template-columns: 1fr; } }
/* "Event des Jahres" (20.08.2026) — volle Breite wie .hero-grid darüber (eigener Block,
kein Grid-Kind), zeigt bewusst IMMER genau zwei Events nebeneinander. Gleiche
Karten-/Tag-Optik wie die 3 Welt-Kacheln, damit es wie ein natürlicher Abschluss des
Grids wirkt statt wie ein fremdes Element. */
.jahres-events {
margin-top: 1.2rem;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.6rem;
box-shadow: var(--shadow);
}
.jahres-events-head { margin-bottom: 1.2rem; }
.jahres-events-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.2rem;
}
@media (max-width: 760px) { .jahres-events-grid { grid-template-columns: 1fr; } }
/* Nutzer-Wunsch 20.08.2026: "die kacheln sollen auch bissl spezieller sein" —
goldener Trophäen-Akzent statt der neutralen Standard-Kartenoptik (eigener,
gedeckter Farbton, kein grelles Gold — augenschonend), plus ein dezenter
wandernder Lichtschimmer oben (".jahres-event-shine"), der die Kachel als
etwas Besonderes markiert statt als normale Karte. Jetzt ein <button>
(öffnet ein Detail-Fenster) statt eines <a> (navigierte sofort weg) —
deshalb hier zusätzlich ein vollständiger Button-Reset. */
/* [hidden] explizit VOR der Klassenregel + als eigene Regel danach: ein
simples "display:block" in .jahres-event-card hat dieselbe Spezifität
wie die eingebaute [hidden]-UA-Regel des Browsers und würde sie sonst im
Zweifel überschreiben (identischer Bug wie schon bei .jahres-event-bild
weiter unten, hier per Playwright-Screenshot erneut gefunden — eine
"leere" Kachel blieb trotz hidden-Attribut sichtbar). */
.jahres-event-card[hidden] { display: none; }
.jahres-event-card {
display: block;
width: 100%;
text-align: left;
font: inherit;
color: inherit;
cursor: pointer;
position: relative;
overflow: hidden;
isolation: isolate;
padding: 1.2rem 1.4rem;
border-radius: calc(var(--radius) - 4px);
border: 1px solid rgba(212, 175, 55, .35);
background:
radial-gradient(circle at 15% 0%, rgba(212, 175, 55, .08), transparent 55%),
rgba(255, 255, 255, .02);
transition: border-color .2s ease, transform .2s ease, box-shadow .2s ease;
}
.jahres-event-card:hover, .jahres-event-card:focus-visible {
border-color: rgba(212, 175, 55, .7);
transform: translateY(-3px);
box-shadow: 0 10px 26px rgba(0, 0, 0, .35), 0 0 22px rgba(212, 175, 55, .18);
}
.jahres-event-shine {
content: "";
position: absolute;
top: 0; left: 6%; right: 6%;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 214, 102, .9), transparent);
animation: jahres-event-goldglanz 4.5s ease-in-out infinite;
pointer-events: none;
}
@keyframes jahres-event-goldglanz { 0%, 100% { opacity: .3; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .jahres-event-shine { animation: none; opacity: .6; } }
.jahres-event-datum {
display: block;
color: #e8c465;
font-weight: 700;
font-size: .82rem;
letter-spacing: .02em;
margin-bottom: .3rem;
}
.jahres-event-card h3 { margin: 0 0 .4rem; font-size: 1.15rem; }
.jahres-event-card p { margin: 0 0 .6rem; color: var(--text-muted); font-size: .93rem; line-height: 1.55; }
.jahres-event-mehr { color: #e8c465; font-weight: 600; font-size: .88rem; }
/* Genau EIN aktives Event: zentrierte, etwas größere Einzelkachel statt
halbleerem Zwei-Spalten-Raster (Nutzer-Wunsch: "spezieller"). */
.jahres-events-grid-einzeln {
grid-template-columns: 1fr;
max-width: 480px;
margin: 0 auto;
}
.jahres-events-grid-einzeln .jahres-event-card { padding: 1.6rem 1.8rem; }
.jahres-events-grid-einzeln .jahres-event-card h3 { font-size: 1.3rem; }
/* Optionales Admin-Bild pro Event (20.08.2026, "Event des Jahres" jetzt in
der Verwaltung pflegbar) — bleibt per [hidden] unsichtbar, solange kein
Bild gesetzt ist, damit die textbasierten Standard-Karten unverändert
aussehen. Bugfix (per Playwright-Test gefunden): "display:block" allein
hätte dieselbe Spezifität wie die eingebaute [hidden]-Regel des Browsers
und würde sie im Zweifel überschreiben (leerer Bild-Rahmen wäre auch OHNE
gesetztes Bild sichtbar gewesen) — deshalb hier zusätzlich explizit
:not([hidden]) statt nur der Klasse allein. */
.jahres-event-bild[hidden] { display: none; }
.jahres-event-bild:not([hidden]) {
display: block;
width: 100%;
aspect-ratio: 16/9;
object-fit: cover;
border-radius: calc(var(--radius) - 4px);
margin-bottom: .9rem;
}
/* ---------- "Event des Jahres" — Detail-Fenster (20.08.2026) ----------
Nutzer-Wunsch: "wen ich drauf drücke dass dein ein kleines fenster
aufgeht wo das bild bissl größer ist mit einem größeren text und
beschreibung, und mit einem link ... bereite das richtig geil krank
vor." Augenschonend gehalten (siehe Projektregel): kein grelles Weiß,
sanftes Ein-/Ausblenden statt hartem Umschalten, respektiert
prefers-reduced-motion. */
.jahres-event-modal-overlay {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
padding: 4vh 5vw;
background: rgba(5, 7, 11, .78);
backdrop-filter: blur(6px);
opacity: 0;
transition: opacity .22s ease;
}
.jahres-event-modal-overlay.offen { opacity: 1; }
.jahres-event-modal-overlay[hidden] { display: none; }
body.modal-offen { overflow: hidden; }
.jahres-event-modal {
position: relative;
width: min(640px, 100%);
max-height: 90vh;
overflow-y: auto;
background: var(--card-bg);
border: 1px solid rgba(212, 175, 55, .4);
border-radius: var(--radius);
box-shadow: 0 24px 60px rgba(0, 0, 0, .5), 0 0 40px rgba(212, 175, 55, .12);
transform: scale(.94) translateY(10px);
transition: transform .25s ease;
}
.jahres-event-modal-overlay.offen .jahres-event-modal { transform: scale(1) translateY(0); }
@media (prefers-reduced-motion: reduce) {
.jahres-event-modal-overlay, .jahres-event-modal { transition: none; }
}
.jahres-event-modal-close {
position: absolute;
top: .9rem; right: .9rem;
z-index: 2;
width: 38px; height: 38px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, .25);
background: rgba(5, 7, 11, .7);
color: var(--text);
font-size: 1.1rem;
cursor: pointer;
transition: border-color .15s ease, transform .15s ease;
}
.jahres-event-modal-close:hover { border-color: rgba(212, 175, 55, .7); transform: scale(1.06); }
.jahres-event-modal-bild {
display: block;
width: 100%;
max-height: 46vh;
object-fit: cover;
border-radius: var(--radius) var(--radius) 0 0;
}
.jahres-event-modal-bild[hidden] { display: none; }
.jahres-event-modal-body { padding: 1.8rem 2rem 2.2rem; }
.jahres-event-modal-datum {
display: block;
color: #e8c465;
font-weight: 700;
font-size: .92rem;
letter-spacing: .03em;
margin-bottom: .5rem;
}
.jahres-event-modal-body h2 { font-size: 1.7rem; margin: 0 0 .8rem; }
.jahres-event-modal-body p { font-size: 1.08rem; line-height: 1.7; color: var(--text-muted); margin: 0 0 1.4rem; }
.jahres-event-modal-link[hidden] { display: none; }
/* ---------- Cards ---------- */ /* ---------- Cards ---------- */
.card { .card {
background: var(--card-bg); background: var(--card-bg);
@@ -1645,6 +1989,50 @@ header.site-header {
box-shadow: 0 0 60px 6px rgba(143,217,234,.18); box-shadow: 0 0 60px 6px rgba(143,217,234,.18);
opacity: .85; opacity: .85;
} }
/* "Diese Seite wächst noch"-Hinweis (20.08.2026, Nutzer-Wunsch) — bewusst
einladend statt wie eine Fehler-/Baustellen-Meldung, im selben Babyblau
wie der Rest des Wachstums-Baum-Themas dieser Seite (.zeit-baum-kappe/
-wurzel). Sitzt VOR dem Baum, damit Besucher sofort verstehen: hier
entsteht noch etwas, ohne dass es wie ein Fehler wirkt. */
.zeit-baustelle {
display: flex;
align-items: flex-start;
gap: 1rem;
max-width: 640px;
margin: 0 auto 2.6rem;
padding: 1.1rem 1.4rem;
border-radius: var(--radius);
background: rgba(143, 217, 234, .06);
border: 1.5px solid rgba(143, 217, 234, .35);
box-shadow: 0 0 24px rgba(143, 217, 234, .12);
}
.zeit-baustelle-icon {
flex-shrink: 0;
font-size: 1.8rem;
line-height: 1;
filter: drop-shadow(0 0 8px rgba(143, 217, 234, .55));
animation: zeit-baustelle-wiegen 3.5s ease-in-out infinite;
}
.zeit-baustelle strong {
display: block;
font-size: 1.05rem;
margin-bottom: .3rem;
color: var(--text);
}
.zeit-baustelle p {
margin: 0;
font-size: .92rem;
color: var(--text-muted);
line-height: 1.55;
}
@keyframes zeit-baustelle-wiegen {
0%, 100% { transform: rotate(-5deg); }
50% { transform: rotate(5deg); }
}
@media (prefers-reduced-motion: reduce) {
.zeit-baustelle-icon { animation: none; }
}
.zeit-baum-kappe { .zeit-baum-kappe {
position: relative; position: relative;
display: flex; display: flex;
@@ -1754,6 +2142,16 @@ header.site-header {
.zeit-ast.zeit-final .zeit-karte { width: calc(100% - 4rem); max-width: 640px; text-align: center; } .zeit-ast.zeit-final .zeit-karte { width: calc(100% - 4rem); max-width: 640px; text-align: center; }
.zeit-ast.zeit-final { justify-content: center !important; } .zeit-ast.zeit-final { justify-content: center !important; }
.zeit-ast.zeit-final .zeit-karte::before { display: none; } .zeit-ast.zeit-final .zeit-karte::before { display: none; }
/* Bugfix 20.08.2026 (Nutzer-Report: "wieso dieser Punkt da in der Mitte"):
.zeit-node ist bei allen normalen Karten position:absolute; left:50%;
top:50% relativ zu .zeit-ast — das sitzt korrekt in der Baum-Mitte,
WEIL die Karte dort nur die halbe Breite einnimmt. Die Finale-Karte
(.zeit-final) wird aber auf fast volle Breite gestreckt und zentriert
(siehe .zeit-karte-Regel oben) — der Knoten landete dadurch mitten im
Text statt am Rand. Die Verbindungslinie (::before, direkt drüber)
wurde dafür schon 2025 ausgeblendet, der Knoten selbst wurde dabei
übersehen. */
.zeit-ast.zeit-final .zeit-node { display: none; }
.zeit-zitat { .zeit-zitat {
font-weight: 700; font-weight: 700;
color: var(--text); color: var(--text);
@@ -1968,6 +2366,51 @@ footer.site-footer {
(z.B. dem DogFather-Manager-Artwork) nicht flach wirken. */ (z.B. dem DogFather-Manager-Artwork) nicht flach wirken. */
filter: saturate(1.26) contrast(1.15) brightness(1.03); filter: saturate(1.26) contrast(1.15) brightness(1.03);
} }
/* Sehr hohe Hochkant-Bilder (z.B. streamer-mascot.jpg mit 900x2812 px) würden
mit aspect-ratio:auto + height:auto in voller natürlicher Höhe rendern und die
danebenstehende Textspalte komplett erschlagen (Nutzer-Report 20.08.2026:
"der soll bissl kleiner sein und nicht so riesig"). Ein zu knapper max-height-
Deckel (erster Versuch: 460px) machte es dann aber zu klein und ließ eine
sichtbare Lücke unter dem Bild, weil die Textspalte daneben normalerweise
deutlich mehr Platz braucht (Nutzer-Report 21.08.2026: "die größe vom bild
vom husky soll dem text rechts nebendran angepasst werden").
Ausprobiert und wieder verworfen: die Höhe per CSS Grid align-items:stretch +
height:100% exakt an die Textspalte zu koppeln. Funktioniert bei Grid NICHT
robust, weil ein 'auto' sizing Track seine Höhe am MAX-CONTENT-Beitrag beider
Felder ausrichtet -- bei einem derart extremen Hochkantbild (Seitenverhältnis
~1:3.1) zieht das Bild die ganze Zeile (und damit auch die Textspalte) auf
seine eigene, riesige natürliche Höhe hoch, egal was man an min-height
probiert (min-height wirkt nur auf den Mindest-, nicht den Wachstums-Beitrag).
Robuster Kompromiss stattdessen: ein großzügiger fester max-height-Wert
(720px statt 460px), der sich an der TYPISCHEN Höhe der Textspalte in diesem
Abschnitt orientiert (auf normalen Desktop-Breiten ~715-850px hoch) -- das
Bild wirkt dadurch angemessen groß und auf Augenhöhe mit dem Text, ohne die
fragile/rückkoppelnde Grid-Stretch-Mechanik. width:fit-content lässt den
Rahmen dabei eng am Bild sitzen, statt breite gestreifte Hintergrund-Balken
danebenzulassen.
WICHTIG: muss NACH ".collage-photo img" stehen — beide Regeln haben dieselbe
Spezifität (eine Klasse + ein Element), die spätere gewinnt. Stünde dieser
Block davor, würde width:100%/height:100% oben die Deckelung wieder aufheben. */
.collage-photo-kompakt {
aspect-ratio: auto;
width: fit-content;
max-width: 100%;
margin: 0 auto;
}
.collage-photo-kompakt img {
width: auto;
height: auto;
max-height: 720px;
max-width: 100%;
display: block;
object-fit: contain;
}
@media (max-width: 780px) {
/* Auf Mobilgeräten stehen die Grid-Spalten untereinander -- dort ist die
Textspalte durch die volle Breite viel kürzer, ein 720px hohes Bild wäre
unangemessen groß für einen schmalen Handy-Screen. Deckel daher kleiner. */
.collage-photo-kompakt img { max-height: 420px; }
}
/* Cineastischer "Poster-Grade" für JEDES Präsentationsfoto auf der Seite: /* Cineastischer "Poster-Grade" für JEDES Präsentationsfoto auf der Seite:
zwei Farbschein-Blooms in der jeweiligen Rahmenfarbe (oben-links / zwei Farbschein-Blooms in der jeweiligen Rahmenfarbe (oben-links /
unten-rechts, wie ein Studio-Rimlight — nutzt dieselbe --frame-Variable unten-rechts, wie ein Studio-Rimlight — nutzt dieselbe --frame-Variable
@@ -2146,7 +2589,7 @@ footer.site-footer {
Cover, die jeden Rand auffüllt, den "contain" freilässt — dadurch Cover, die jeden Rand auffüllt, den "contain" freilässt — dadurch
wirkt es trotzdem randlos, obwohl die scharfen Pixel garantiert wirkt es trotzdem randlos, obwohl die scharfen Pixel garantiert
nie beschnitten werden. */ nie beschnitten werden. */
background-image: url("../img/universe-trio.jpg"), url("../img/universe-trio-blur.jpg"); background-image: url("../img/universe-trio.jpg?v=20260820h"), url("../img/universe-trio-blur.jpg?v=20260820h");
background-size: contain, cover; background-size: contain, cover;
background-position: center, center; background-position: center, center;
background-repeat: no-repeat, no-repeat; background-repeat: no-repeat, no-repeat;
@@ -3188,3 +3631,342 @@ body.start-seite .music-toggle:focus-visible {
margin: 1.5rem 0; margin: 1.5rem 0;
} }
.todo-note strong { color: var(--accent-2); } .todo-note strong { color: var(--accent-2); }
/* ===================================================================
Streamplan — "Live-Radar" (20.08.2026, Nutzer-Wunsch: "soll viel
geiler und spezieller sein, überrasch mich"). Ein 24-Stunden-Ziffernblatt
als SVG: der feste 20-Uhr-Termin leuchtet dauerhaft als Fixpunkt, ein
"Jetzt"-Zeiger bewegt sich in Echtzeit mit der tatsächlichen Uhrzeit,
ein sanft rotierender Lichtschein sorgt für Atmosphäre. Schaltet live
auf Rot/Puls um, sobald DogFather laut /live-status wirklich sendet —
sonst zählt ein echter Sekunden-Countdown zum nächsten 20-Uhr-Termin
runter. Bewusst augenschonend: gedeckte Farben, langsame, sanfte
Animation, kompletter Stillstand bei prefers-reduced-motion. ================================================================= */
/* Reihe für 1-2 Uhren nebeneinander (20.08.2026: zweite Uhr für ein
"spezielles Live-Event", siehe assets/js/streamplan.js) — bricht auf
schmalen Bildschirmen automatisch untereinander um. */
.stream-radar-reihe {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: flex-start;
gap: 2.4rem;
margin-top: 1.4rem;
}
/* .stream-radar-wrap setzt selbst "display", das schlägt bei gleicher
Spezifität die eingebaute [hidden]-Regel des Browsers (identisches,
inzwischen mehrfach gefundenes Muster auf dieser Seite) — deshalb hier
explizit VOR der Klassenregel. */
.stream-radar-wrap[hidden] { display: none; }
.stream-radar-wrap { display: flex; flex-direction: column; align-items: center; margin: 0 auto; }
.stream-radar {
position: relative;
width: min(300px, 70vw);
aspect-ratio: 1;
margin: 0 auto;
}
.stream-radar-sweep {
position: absolute;
inset: -6%;
border-radius: 50%;
background: conic-gradient(from 0deg,
transparent 0deg, rgba(143, 217, 234, .28) 40deg, transparent 90deg,
transparent 200deg, rgba(232, 196, 101, .22) 250deg, transparent 300deg,
transparent 320deg, rgba(139, 92, 246, .24) 350deg, transparent 360deg);
animation: stream-radar-rotate 16s linear infinite;
pointer-events: none;
}
/* Farbvariante fürs spezielle Live-Event (Nutzer-Wunsch "bissl Farbe") —
Magenta/Violett/Gold statt Cyan/Violett/Gold, damit die zweite Uhr auch
auf den ersten Blick als "etwas Besonderes" erkennbar ist. */
.stream-radar.event-spezial .stream-radar-sweep {
background: conic-gradient(from 0deg,
transparent 0deg, rgba(255, 92, 180, .26) 40deg, transparent 90deg,
transparent 200deg, rgba(232, 196, 101, .22) 250deg, transparent 300deg,
transparent 320deg, rgba(139, 92, 246, .28) 350deg, transparent 360deg);
}
@keyframes stream-radar-rotate { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .stream-radar-sweep { animation: none; } }
.stream-radar-dial { position: relative; width: 100%; height: 100%; display: block; }
/* Bunter Verlauf statt reinem Grau-Ton auf dem äußeren Ring (Nutzer-Wunsch
"bissl Farbe") — Farbverlauf steckt als <linearGradient> direkt im SVG
jeder Uhr (siehe streamplan.html), hier nur die Basiswerte/Deckkraft. */
.radar-ring-outer { fill: none; stroke-width: 2; opacity: .8; }
.radar-ring-inner { fill: rgba(255, 255, 255, .02); stroke: rgba(255, 255, 255, .07); stroke-width: 1; }
.radar-tick { stroke: rgba(255, 255, 255, .22); stroke-width: 1.5; }
.radar-tick-major { stroke: rgba(255, 255, 255, .5); stroke-width: 2.2; }
.radar-tick-label { fill: var(--text-muted); font-size: 11px; font-family: var(--font-body, inherit); }
.radar-fixmarker-dot {
fill: #e8c465;
filter: drop-shadow(0 0 6px rgba(232, 196, 101, .9)) drop-shadow(0 0 14px rgba(232, 196, 101, .5));
animation: stream-radar-fixpuls 3.2s ease-in-out infinite;
}
@keyframes stream-radar-fixpuls { 0%, 100% { opacity: .75; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .radar-fixmarker-dot { animation: none; opacity: .9; } }
.radar-now-hand {
stroke: var(--accent);
stroke-width: 2.5;
stroke-linecap: round;
filter: drop-shadow(0 0 4px var(--accent));
transition: x2 .6s ease, y2 .6s ease;
}
.radar-now-dot {
fill: var(--accent);
filter: drop-shadow(0 0 6px var(--accent));
transition: cx .6s ease, cy .6s ease;
}
.stream-radar.ist-live .radar-now-hand,
.stream-radar.ist-live .radar-now-dot { stroke: #ff5c72; fill: #ff5c72; filter: drop-shadow(0 0 8px rgba(255, 92, 114, .8)); }
.radar-center-dot { fill: var(--text); opacity: .8; }
.stream-radar-center {
position: absolute;
inset: 24%;
border-radius: 50%;
background: rgba(8, 10, 14, .82);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: .5rem;
gap: .25rem;
}
.stream-radar-dot {
width: 10px; height: 10px;
border-radius: 50%;
background: var(--text-muted);
margin-bottom: .2rem;
}
.stream-radar.ist-live .stream-radar-dot { background: #ff5c72; box-shadow: 0 0 10px rgba(255, 92, 114, .8); animation: stream-radar-live-puls 1.4s ease-in-out infinite; }
@keyframes stream-radar-live-puls { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.35); opacity: .75; } }
@media (prefers-reduced-motion: reduce) { .stream-radar.ist-live .stream-radar-dot { animation: none; } }
.stream-radar-status { font-size: 1rem; font-weight: 800; color: var(--text); line-height: 1.25; }
.stream-radar-countdown {
font-variant-numeric: tabular-nums;
font-size: 1.55rem;
font-weight: 900;
letter-spacing: .02em;
color: var(--accent);
}
.stream-radar.ist-live .stream-radar-countdown { color: #ff5c72; }
.stream-radar-sub { font-size: .74rem; color: var(--text-muted); }
/* Farbvariante fürs spezielle Live-Event: der Fixpunkt ist hier nicht der
tägliche 20-Uhr-Termin, sondern der eigene Zieltermin -- Magenta statt
Gold, damit man beide Uhren auf einen Blick unterscheidet. */
.stream-radar.event-spezial .radar-fixmarker-dot {
fill: #ff5cb8;
filter: drop-shadow(0 0 6px rgba(255, 92, 184, .9)) drop-shadow(0 0 14px rgba(255, 92, 184, .5));
}
.stream-radar.event-spezial:not(.ist-live) .radar-now-hand,
.stream-radar.event-spezial:not(.ist-live) .radar-now-dot { stroke: #c9a6ff; fill: #c9a6ff; filter: drop-shadow(0 0 5px rgba(201, 166, 255, .8)); }
.stream-radar.event-spezial:not(.ist-live) .stream-radar-countdown { color: #ff5cb8; }
.stream-radar-titel { font-size: .95rem; font-weight: 800; color: #ff9ed4; margin-bottom: .3rem; }
/* .btn setzt selbst "display", das schlägt bei gleicher Spezifität die
eingebaute [hidden]-Regel des Browsers (identisches Muster wie bei
.jahres-event-card weiter oben) — deshalb hier explizit. */
.stream-radar-watch-btn[hidden] { display: none; }
.stream-radar-watch-btn { margin-top: 1.1rem; }
.stream-radar-hinweis { margin-top: 1rem; opacity: .8; }
/* ===================================================================
"Stimmen" — eigene Nachricht einreichen (20.08.2026, Nutzer-Wunsch:
"eine richtig geile kachel ... das wird mega persoenlich zu den fans,
bitte wirklich krass geil speziell"). Warmer Babyblau/Rosé/Lila-
Farbverlauf statt der neutralen Standardkarte, wandernder Lichtschein
am Rand (dieselbe Zwei-Ebenen-Technik wie schon an mehreren Stellen
dieses Projekts, siehe .supp-card::before) — bewusst augenschonend
(gedeckte Farben, sanfte Bewegung, respektiert reduzierte Bewegung). ================================================================= */
/* Nutzer-Wunsch 20.08.2026 (2. Runde): "die kachel soll noch spezieller und
geiler aussehen". Deutlich aufgewertet gegenüber der ersten Fassung:
wandernder Farbverlauf-Rahmen rundherum (Zwei-Ebenen-Technik mit
background-position statt eines rotierenden Rings — siehe dokumentierte
Projekt-Lektion: rotierende Masken verzerren auf eckigen Flächen),
schwebende Herz-Partikel im Hintergrund, Medaillon statt nacktem Emoji,
Farbverlauf-Überschrift und Eingabefelder, die beim Tippen aufleuchten.
Bleibt trotzdem augenschonend: gedeckte Töne, langsame Bewegungen,
vollständiger Stillstand bei prefers-reduced-motion. */
.stimmen-einreichen-kachel {
position: relative;
isolation: isolate;
overflow: hidden;
max-width: 580px;
margin: 0 auto;
padding: 2.4rem 2.4rem 2.6rem;
border-radius: calc(var(--radius) + 4px);
background:
radial-gradient(ellipse at 12% 0%, rgba(255, 154, 208, .13), transparent 58%),
radial-gradient(ellipse at 88% 100%, rgba(143, 217, 234, .13), transparent 58%),
linear-gradient(180deg, rgba(24, 34, 51, .96), rgba(14, 20, 32, .98));
box-shadow: 0 30px 70px rgba(0, 0, 0, .45), 0 0 50px rgba(143, 217, 234, .06);
}
/* Farbverlauf-Rahmen: zwei Ebenen, nur die Hintergrundposition wandert. */
.stimmen-einreichen-kachel::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: inherit;
padding: 1.5px;
background: linear-gradient(115deg, rgba(255,154,208,.75), rgba(143,217,234,.75), rgba(199,204,209,.5), rgba(255,154,208,.75));
background-size: 300% 100%;
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask-composite: exclude;
animation: stimmen-rahmen-wandert 9s linear infinite;
}
@keyframes stimmen-rahmen-wandert { to { background-position: 300% 0; } }
/* Schwebende Herz-Partikel — reines CSS, keine zusätzlichen Elemente nötig. */
.stimmen-einreichen-glanz {
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background-image:
radial-gradient(circle, rgba(255,154,208,.5) 2px, transparent 3px),
radial-gradient(circle, rgba(143,217,234,.45) 2px, transparent 3px),
radial-gradient(circle, rgba(255,154,208,.35) 1.5px, transparent 3px);
background-size: 190px 190px, 260px 260px, 140px 140px;
background-position: 12% 18%, 78% 62%, 44% 88%;
animation: stimmen-partikel-treiben 16s ease-in-out infinite alternate;
}
@keyframes stimmen-partikel-treiben {
from { background-position: 12% 18%, 78% 62%, 44% 88%; opacity: .55; }
to { background-position: 18% 26%, 71% 54%, 50% 79%; opacity: .95; }
}
@media (prefers-reduced-motion: reduce) {
.stimmen-einreichen-kachel::before, .stimmen-einreichen-glanz { animation: none; }
}
.stimmen-einreichen-kopf { text-align: center; margin-bottom: 1.8rem; }
/* Medaillon statt nacktem Emoji: Kreis mit Farbschein und sanftem Puls. */
.stimmen-einreichen-herz {
display: inline-flex;
align-items: center;
justify-content: center;
width: 74px; height: 74px;
border-radius: 50%;
font-size: 2.1rem;
background: radial-gradient(circle at 35% 30%, rgba(255,154,208,.28), rgba(143,217,234,.12) 70%);
border: 1.5px solid rgba(255, 154, 208, .45);
box-shadow: 0 0 26px rgba(255, 154, 208, .3), inset 0 0 22px rgba(255, 154, 208, .12);
animation: stimmen-herz-puls 3.4s ease-in-out infinite;
}
@keyframes stimmen-herz-puls {
0%, 100% { transform: scale(1); box-shadow: 0 0 22px rgba(255,154,208,.22), inset 0 0 20px rgba(255,154,208,.1); }
50% { transform: scale(1.06); box-shadow: 0 0 40px rgba(255,154,208,.45), inset 0 0 26px rgba(255,154,208,.18); }
}
@media (prefers-reduced-motion: reduce) { .stimmen-einreichen-herz { animation: none; } }
.stimmen-einreichen-kopf h2 {
margin: .9rem 0 .6rem;
background: linear-gradient(100deg, #ffd0e8, #ffffff 40%, #a8e4f2);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.stimmen-einreichen-kopf p { color: var(--text-muted); }
#stimmen-formular .field { margin-bottom: 1.1rem; }
/* Eingabefelder leuchten beim Tippen in der Kachel-Farbwelt auf. */
#stimmen-formular .field input,
#stimmen-formular .field textarea {
transition: border-color .2s ease, box-shadow .2s ease, background .2s ease;
}
#stimmen-formular .field input:focus,
#stimmen-formular .field textarea:focus {
outline: none;
border-color: rgba(255, 154, 208, .75);
box-shadow: 0 0 0 3px rgba(255, 154, 208, .14), 0 0 22px rgba(255, 154, 208, .18);
background: rgba(255, 255, 255, .06);
}
.stimmen-erfolg { color: #baf7d6; font-size: .9rem; margin-top: 1rem; display: none; text-align: center; }
.stimmen-erfolg.zeige { display: block; }
.stimmen-fehler { color: #ff9a9a; font-size: .88rem; margin-top: 1rem; display: none; text-align: center; }
.stimmen-fehler.zeige { display: block; }
/* Freigegebene Stimmen (20.08.2026, 2. Runde: "und auch die die danach
kommen wenn ich sie eingebe die sollen viel geiler sein"): echte
Präsentationskarten statt schlichter Kästen — Farbschein-Bloom in der
Ecke, großes Anführungszeichen als Wasserzeichen, farbige Akzentlinie
oben, Avatar-Medaillon mit dem Anfangsbuchstaben, TikTok-Handle als
eigener Chip. Beim Überfahren hebt sich die Karte leicht an. */
.stimmen-karte {
position: relative;
isolation: isolate;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 2rem 1.8rem 1.6rem;
border-radius: calc(var(--radius) - 2px);
border: 1px solid rgba(143, 217, 234, .22);
background:
radial-gradient(ellipse at 100% 0%, rgba(255, 154, 208, .1), transparent 55%),
linear-gradient(180deg, rgba(24, 34, 51, .9), rgba(15, 21, 33, .95));
box-shadow: 0 16px 40px rgba(0, 0, 0, .32);
transition: transform .25s ease, border-color .25s ease, box-shadow .25s ease;
}
.stimmen-karte:hover {
transform: translateY(-4px);
border-color: rgba(255, 154, 208, .5);
box-shadow: 0 22px 52px rgba(0, 0, 0, .42), 0 0 30px rgba(255, 154, 208, .14);
}
/* Farbige Akzentlinie oben — greift die Farbwelt der Einreich-Kachel auf. */
.stimmen-karte::after {
content: "";
position: absolute;
top: 0; left: 0; right: 0;
height: 2.5px;
background: linear-gradient(90deg, rgba(255,154,208,.9), rgba(143,217,234,.9));
}
/* Großes Anführungszeichen als dezentes Wasserzeichen im Hintergrund. */
.stimmen-karte::before {
content: "„";
position: absolute;
top: -.9rem; right: .6rem;
z-index: -1;
font-size: 8rem;
font-family: Georgia, serif;
color: rgba(143, 217, 234, .09);
line-height: 1;
pointer-events: none;
}
.stimmen-karte-text {
position: relative;
z-index: 1;
flex: 1;
margin: 0 0 1.3rem;
line-height: 1.75;
font-size: 1.02rem;
color: var(--text);
}
.stimmen-karte-fuss {
display: flex;
align-items: center;
gap: .7rem;
padding-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, .07);
}
/* Avatar-Medaillon mit dem Anfangsbuchstaben (wird in stimmen.js gefüllt). */
.stimmen-karte-avatar {
display: flex;
align-items: center;
justify-content: center;
width: 40px; height: 40px;
flex-shrink: 0;
border-radius: 50%;
font-weight: 800;
font-size: 1rem;
color: #0d1420;
background: linear-gradient(135deg, #ffd0e8, #a8e4f2);
box-shadow: 0 0 16px rgba(255, 154, 208, .3);
}
.stimmen-karte-namen { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.stimmen-karte-name { font-weight: 700; color: var(--text); font-size: .95rem; }
.stimmen-karte-tiktok {
align-self: flex-start;
font-size: .76rem;
font-weight: 600;
color: #a8e4f2;
padding: .1rem .5rem;
border-radius: 999px;
background: rgba(143, 217, 234, .12);
border: 1px solid rgba(143, 217, 234, .28);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 KiB

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 KiB

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

After

Width:  |  Height:  |  Size: 239 KiB

+18
View File
@@ -0,0 +1,18 @@
/* =====================================================================
data-manager.js — Manager-Datenobjekt (21.08.2026)
DogFather (Filipe) selbst hat weiterhin seine eigene, feste Seite
(manager.html) — dieses Array ist für WEITERE Manager gedacht, die
künftig dazukommen könnten (siehe zeitreise.html, "Aufbau eines
eigenen Creator-Systems": "Eine langfristige Struktur für Creator,
Scouts und zukünftige Manager"). Noch niemand final geklärt, deshalb
bewusst leer — genau wie data-creator.js, bis echte Namen feststehen.
Einfach nach dem Muster in data-scouts.js Objekte ins Array einfügen,
sobald es so weit ist — manager.html liest automatisch aus.
Felder: siehe data-scouts.js (identisches Muster).
===================================================================== */
const MANAGERS = [];
if (typeof window !== "undefined") window.MANAGERS = MANAGERS;
+5 -5
View File
@@ -68,7 +68,7 @@ const MODIS = [
bioHtml: { bioHtml: {
de: ` de: `
<p>Hallo, ich bin <strong>Diene</strong> und auf TikTok findet ihr mich unter <strong>@_ditschi</strong>.</p> <p>Hallo, ich bin <strong>Diene</strong> und auf TikTok findet ihr mich unter <strong>@_ditschi</strong>.</p>
<p>Ich bin 37 Jahre alt, verheiratet und Mutter von zwei Kindern. Wer mich etwas besser kennt, weiß, dass ich manchmal ziemlich verpeilt und vergesslich sein kann. Und wer mich noch nicht kennt, wird es vermutlich ziemlich schnell bemerken. Genau das gehört aber auch einfach zu mir.</p> <p>Ich bin im August 1988 geboren, verheiratet und Mutter von zwei Kindern. Wer mich etwas besser kennt, weiß, dass ich manchmal ziemlich verpeilt und vergesslich sein kann. Und wer mich noch nicht kennt, wird es vermutlich ziemlich schnell bemerken. Genau das gehört aber auch einfach zu mir.</p>
<p>Auf TikTok bin ich ungefähr seit dem Jahr 2020 aktiv. Anfangs habe ich hauptsächlich Videos angesehen und gelegentlich eigene Videos für mich erstellt.</p> <p>Auf TikTok bin ich ungefähr seit dem Jahr 2020 aktiv. Anfangs habe ich hauptsächlich Videos angesehen und gelegentlich eigene Videos für mich erstellt.</p>
<p>Ende April 2026 war ich zum ersten Mal in einem Livestream von Dogi. Seit Anfang Mai bin ich regelmäßig – meistens sogar täglich – dabei.</p> <p>Ende April 2026 war ich zum ersten Mal in einem Livestream von Dogi. Seit Anfang Mai bin ich regelmäßig – meistens sogar täglich – dabei.</p>
<h3>Warum bin ich geblieben?</h3> <h3>Warum bin ich geblieben?</h3>
@@ -86,7 +86,7 @@ const MODIS = [
`, `,
"de-CH": ` "de-CH": `
<p>Hoi, ich bi <strong>Diene</strong> und uf TikTok findet ihr mich unter <strong>@_ditschi</strong>.</p> <p>Hoi, ich bi <strong>Diene</strong> und uf TikTok findet ihr mich unter <strong>@_ditschi</strong>.</p>
<p>Ich bi 37 Jahr alt, verhürotet und Mueter vo zwöi Chind. Wer mich es bitzeli besser kennt, weiss, dass ich mängisch ziemlich verpeilt und vergesslich sii cha. Und wer mich no nöd kennt, wird's wahrschindlich ziemlich schnäll merke. Aber genau das ghört eifach au zu mir dezue.</p> <p>Ich bi im Auguscht 1988 gebore, verhürotet und Mueter vo zwöi Chind. Wer mich es bitzeli besser kennt, weiss, dass ich mängisch ziemlich verpeilt und vergesslich sii cha. Und wer mich no nöd kennt, wird's wahrschindlich ziemlich schnäll merke. Aber genau das ghört eifach au zu mir dezue.</p>
<p>Uf TikTok bin ich öppe sit 2020 aktiv. Am Afang han ich hauptsächlich Videos aglueget und ab und zue mal es eigets Video für mich gmacht.</p> <p>Uf TikTok bin ich öppe sit 2020 aktiv. Am Afang han ich hauptsächlich Videos aglueget und ab und zue mal es eigets Video für mich gmacht.</p>
<p>Ändi April 2026 bin ich zum erste Mal i-me Livestream vom Dogi gsi. Sit Afang Mai bin ich regelmässig – meistens sogar täglich – debii.</p> <p>Ändi April 2026 bin ich zum erste Mal i-me Livestream vom Dogi gsi. Sit Afang Mai bin ich regelmässig – meistens sogar täglich – debii.</p>
<h3>Wieso bin ich blibe?</h3> <h3>Wieso bin ich blibe?</h3>
@@ -104,7 +104,7 @@ const MODIS = [
`, `,
en: ` en: `
<p>Hi, I'm <strong>Diene</strong>, and you can find me on TikTok as <strong>@_ditschi</strong>.</p> <p>Hi, I'm <strong>Diene</strong>, and you can find me on TikTok as <strong>@_ditschi</strong>.</p>
<p>I'm 37 years old, married, and a mother of two. Anyone who knows me a bit better knows that I can be pretty scatterbrained and forgetful sometimes. And anyone who doesn't know me yet will probably notice pretty quickly. But that's just part of who I am.</p> <p>I was born in August 1988, married, and a mother of two. Anyone who knows me a bit better knows that I can be pretty scatterbrained and forgetful sometimes. And anyone who doesn't know me yet will probably notice pretty quickly. But that's just part of who I am.</p>
<p>I've been active on TikTok since around 2020. At first I mostly watched videos and occasionally made a few of my own just for fun.</p> <p>I've been active on TikTok since around 2020. At first I mostly watched videos and occasionally made a few of my own just for fun.</p>
<p>At the end of April 2026, I joined one of Dogi's livestreams for the very first time. Since the beginning of May, I've been there regularly — usually even daily.</p> <p>At the end of April 2026, I joined one of Dogi's livestreams for the very first time. Since the beginning of May, I've been there regularly — usually even daily.</p>
<h3>Why did I stay?</h3> <h3>Why did I stay?</h3>
@@ -122,7 +122,7 @@ const MODIS = [
`, `,
fr: ` fr: `
<p>Salut, je suis <strong>Diene</strong> et vous me trouverez sur TikTok sous le pseudo <strong>@_ditschi</strong>.</p> <p>Salut, je suis <strong>Diene</strong> et vous me trouverez sur TikTok sous le pseudo <strong>@_ditschi</strong>.</p>
<p>J'ai 37 ans, je suis mariée et maman de deux enfants. Ceux qui me connaissent un peu mieux savent que je peux être plutôt tête en l'air et distraite. Et ceux qui ne me connaissent pas encore le remarqueront sans doute assez vite. Mais ça fait tout simplement partie de moi.</p> <p>Je suis née en août 1988, mariée et maman de deux enfants. Ceux qui me connaissent un peu mieux savent que je peux être plutôt tête en l'air et distraite. Et ceux qui ne me connaissent pas encore le remarqueront sans doute assez vite. Mais ça fait tout simplement partie de moi.</p>
<p>Je suis active sur TikTok depuis environ 2020. Au début, je regardais surtout des vidéos et je créais de temps en temps les miennes, juste pour moi.</p> <p>Je suis active sur TikTok depuis environ 2020. Au début, je regardais surtout des vidéos et je créais de temps en temps les miennes, juste pour moi.</p>
<p>Fin avril 2026, j'ai assisté pour la première fois à un livestream de Dogi. Depuis début mai, j'y suis régulièrement — le plus souvent même tous les jours.</p> <p>Fin avril 2026, j'ai assisté pour la première fois à un livestream de Dogi. Depuis début mai, j'y suis régulièrement — le plus souvent même tous les jours.</p>
<h3>Pourquoi suis-je restée ?</h3> <h3>Pourquoi suis-je restée ?</h3>
@@ -140,7 +140,7 @@ const MODIS = [
`, `,
pt: ` pt: `
<p>Olá, eu sou a <strong>Diene</strong> e podem encontrar-me no TikTok como <strong>@_ditschi</strong>.</p> <p>Olá, eu sou a <strong>Diene</strong> e podem encontrar-me no TikTok como <strong>@_ditschi</strong>.</p>
<p>Tenho 37 anos, sou casada e mãe de dois filhos. Quem me conhece um pouco melhor sabe que às vezes posso ser bastante distraída e esquecida. E quem ainda não me conhece, provavelmente vai reparar nisso bem depressa. Mas isso também faz simplesmente parte de mim.</p> <p>Nasci em agosto de 1988, sou casada e mãe de dois filhos. Quem me conhece um pouco melhor sabe que às vezes posso ser bastante distraída e esquecida. E quem ainda não me conhece, provavelmente vai reparar nisso bem depressa. Mas isso também faz simplesmente parte de mim.</p>
<p>Estou ativa no TikTok desde cerca de 2020. No início, via sobretudo vídeos e, de vez em quando, criava os meus próprios, só para mim.</p> <p>Estou ativa no TikTok desde cerca de 2020. No início, via sobretudo vídeos e, de vez em quando, criava os meus próprios, só para mim.</p>
<p>No final de abril de 2026, assisti pela primeira vez a uma livestream do Dogi. Desde o início de maio que estou presente regularmente — na maioria das vezes até diariamente.</p> <p>No final de abril de 2026, assisti pela primeira vez a uma livestream do Dogi. Desde o início de maio que estou presente regularmente — na maioria das vezes até diariamente.</p>
<h3>Porque é que fiquei?</h3> <h3>Porque é que fiquei?</h3>
+19 -8
View File
@@ -74,7 +74,7 @@ const SCOUTS = [
<p>Als Scout unterstütze ich das Team dabei, neue Talente zu entdecken, ihnen zur Seite zu stehen und sie auf ihrem persönlichen Weg zu begleiten. Dabei geht es mir nicht nur um Zahlen oder Erfolge, sondern vor allem um die Menschen, ihre Ziele und ihre Entwicklung.</p> <p>Als Scout unterstütze ich das Team dabei, neue Talente zu entdecken, ihnen zur Seite zu stehen und sie auf ihrem persönlichen Weg zu begleiten. Dabei geht es mir nicht nur um Zahlen oder Erfolge, sondern vor allem um die Menschen, ihre Ziele und ihre Entwicklung.</p>
<p>Ich freue mich auf alles, was noch vor mir liegt: neue Begegnungen, besondere Erinnerungen und viele weitere gemeinsame Momente.</p> <p>Ich freue mich auf alles, was noch vor mir liegt: neue Begegnungen, besondere Erinnerungen und viele weitere gemeinsame Momente.</p>
<p class="quote" style="margin-top:1rem;">Das bin ich – Patrick. Nicht perfekt, manchmal mit einem grimmigen Blick, aber immer mit Herz. ❤️</p> <p class="quote" style="margin-top:1rem;">Das bin ich – Patrick. Nicht perfekt, manchmal mit einem grimmigen Blick, aber immer mit Herz. ❤️</p>
<p style="text-align:right;"><strong>– euer Patrick ❤️</strong></p> <p style="text-align:right;"><strong>– euer Patrick ❤️</strong><br />Scout von Dogfather</p>
`, `,
"de-CH": ` "de-CH": `
<p>Im Oktober 2024 han ich schliesslich de nöchscht Schritt gwagt und bi zum erste Mal live gange. Sithär han ich zahlriichi Mönsche cha kenneleärne und e paar dervo fescht i mis Herz gschlosse. Genau die Begägnige und dr persönlich Uustuusch mached TikTok für mich zu öppisem Bsundrigem.</p> <p>Im Oktober 2024 han ich schliesslich de nöchscht Schritt gwagt und bi zum erste Mal live gange. Sithär han ich zahlriichi Mönsche cha kenneleärne und e paar dervo fescht i mis Herz gschlosse. Genau die Begägnige und dr persönlich Uustuusch mached TikTok für mich zu öppisem Bsundrigem.</p>
@@ -83,7 +83,7 @@ const SCOUTS = [
<p>Als Scout unterstütz ich s Team derbi, neui Talänt z'entdecke, ihne zur Syte z'stah und sie uf ihrem persönliche Wäg z'begleite. Debii gaht's mir nöd nume um Zahle oder Erfolg, sondern vor allem um d Mönsche, ihri Ziel und ihri Entwicklig.</p> <p>Als Scout unterstütz ich s Team derbi, neui Talänt z'entdecke, ihne zur Syte z'stah und sie uf ihrem persönliche Wäg z'begleite. Debii gaht's mir nöd nume um Zahle oder Erfolg, sondern vor allem um d Mönsche, ihri Ziel und ihri Entwicklig.</p>
<p>Ich fröi mich uf alles, wo no vor mir liit: neui Begägnige, bsundrigi Erinnerige und no vill gmeinsami Momänt.</p> <p>Ich fröi mich uf alles, wo no vor mir liit: neui Begägnige, bsundrigi Erinnerige und no vill gmeinsami Momänt.</p>
<p class="quote" style="margin-top:1rem;">Das bin ich – Patrick. Nöd perfekt, mängisch mit emene grimmige Blick, aber immer mit Herz. ❤️</p> <p class="quote" style="margin-top:1rem;">Das bin ich – Patrick. Nöd perfekt, mängisch mit emene grimmige Blick, aber immer mit Herz. ❤️</p>
<p style="text-align:right;"><strong>– öie Patrick ❤️</strong></p> <p style="text-align:right;"><strong>– öie Patrick ❤️</strong><br />Scout vom Dogfather</p>
`, `,
en: ` en: `
<p>In October 2024, I finally took the next step and went live for the first time. Since then, I've gotten to know so many people, and some of them have found a firm place in my heart. It's exactly these encounters and the personal exchange that make TikTok something special to me.</p> <p>In October 2024, I finally took the next step and went live for the first time. Since then, I've gotten to know so many people, and some of them have found a firm place in my heart. It's exactly these encounters and the personal exchange that make TikTok something special to me.</p>
@@ -92,7 +92,7 @@ const SCOUTS = [
<p>As a scout, I help the team discover new talent, stand by their side, and guide them on their personal journey. For me, it's not just about numbers or success — it's above all about the people, their goals, and their growth.</p> <p>As a scout, I help the team discover new talent, stand by their side, and guide them on their personal journey. For me, it's not just about numbers or success — it's above all about the people, their goals, and their growth.</p>
<p>I'm looking forward to everything still ahead of me: new encounters, special memories, and many more moments together.</p> <p>I'm looking forward to everything still ahead of me: new encounters, special memories, and many more moments together.</p>
<p class="quote" style="margin-top:1rem;">That's me – Patrick. Not perfect, sometimes with a grumpy look, but always with heart. ❤️</p> <p class="quote" style="margin-top:1rem;">That's me – Patrick. Not perfect, sometimes with a grumpy look, but always with heart. ❤️</p>
<p style="text-align:right;"><strong>– your Patrick ❤️</strong></p> <p style="text-align:right;"><strong>– your Patrick ❤️</strong><br />Scout for Dogfather</p>
`, `,
fr: ` fr: `
<p>En octobre 2024, j'ai finalement franchi l'étape suivante et j'ai fait mon premier live. Depuis, j'ai pu rencontrer de nombreuses personnes et certaines d'entre elles ont pris une vraie place dans mon cœur. Ce sont justement ces rencontres et ces échanges personnels qui rendent TikTok si particulier pour moi.</p> <p>En octobre 2024, j'ai finalement franchi l'étape suivante et j'ai fait mon premier live. Depuis, j'ai pu rencontrer de nombreuses personnes et certaines d'entre elles ont pris une vraie place dans mon cœur. Ce sont justement ces rencontres et ces échanges personnels qui rendent TikTok si particulier pour moi.</p>
@@ -101,7 +101,7 @@ const SCOUTS = [
<p>En tant que scout, j'aide l'équipe à découvrir de nouveaux talents, à les épauler et à les accompagner sur leur propre chemin. Ce qui compte pour moi, ce n'est pas seulement les chiffres ou le succès, mais avant tout les personnes, leurs objectifs et leur évolution.</p> <p>En tant que scout, j'aide l'équipe à découvrir de nouveaux talents, à les épauler et à les accompagner sur leur propre chemin. Ce qui compte pour moi, ce n'est pas seulement les chiffres ou le succès, mais avant tout les personnes, leurs objectifs et leur évolution.</p>
<p>Je me réjouis de tout ce qui m'attend encore : de nouvelles rencontres, des souvenirs particuliers et bien d'autres moments partagés.</p> <p>Je me réjouis de tout ce qui m'attend encore : de nouvelles rencontres, des souvenirs particuliers et bien d'autres moments partagés.</p>
<p class="quote" style="margin-top:1rem;">C'est moi, Patrick. Pas parfait, parfois avec un regard grognon, mais toujours avec du cœur. ❤️</p> <p class="quote" style="margin-top:1rem;">C'est moi, Patrick. Pas parfait, parfois avec un regard grognon, mais toujours avec du cœur. ❤️</p>
<p style="text-align:right;"><strong>– votre Patrick ❤️</strong></p> <p style="text-align:right;"><strong>– votre Patrick ❤️</strong><br />Scout de Dogfather</p>
`, `,
pt: ` pt: `
<p>Em outubro de 2024, finalmente dei o próximo passo e fiz a minha primeira live. Desde então, pude conhecer muitas pessoas e algumas delas conquistaram um lugar especial no meu coração. São exatamente esses encontros e a troca pessoal que tornam o TikTok algo especial para mim.</p> <p>Em outubro de 2024, finalmente dei o próximo passo e fiz a minha primeira live. Desde então, pude conhecer muitas pessoas e algumas delas conquistaram um lugar especial no meu coração. São exatamente esses encontros e a troca pessoal que tornam o TikTok algo especial para mim.</p>
@@ -110,11 +110,21 @@ const SCOUTS = [
<p>Como scout, ajudo a equipe a descobrir novos talentos, a estar ao lado deles e a acompanhá-los no seu caminho pessoal. Para mim, não se trata apenas de números ou sucesso, mas sobretudo das pessoas, dos seus objetivos e do seu desenvolvimento.</p> <p>Como scout, ajudo a equipe a descobrir novos talentos, a estar ao lado deles e a acompanhá-los no seu caminho pessoal. Para mim, não se trata apenas de números ou sucesso, mas sobretudo das pessoas, dos seus objetivos e do seu desenvolvimento.</p>
<p>Estou animado com tudo o que ainda está por vir: novos encontros, memórias especiais e muitos outros momentos em conjunto.</p> <p>Estou animado com tudo o que ainda está por vir: novos encontros, memórias especiais e muitos outros momentos em conjunto.</p>
<p class="quote" style="margin-top:1rem;">Este sou eu – Patrick. Não sou perfeito, às vezes com um olhar carrancudo, mas sempre com coração. ❤️</p> <p class="quote" style="margin-top:1rem;">Este sou eu – Patrick. Não sou perfeito, às vezes com um olhar carrancudo, mas sempre com coração. ❤️</p>
<p style="text-align:right;"><strong>– o seu Patrick ❤️</strong></p> <p style="text-align:right;"><strong>– o seu Patrick ❤️</strong><br />Scout do Dogfather</p>
`, `,
}, },
quote: "", // TODO: Zitat von Patrick einholen quote: {
social: {}, de: "Wege entstehen dadurch, dass man sie geht.",
"de-CH": "Wäg entstönd defür, dass mer sie gaht.",
en: "Paths are created by walking them.",
fr: "Les chemins se créent en marchant.",
pt: "Os caminhos se fazem ao caminhar.",
},
// TikTok-Button auf dem Profil (Nutzer-Wunsch 21.08.2026) -- profil.html
// rendert daraus automatisch den echten .btn-tiktok-Button unter dem
// Namen/Rolle-Text, kein weiterer Code-Umbau nötig (siehe socialLabels
// in profil.html).
social: { tiktok: "https://www.tiktok.com/@patrick180585" },
status: "aktiv", status: "aktiv",
}, },
{ {
@@ -294,7 +304,8 @@ const SCOUTS = [
de: "Ein Mann, ein Wort.", "de-CH": "Bi mir gilt: gseit isch gseit.", de: "Ein Mann, ein Wort.", "de-CH": "Bi mir gilt: gseit isch gseit.",
en: "My word is my bond.", fr: "Chose promise, chose due.", pt: "A minha palavra é a minha honra.", en: "My word is my bond.", fr: "Chose promise, chose due.", pt: "A minha palavra é a minha honra.",
}, },
social: {}, // TikTok-Button auf dem Profil (Nutzer-Wunsch 21.08.2026), siehe Kommentar bei Patrick oben.
social: { tiktok: "https://www.tiktok.com/@bananenstift009" },
status: "aktiv", status: "aktiv",
}, },
// TODO: Dogi liefert die vollständige, aktuelle Scout-Liste (Namen + // TODO: Dogi liefert die vollständige, aktuelle Scout-Liste (Namen +
+87 -7
View File
@@ -29,12 +29,15 @@ window.I18N_PAGE = {
fr: "Cet abonnement n'est ni une boutique ordinaire ni un simple achat. Il est destiné aux personnes qui soutiennent Dogfather durablement et souhaitent faire partie de ce voyage commun.", fr: "Cet abonnement n'est ni une boutique ordinaire ni un simple achat. Il est destiné aux personnes qui soutiennent Dogfather durablement et souhaitent faire partie de ce voyage commun.",
pt: "Esta assinatura não é uma loja comum nem uma compra simples. É para as pessoas que apoiam Dogfather a longo prazo e querem ser parte especial dessa jornada em comum.", pt: "Esta assinatura não é uma loja comum nem uma compra simples. É para as pessoas que apoiam Dogfather a longo prazo e querem ser parte especial dessa jornada em comum.",
}, },
/* Bugfix/Nutzer-Wunsch 20.08.2026: exakter Preis entfernt (nur die erste
Teilphrase geändert, Rest jeder Sprachfassung unangetastet), solange die
Registrierung gesperrt ist — siehe .btn-gold-locked/abo-preis-zahl. */
ab_text2: { ab_text2: {
de: "Für 4,99 € im Monat erhältst du Zugang zu einem exklusiven Supporter-Bereich, besonderen Inhalten und einer persönlichen Treuekollektion. Mit jedem erfolgreich bezahlten Meilenstein wächst deine Sammlung: vom kleinen Hasen-Teddy über besondere Jubiläumsprämien bis zum exklusiven Hoodie nach 30 Monaten. Danach beginnt für dich eine neue Kollektion — mit neuen Designs, neuen Erinnerungen und derselben Bedeutung.", de: "Mit deinem monatlichen Beitrag erhältst du Zugang zu einem exklusiven Supporter-Bereich, besonderen Inhalten und einer persönlichen Treuekollektion. Mit jedem erfolgreich bezahlten Meilenstein wächst deine Sammlung: vom kleinen Hasen-Teddy über besondere Jubiläumsprämien bis zum exklusiven Hoodie nach 30 Monaten. Danach beginnt für dich eine neue Kollektion — mit neuen Designs, neuen Erinnerungen und derselben Bedeutung.",
"de-CH": "Für 4,99 € im Monat überchunnsch du Zugang zu emene exklusive Supporter-Bereich, bsundere Inhalt und ere persönliche Treuekollektion. Mit jedem erfolgriich bezahlte Meilestei wachst dini Sammlig: vom chline Hase-Teddy über bsundere Jubiläumsprämie bis zum exklusive Hoodie nach 30 Monet. Dernah fangt für dich e neui Kollektion a — mit neue Designs, neue Erinnerige und derselbe Bedeutig.", "de-CH": "Mit dim monetliche Bytrag überchunnsch du Zugang zu emene exklusive Supporter-Bereich, bsundere Inhalt und ere persönliche Treuekollektion. Mit jedem erfolgriich bezahlte Meilestei wachst dini Sammlig: vom chline Hase-Teddy über bsundere Jubiläumsprämie bis zum exklusive Hoodie nach 30 Monet. Dernah fangt für dich e neui Kollektion a — mit neue Designs, neue Erinnerige und derselbe Bedeutig.",
en: "For 4.99 € a month, you get access to an exclusive supporter area, special content, and a personal loyalty collection. With every successfully paid milestone, your collection grows — from the small bunny teddy through special anniversary rewards up to the exclusive hoodie after 30 months. After that, a new collection begins for you, with new designs, new memories, and the same meaning.", en: "With your monthly contribution, you get access to an exclusive supporter area, special content, and a personal loyalty collection. With every successfully paid milestone, your collection grows — from the small bunny teddy through special anniversary rewards up to the exclusive hoodie after 30 months. After that, a new collection begins for you, with new designs, new memories, and the same meaning.",
fr: "Pour 4,99 € par mois, tu as accès à un espace supporter exclusif, à du contenu spécial et à une collection de fidélité personnelle. Avec chaque mois payé avec succès, ta collection grandit : du petit lapin en peluche aux récompenses d'anniversaire jusqu'au hoodie exclusif après 30 mois. Ensuite, une nouvelle collection commence pour toi — avec de nouveaux designs, de nouveaux souvenirs et la même signification.", fr: "Avec ta contribution mensuelle, tu as accès à un espace supporter exclusif, à du contenu spécial et à une collection de fidélité personnelle. Avec chaque mois payé avec succès, ta collection grandit : du petit lapin en peluche aux récompenses d'anniversaire jusqu'au hoodie exclusif après 30 mois. Ensuite, une nouvelle collection commence pour toi — avec de nouveaux designs, de nouveaux souvenirs et la même signification.",
pt: "Por 4,99 € por mês, você tem acesso a uma área exclusiva de apoiadores, conteúdo especial e uma coleção pessoal de fidelidade. A cada mês pago com sucesso, sua coleção cresce: do pequeno coelhinho de pelúcia até recompensas especiais de aniversário e o hoodie exclusivo após 30 meses. Depois disso, começa uma nova coleção para você — com novos designs, novas memórias e o mesmo significado.", pt: "Com sua contribuição mensal, você tem acesso a uma área exclusiva de apoiadores, conteúdo especial e uma coleção pessoal de fidelidade. A cada mês pago com sucesso, sua coleção cresce: do pequeno coelhinho de pelúcia até recompensas especiais de aniversário e o hoodie exclusivo após 30 meses. Depois disso, começa uma nova coleção para você — com novos designs, novas memórias e o mesmo significado.",
}, },
ab_vorteil1: { ab_vorteil1: {
de: "Exklusiver Supporter-Bereich", "de-CH": "Exklusive Supporter-Bereich", de: "Exklusiver Supporter-Bereich", "de-CH": "Exklusive Supporter-Bereich",
@@ -123,10 +126,62 @@ window.I18N_PAGE = {
de: "Registrieren", "de-CH": "Registriere", de: "Registrieren", "de-CH": "Registriere",
en: "Register", fr: "S'inscrire", pt: "Registrar", en: "Register", fr: "S'inscrire", pt: "Registrar",
}, },
ab_tab_register: {
de: "Neu registrieren", "de-CH": "Nöi registriere",
en: "Sign up", fr: "S'inscrire", pt: "Registrar",
},
ab_tab_login: {
de: "Schon registriert? Einloggen", "de-CH": "Scho registriert? Yylogge",
en: "Already registered? Log in", fr: "Déjà inscrit ? Se connecter", pt: "Já registrado? Entrar",
},
ab_switch_login: { ab_switch_login: {
de: "Schon Supporter? Hier einloggen", "de-CH": "Scho Supporter? Da yylogge", de: "Schon Supporter? Hier einloggen", "de-CH": "Scho Supporter? Da yylogge",
en: "Already a supporter? Log in here", fr: "Déjà supporter ? Connecte-toi ici", pt: "Já é apoiador? Faça login aqui", en: "Already a supporter? Log in here", fr: "Déjà supporter ? Connecte-toi ici", pt: "Já é apoiador? Faça login aqui",
}, },
ab_feld_code_neu: {
de: "Denk dir jetzt deinen Zugangscode aus (mindestens 6 Zeichen)",
"de-CH": "Dänk der jetz din Zuegangscode us (mindeschtens 6 Zeiche)",
en: "Now choose your own access code (at least 6 characters)",
fr: "Choisis maintenant ton code d'accès (au moins 6 caractères)",
pt: "Agora escolha seu código de acesso (mínimo 6 caracteres)",
},
ab_feld_code_platzhalter: {
de: "Frei wählbar — z. B. DogiCrew2026",
"de-CH": "Frei wählbar — z. B. DogiCrew2026",
en: "Anything you like — e.g. DogiCrew2026",
fr: "Comme tu veux — p. ex. DogiCrew2026",
pt: "À sua escolha — ex. DogiCrew2026",
},
ab_code_hinweis_neu: {
de: "Mit diesem Code loggst du dich künftig ein — merk ihn dir gut.",
"de-CH": "Mit däm Code loggsch di künftig y — merk der en guet.",
en: "You'll use this code to log in from now on — keep it safe.",
fr: "Tu utiliseras ce code pour te connecter — garde-le en lieu sûr.",
pt: "Você usará este código para entrar a partir de agora — guarde-o bem.",
},
ab_feld_code_eigen: {
de: "Dein Zugangscode", "de-CH": "Din Zuegangscode",
en: "Your access code", fr: "Ton code d'accès", pt: "Seu código de acesso",
},
ab_btn_login: {
de: "Einloggen", "de-CH": "Yylogge",
en: "Log in", fr: "Se connecter", pt: "Entrar",
},
ab_code_vergessen: {
de: "Code vergessen?", "de-CH": "Code vergässe?",
en: "Forgot your code?", fr: "Code oublié ?", pt: "Esqueceu seu código?",
},
ab_neuer_code_hinweis: {
de: "Fast fertig — leg jetzt deinen neuen Zugangscode fest.",
"de-CH": "Fascht fertig — leg jetz din nöie Zuegangscode fescht.",
en: "Almost done — now set your new access code.",
fr: "Presque fini — définis maintenant ton nouveau code d'accès.",
pt: "Quase pronto — agora defina seu novo código de acesso.",
},
ab_btn_code_speichern: {
de: "Zugangscode speichern", "de-CH": "Zuegangscode spichere",
en: "Save access code", fr: "Enregistrer le code d'accès", pt: "Salvar código de acesso",
},
ab_btn_login_code: { ab_btn_login_code: {
de: "Login-Code anfordern", "de-CH": "Login-Code aafordere", de: "Login-Code anfordern", "de-CH": "Login-Code aafordere",
en: "Request login code", fr: "Demander un code de connexion", pt: "Solicitar código de login", en: "Request login code", fr: "Demander un code de connexion", pt: "Solicitar código de login",
@@ -139,6 +194,16 @@ window.I18N_PAGE = {
de: "Wir haben dir einen 6-stelligen Code per E-Mail geschickt.", "de-CH": "Mir händ dir en 6-stellige Code per E-Mail gschickt.", de: "Wir haben dir einen 6-stelligen Code per E-Mail geschickt.", "de-CH": "Mir händ dir en 6-stellige Code per E-Mail gschickt.",
en: "We've sent you a 6-digit code by email.", fr: "Nous t'avons envoyé un code à 6 chiffres par e-mail.", pt: "Enviamos um código de 6 dígitos para o seu e-mail.", en: "We've sent you a 6-digit code by email.", fr: "Nous t'avons envoyé un code à 6 chiffres par e-mail.", pt: "Enviamos um código de 6 dígitos para o seu e-mail.",
}, },
/* Hinweis im Registrierungsformular (21.08.2026): erklärt vorab, dass jetzt
zwei weitere Schritte kommen (Code aus der E-Mail, danach eigener
Zugangscode) -- sonst wirkt der Sprung zur Code-Eingabe überraschend. */
ab_reg_hinweis_bestaetigung: {
de: "Wir schicken dir gleich einen 6-stelligen Code per E-Mail. Danach legst du deinen eigenen Zugangscode fest.",
"de-CH": "Mir schicked dir grad en 6-stellige Code per E-Mail. Denn legsch du din eigene Zuegangscode fescht.",
en: "We'll send you a 6-digit code by email in a moment. After that you'll set your own access code.",
fr: "Nous t'enverrons dans un instant un code à 6 chiffres par e-mail. Ensuite, tu définiras ton propre code d'accès.",
pt: "Enviaremos em instantes um código de 6 dígitos para o seu e-mail. Depois você define o seu próprio código de acesso.",
},
ab_feld_code: { ab_feld_code: {
de: "Code", "de-CH": "Code", de: "Code", "de-CH": "Code",
en: "Code", fr: "Code", pt: "Código", en: "Code", fr: "Code", pt: "Código",
@@ -148,8 +213,23 @@ window.I18N_PAGE = {
en: "Confirm", fr: "Confirmer", pt: "Confirmar", en: "Confirm", fr: "Confirmer", pt: "Confirmar",
}, },
ab_eingeloggt_als: { ab_eingeloggt_als: {
de: "Eingeloggt. Noch ein letzter Schritt:", "de-CH": "Yyglogget. No en letschte Schritt:", de: "Eingeloggt.", "de-CH": "Yyglogget.",
en: "Logged in. One last step:", fr: "Connecté. Une dernière étape :", pt: "Conectado. Um último passo:", en: "Logged in.", fr: "Connecté.", pt: "Conectado.",
},
ab_subscribe_locked_hinweis: {
de: "Dein Konto ist bereit — die Bezahlung startet, sobald das Abo offiziell live geht. Schau einfach bald wieder vorbei.",
"de-CH": "Dis Konto isch parat — d Zahlig startet, sobald s Abo offiziell live gaht. Schau eifach bald wieder verbi.",
en: "Your account is ready — payment opens as soon as the subscription officially launches. Just check back soon.",
fr: "Ton compte est prêt — le paiement s'ouvrira dès le lancement officiel de l'abonnement. Repasse bientôt.",
pt: "Sua conta está pronta — o pagamento abre assim que a assinatura for lançada oficialmente. Volte em breve.",
},
ab_weiter_startseite: {
de: "Zur Startseite →", "de-CH": "Zur Startsiite →",
en: "To the homepage →", fr: "Vers l'accueil →", pt: "Para a página inicial →",
},
ab_weiter_streamplan: {
de: "Zum Streamplan →", "de-CH": "Zum Streamplan →",
en: "To the stream schedule →", fr: "Vers le programme des lives →", pt: "Para a programação de lives →",
}, },
ab_oder_email: { ab_oder_email: {
de: "oder mit E-Mail", "de-CH": "oder mit E-Mail", de: "oder mit E-Mail", "de-CH": "oder mit E-Mail",
+11 -5
View File
@@ -8,12 +8,18 @@ window.I18N_PAGE = {
de: "Werde Teil vom Team", "de-CH": "Wird Teil vom Team", de: "Werde Teil vom Team", "de-CH": "Wird Teil vom Team",
en: "Join the team", fr: "Rejoins l'équipe", pt: "Faça parte da equipe", en: "Join the team", fr: "Rejoins l'équipe", pt: "Faça parte da equipe",
}, },
// Nutzer-Wunsch 21.08.2026: das letzte Wort stand auf manchen Bildschirmbreiten
// ganz allein in einer eigenen Zeile ("Weg" unter sich, siehe Screenshot) --
// sieht typografisch unruhig aus. &nbsp; zwischen den letzten zwei Wörtern
// verhindert, dass genau DAZWISCHEN umgebrochen wird: fällt eine Zeile um,
// dann nehmen die letzten zwei Wörter sich gegenseitig mit statt eines davon
// einsam zurückzulassen.
bw_h1: { bw_h1: {
de: "Community oder Agentur — such dir deinen Weg", de: "Community oder Agentur — such dir deinen&nbsp;Weg",
"de-CH": "Community oder Agentur — such dir dis Wäg", "de-CH": "Community oder Agentur — such dir dis&nbsp;Wäg",
en: "Community or agency — find your path", en: "Community or agency — find your&nbsp;path",
fr: "Communauté ou agence — trouve ta voie", fr: "Communauté ou agence — trouve ta&nbsp;voie",
pt: "Comunidade ou agência — encontre o seu caminho", pt: "Comunidade ou agência — encontre o seu&nbsp;caminho",
}, },
bw_lead: { bw_lead: {
de: "Vier Bewerbungen, drei ganz unterschiedliche Rollen. Wähl, was zu dir passt.", de: "Vier Bewerbungen, drei ganz unterschiedliche Rollen. Wähl, was zu dir passt.",
-8
View File
@@ -38,14 +38,6 @@ window.I18N_PAGE = {
de: "Discord beitreten", "de-CH": "Bi Discord mitmache", de: "Discord beitreten", "de-CH": "Bi Discord mitmache",
en: "Join Discord", fr: "Rejoindre le Discord", pt: "Entrar no Discord", en: "Join Discord", fr: "Rejoindre le Discord", pt: "Entrar no Discord",
}, },
com_bild_platzhalter: {
de: "Community-Bild", "de-CH": "Community-Bild",
en: "Community Photo", fr: "Photo de la communauté", pt: "Foto da comunidade",
},
com_foto_folgt: {
de: "Foto folgt", "de-CH": "Foto chunnt no",
en: "Photo coming soon", fr: "Photo à venir", pt: "Foto em breve",
},
com_story_h2: { com_story_h2: {
de: "Team Dogi – mehr als eine Community 🩵", "de-CH": "Team Dogi – meh als e Community 🩵", de: "Team Dogi – mehr als eine Community 🩵", "de-CH": "Team Dogi – meh als e Community 🩵",
en: "Team Dogi – More Than a Community 🩵", fr: "Team Dogi – bien plus qu'une communauté 🩵", pt: "Team Dogi – mais do que uma comunidade 🩵", en: "Team Dogi – More Than a Community 🩵", fr: "Team Dogi – bien plus qu'une communauté 🩵", pt: "Team Dogi – mais do que uma comunidade 🩵",
+7
View File
@@ -39,6 +39,13 @@ window.I18N_PAGE = {
de: "Profil ansehen", "de-CH": "Profil aluege", de: "Profil ansehen", "de-CH": "Profil aluege",
en: "View profile", fr: "Voir le profil", pt: "Ver perfil", en: "View profile", fr: "Voir le profil", pt: "Ver perfil",
}, },
/* 21.08.2026: Fallback-Kategorie für über die Verwaltung angelegte
Creator ohne eigenes Genre (die Verwaltung fragt aktuell keine
Genre-Kategorie ab) -- verhindert einen "undefined"-Filterknopf. */
crt_kategorie_sonstige: {
de: "Sonstige", "de-CH": "Anders",
en: "Other", fr: "Autre", pt: "Outro",
},
crt_bewerben_btn: { crt_bewerben_btn: {
de: "Als Creator bewerben", "de-CH": "Als Creator bewärbe", de: "Als Creator bewerben", "de-CH": "Als Creator bewärbe",
en: "Apply as a creator", fr: "Postuler comme créateur", pt: "Candidatar-se como creator", en: "Apply as a creator", fr: "Postuler comme créateur", pt: "Candidatar-se como creator",
+4
View File
@@ -37,6 +37,10 @@ window.I18N_PAGE = {
de: "🐰 HasiDog – Eine eigene Welt voller Chaos, Herz und Fantasie", "de-CH": "🐰 HasiDog – E eigeti Wält vollr Chaos, Herz und Fantasie", de: "🐰 HasiDog – Eine eigene Welt voller Chaos, Herz und Fantasie", "de-CH": "🐰 HasiDog – E eigeti Wält vollr Chaos, Herz und Fantasie",
en: "🐰 HasiDog – A world of its own, full of chaos, heart and imagination", fr: "🐰 HasiDog – Un monde à part, plein de chaos, de cœur et d'imagination", pt: "🐰 HasiDog – Um mundo à parte, cheio de caos, coração e imaginação", en: "🐰 HasiDog – A world of its own, full of chaos, heart and imagination", fr: "🐰 HasiDog – Un monde à part, plein de chaos, de cœur et d'imagination", pt: "🐰 HasiDog – Um mundo à parte, cheio de caos, coração e imaginação",
}, },
hd_btn_tiktok: {
de: "Zur HasiDog TikTok-Seite", "de-CH": "Zur HasiDog TikTok-Site",
en: "Go to HasiDog's TikTok page", fr: "Voir la page TikTok de HasiDog", pt: "Ir para a página TikTok do HasiDog",
},
hd_welt_1: { hd_welt_1: {
de: "HasiDog ist mehr als nur eine Figur. Hinter HasiDog steckt eine eigene Welt aus Humor, Musik, Geschichten, Überraschungen und liebenswertem Chaos. 🩵", de: "HasiDog ist mehr als nur eine Figur. Hinter HasiDog steckt eine eigene Welt aus Humor, Musik, Geschichten, Überraschungen und liebenswertem Chaos. 🩵",
"de-CH": "HasiDog isch meh als nur e Figur. Hinter HasiDog steckt e eigeti Wält us Humor, Musig, Gschichte, Überraschige und liebenswerte Chaos. 🩵", "de-CH": "HasiDog isch meh als nur e Figur. Hinter HasiDog steckt e eigeti Wält us Humor, Musig, Gschichte, Überraschige und liebenswerte Chaos. 🩵",
+21 -8
View File
@@ -81,6 +81,27 @@ window.I18N_PAGE = {
fr: "En tant que manager chez Spicy Media : scouts, creators et une collaboration équitable, d'égal à égal.", fr: "En tant que manager chez Spicy Media : scouts, creators et une collaboration équitable, d'égal à égal.",
pt: "Como manager na Spicy Media: scouts, creators e uma parceria justa entre iguais.", pt: "Como manager na Spicy Media: scouts, creators e uma parceria justa entre iguais.",
}, },
/* ---------- "Event des Jahres" (20.08.2026, Nutzer-Wunsch) — volle Breite unter den
3 Welt-Kacheln, zeigt bewusst IMMER genau zwei Events. Texte sind gekürzte, inhaltlich
unveränderte Fassungen der ausführlichen Beschreibungen aus i18n-zeitreise.js
(zr_ev4_h3/p, zr_ev9_h3/p) — dieselben zwei Events, nur kompakter für die Kachel. ---------- */
idx_jahres_events_tag: {
de: "🏆 Event des Jahres", "de-CH": "🏆 Event vom Jahr",
en: "🏆 Event of the Year", fr: "🏆 Événement de l'année", pt: "🏆 Evento do ano",
},
idx_jahres_events_mehr: {
de: "Mehr erfahren →", "de-CH": "Mehr erfahre →",
en: "Learn more →", fr: "En savoir plus →", pt: "Saiba mais →",
},
idx_jahres_event_modal_link: {
de: "Direkt ansehen →", "de-CH": "Direkt aluege →",
en: "View directly →", fr: "Voir directement →", pt: "Ver diretamente →",
},
idx_jahres_event_modal_teilen: {
de: "📤 Teilen", "de-CH": "📤 Teile",
en: "📤 Share", fr: "📤 Partager", pt: "📤 Compartilhar",
},
idx_char_eyebrow: { idx_char_eyebrow: {
de: "🩵 Die Gesichter der Welten", "de-CH": "🩵 D Gsichter vo de Wälte", de: "🩵 Die Gesichter der Welten", "de-CH": "🩵 D Gsichter vo de Wälte",
en: "🩵 The Faces of the Worlds", fr: "🩵 Les visages des mondes", pt: "🩵 Os rostos dos mundos", en: "🩵 The Faces of the Worlds", fr: "🩵 Les visages des mondes", pt: "🩵 Os rostos dos mundos",
@@ -187,14 +208,6 @@ window.I18N_PAGE = {
de: "Modi-Team ansehen", "de-CH": "Modi-Team aluege", de: "Modi-Team ansehen", "de-CH": "Modi-Team aluege",
en: "View Modi-Team", fr: "Voir l'équipe Modi", pt: "Ver equipe Modi", en: "View Modi-Team", fr: "Voir l'équipe Modi", pt: "Ver equipe Modi",
}, },
idx_aktionen_h2: {
de: "Was gerade läuft", "de-CH": "Was grad lauft",
en: "What's happening right now", fr: "Ce qui se passe en ce moment", pt: "O que está rolando agora",
},
idx_aktionen_btn: {
de: "Alle Aktionen & Projekte", "de-CH": "Alli Aktione & Projekt",
en: "All actions & projects", fr: "Toutes les actions et projets", pt: "Todas as ações e projetos",
},
idx_galerie_h2: { de: "Impressionen", "de-CH": "Impressione", en: "Impressions", fr: "Aperçus", pt: "Impressões" }, idx_galerie_h2: { de: "Impressionen", "de-CH": "Impressione", en: "Impressions", fr: "Aperçus", pt: "Impressões" },
idx_galerie_btn: { idx_galerie_btn: {
de: "Zur ganzen Galerie", "de-CH": "Zur ganze Galerie", de: "Zur ganzen Galerie", "de-CH": "Zur ganze Galerie",
+12
View File
@@ -177,4 +177,16 @@ window.I18N_PAGE = {
fr: "Les scouts trouvent et accompagnent de nouveaux talents pour Dogi.", fr: "Les scouts trouvent et accompagnent de nouveaux talents pour Dogi.",
pt: "Os scouts encontram e acompanham novos talentos para o Dogi.", pt: "Os scouts encontram e acompanham novos talentos para o Dogi.",
}, },
/* 21.08.2026: Abschnitt für weitere, über die Verwaltung angelegte Manager
(siehe zeitreise.html, "Aufbau eines eigenen Creator-Systems" ->
"zukünftige Manager"). Bleibt komplett unsichtbar, solange keiner
eingetragen ist -- siehe renderWeitereManager() weiter unten. */
mgr_weitere_h2: {
de: "Weitere Manager", "de-CH": "Witeri Manager",
en: "More Managers", fr: "Autres managers", pt: "Outros managers",
},
mgr_weitere_profil_btn: {
de: "Profil ansehen", "de-CH": "Profil aluege",
en: "View profile", fr: "Voir le profil", pt: "Ver perfil",
},
}; };
+8
View File
@@ -30,6 +30,10 @@ window.I18N_PAGE = {
de: "Manager DogFather — Creator", "de-CH": "Manager DogFather — Creator", de: "Manager DogFather — Creator", "de-CH": "Manager DogFather — Creator",
en: "Manager DogFather — Creator", fr: "Manager DogFather — Creator", pt: "Manager DogFather — Creator", en: "Manager DogFather — Creator", fr: "Manager DogFather — Creator", pt: "Manager DogFather — Creator",
}, },
pr_eyebrow_manager: {
de: "Manager DogFather — Spicy Media", "de-CH": "Manager DogFather — Spicy Media",
en: "Manager DogFather — Spicy Media", fr: "Manager DogFather — Spicy Media", pt: "Manager DogFather — Spicy Media",
},
pr_back_modis: { pr_back_modis: {
de: "← Zurück zum Modi-Team", "de-CH": "← Zrugg zum Modi-Team", de: "← Zurück zum Modi-Team", "de-CH": "← Zrugg zum Modi-Team",
en: "← Back to the Modi Team", fr: "← Retour à l'équipe Modi", pt: "← Voltar à equipe Modi", en: "← Back to the Modi Team", fr: "← Retour à l'équipe Modi", pt: "← Voltar à equipe Modi",
@@ -42,6 +46,10 @@ window.I18N_PAGE = {
de: "← Zurück zu den Creatorn", "de-CH": "← Zrugg zu de Creator", de: "← Zurück zu den Creatorn", "de-CH": "← Zrugg zu de Creator",
en: "← Back to the Creators", fr: "← Retour aux Creators", pt: "← Voltar aos Creators", en: "← Back to the Creators", fr: "← Retour aux Creators", pt: "← Voltar aos Creators",
}, },
pr_back_manager: {
de: "← Zurück zu Manager DogFather", "de-CH": "← Zrugg zu Manager DogFather",
en: "← Back to Manager DogFather", fr: "← Retour à Manager DogFather", pt: "← Voltar a Manager DogFather",
},
pr_badge_prefix: { pr_badge_prefix: {
de: "Stufe", "de-CH": "Stuefe", de: "Stufe", "de-CH": "Stuefe",
en: "Level", fr: "Niveau", pt: "Nível", en: "Level", fr: "Niveau", pt: "Nível",
+37 -27
View File
@@ -16,37 +16,47 @@ window.I18N_PAGE = {
de: "Was Community und Team sagen.", "de-CH": "Was d Community und s Team säge.", de: "Was Community und Team sagen.", "de-CH": "Was d Community und s Team säge.",
en: "What the community and the team say.", fr: "Ce que disent la communauté et l'équipe.", pt: "O que a comunidade e a equipe dizem.", en: "What the community and the team say.", fr: "Ce que disent la communauté et l'équipe.", pt: "O que a comunidade e a equipe dizem.",
}, },
st_quote_community: { /* Eigene Stimme einreichen (20.08.2026) */
de: `„Hier steht bald eine echte Stimme aus der Community. 🩵"`, st_form_titel: {
"de-CH": `„Da staht bald e echti Stimm us de Community. 🩵"`, de: "Deine Stimme zählt", "de-CH": "Dini Stimm zählt",
en: `"A real voice from the community will be here soon. 🩵"`, en: "Your Voice Matters", fr: "Ta voix compte", pt: "Sua voz importa",
fr: `« Un vrai témoignage de la communauté arrivera bientôt ici. 🩵 »`,
pt: `„Em breve, uma voz real da comunidade estará aqui. 🩵"`,
}, },
st_label_community: { st_form_text: {
de: "— Community-Mitglied", "de-CH": "— Community-Mitglied", de: "Was bedeutet dir Team Dogi? Schreib uns deine Geschichte — mit etwas Glück liest DogFather sie persönlich und stellt sie hier auf die Seite.",
en: "— Community Member", fr: "— Membre de la communauté", pt: "— Membro da comunidade", "de-CH": "Was bedütet dir Team Dogi? Schriib eus dini Gschicht — mit chli Glück list DogFather sie persönlich und stellt sie da uf d Site.",
en: "What does Team Dogi mean to you? Write us your story — with a bit of luck, DogFather will read it personally and feature it here on the site.",
fr: "Que représente Team Dogi pour toi ? Écris-nous ton histoire — avec un peu de chance, DogFather la lira personnellement et la publiera ici.",
pt: "O que a Team Dogi significa para você? Escreva sua história — com um pouco de sorte, o DogFather vai ler pessoalmente e publicar aqui.",
}, },
st_quote_team: { st_form_name_label: {
de: `„Hier steht bald eine echte Stimme aus dem Team. 🩵"`, de: "Dein Name", "de-CH": "Dis Name",
"de-CH": `„Da staht bald e echti Stimm vom Team. 🩵"`, en: "Your Name", fr: "Ton nom", pt: "Seu nome",
en: `"A real voice from the team will be here soon. 🩵"`,
fr: `« Un vrai témoignage de l'équipe arrivera bientôt ici. 🩵 »`,
pt: `„Em breve, uma voz real da equipe estará aqui. 🩵"`,
}, },
st_label_team: { st_form_tiktok_label: {
de: "— Team-Mitglied", "de-CH": "— Team-Mitglied", de: "TikTok-Name (optional)", "de-CH": "TikTok-Name (optional)",
en: "— Team Member", fr: "— Membre de l'équipe", pt: "— Membro da equipe", en: "TikTok Username (optional)", fr: "Nom TikTok (facultatif)", pt: "Nome no TikTok (opcional)",
}, },
st_quote_scout: { st_form_nachricht_label: {
de: `„Hier steht bald eine echte Stimme von Scouts &amp; Creatorn. 🩵"`, de: "Deine Nachricht", "de-CH": "Dini Nachricht",
"de-CH": `„Da staht bald e echti Stimm vo Scouts &amp; Creator. 🩵"`, en: "Your Message", fr: "Ton message", pt: "Sua mensagem",
en: `"A real voice from scouts &amp; creators will be here soon. 🩵"`,
fr: `« Un vrai témoignage de scouts et créateurs arrivera bientôt ici. 🩵 »`,
pt: `„Em breve, uma voz real de scouts e criadores estará aqui. 🩵"`,
}, },
st_label_scout: { st_form_nachricht_placeholder: {
de: "— Scout/Creator", "de-CH": "— Scout/Creator", de: "Erzähl uns, was dir Team Dogi bedeutet …", "de-CH": "Verzell eus, was dir Team Dogi bedütet …",
en: "— Scout/Creator", fr: "— Scout/Créateur", pt: "— Scout/Criador", en: "Tell us what Team Dogi means to you …", fr: "Dis-nous ce que Team Dogi représente pour toi …", pt: "Conte-nos o que a Team Dogi significa para você …",
},
st_form_absenden: {
de: "💙 Nachricht einreichen", "de-CH": "💙 Nachricht ischicke",
en: "💙 Submit Message", fr: "💙 Envoyer le message", pt: "💙 Enviar mensagem",
},
st_form_erfolg: {
de: "Danke dir! Deine Nachricht wurde eingereicht und wird von Dogi persönlich geprüft. 🩵",
"de-CH": "Danke dir! Dini Nachricht isch ischickt worde und wird vo Dogi persönlich gprüeft. 🩵",
en: "Thank you! Your message has been submitted and will be reviewed by Dogi personally. 🩵",
fr: "Merci ! Ton message a été envoyé et sera examiné personnellement par Dogi. 🩵",
pt: "Obrigado! Sua mensagem foi enviada e será revisada pessoalmente pelo Dogi. 🩵",
},
st_echte_titel: {
de: "Das sagt die Community", "de-CH": "Das seit d Community",
en: "What the Community Says", fr: "Ce que dit la communauté", pt: "O que a comunidade diz",
}, },
}; };
+42
View File
@@ -20,6 +20,48 @@ window.I18N_PAGE = {
de: "Zur Dogi TikTok-Seite", "de-CH": "Zur Dogi TikTok-Site", de: "Zur Dogi TikTok-Seite", "de-CH": "Zur Dogi TikTok-Site",
en: "Go to Dogi's TikTok page", fr: "Voir la page TikTok de Dogi", pt: "Ir para a página TikTok do Dogi", en: "Go to Dogi's TikTok page", fr: "Voir la page TikTok de Dogi", pt: "Ir para a página TikTok do Dogi",
}, },
/* Live-Radar (20.08.2026) */
sp_radar_loading: {
de: "Lädt …", "de-CH": "Lädt …",
en: "Loading …", fr: "Chargement …", pt: "Carregando …",
},
sp_radar_offline: {
de: "Nächster Fix-Stream in", "de-CH": "Nächschte Fix-Stream in",
en: "Next fixed stream in", fr: "Prochain live fixe dans", pt: "Próxima live fixa em",
},
sp_radar_countdown_label: {
de: "täglich ab 20 Uhr", "de-CH": "täglich ab 20 Uhr",
en: "daily from 8 PM", fr: "chaque jour à 20h", pt: "diariamente às 20h",
},
sp_radar_live: {
de: "🔴 DogFather ist LIVE", "de-CH": "🔴 DogFather isch LIVE",
en: "🔴 DogFather is LIVE", fr: "🔴 DogFather est EN DIRECT", pt: "🔴 DogFather está AO VIVO",
},
sp_radar_live_generic: {
de: "Jetzt live auf TikTok", "de-CH": "Jetzt live uf TikTok",
en: "Live now on TikTok", fr: "En direct maintenant sur TikTok", pt: "Ao vivo agora no TikTok",
},
sp_radar_watch_btn: {
de: "🔴 Jetzt anschauen", "de-CH": "🔴 Jetzt aluege",
en: "🔴 Watch now", fr: "🔴 Regarder maintenant", pt: "🔴 Assistir agora",
},
sp_radar_hinweis: {
de: "Alle Zeiten in deiner Ortszeit.", "de-CH": "Alli Ziite in dinere Ortsziit.",
en: "All times shown in your local time.", fr: "Tous les horaires affichés dans ton fuseau local.", pt: "Todos os horários na sua hora local.",
},
sp_radar_hinweis_spezial: {
de: "Spezielles Live-Event", "de-CH": "Spezielles Live-Event",
en: "Special live event", fr: "Événement en direct spécial", pt: "Evento ao vivo especial",
},
sp_radar_mehr_erfahren: {
de: "Mehr erfahren →", "de-CH": "Mehr erfahre →",
en: "Learn more →", fr: "En savoir plus →", pt: "Saiba mais →",
},
sp_radar_teilen: {
de: "📤 Teilen", "de-CH": "📤 Teile",
en: "📤 Share", fr: "📤 Partager", pt: "📤 Compartilhar",
},
sp_card1_tag: { sp_card1_tag: {
de: "Fixer Termin", "de-CH": "Fixe Termin", de: "Fixer Termin", "de-CH": "Fixe Termin",
en: "Fixed Time", fr: "Rendez-vous fixe", pt: "Horário Fixo", en: "Fixed Time", fr: "Rendez-vous fixe", pt: "Horário Fixo",
+37
View File
@@ -58,6 +58,17 @@ window.I18N_PAGE = {
de: "Jetzt abonnieren", "de-CH": "Jetzt abonniere", de: "Jetzt abonnieren", "de-CH": "Jetzt abonniere",
en: "Subscribe now", fr: "S'abonner maintenant", pt: "Assine agora", en: "Subscribe now", fr: "S'abonner maintenant", pt: "Assine agora",
}, },
su_kein_abo_soon: {
de: "Coming soon…", "de-CH": "Coming soon…",
en: "Coming soon…", fr: "Coming soon…", pt: "Coming soon…",
},
su_kein_abo_hinweis: {
de: "Alles hier ist schon für dich vorbereitet — sobald das Abo startet, kannst du es mit einem Klick aktivieren.",
"de-CH": "Alles da isch scho für di parat — sobald s Abo startet, chasch es mit eim Klick aktiviere.",
en: "Everything here is already set up for you — as soon as the subscription starts, you can activate it with one click.",
fr: "Tout est déjà prêt pour toi — dès le lancement de l'abonnement, tu pourras l'activer en un clic.",
pt: "Tudo aqui já está preparado para você — assim que a assinatura começar, você poderá ativá-la com um clique.",
},
su_fenster2_h2: { su_fenster2_h2: {
de: "Mein aktueller Fortschritt", "de-CH": "Mis aktuelle Fortschritt", de: "Mein aktueller Fortschritt", "de-CH": "Mis aktuelle Fortschritt",
en: "My Current Progress", fr: "Ma progression actuelle", pt: "Meu progresso atual", en: "My Current Progress", fr: "Ma progression actuelle", pt: "Meu progresso atual",
@@ -102,4 +113,30 @@ window.I18N_PAGE = {
de: "Zur DogiCrew 🩵", "de-CH": "Zur DogiCrew 🩵", de: "Zur DogiCrew 🩵", "de-CH": "Zur DogiCrew 🩵",
en: "Go to the DogiCrew 🩵", fr: "Aller à la DogiCrew 🩵", pt: "Ir para a DogiCrew 🩵", en: "Go to the DogiCrew 🩵", fr: "Aller à la DogiCrew 🩵", pt: "Ir para a DogiCrew 🩵",
}, },
/* Supporter-Abstimmungen (20.08.2026) */
su_poll_eyebrow: {
de: "🗳️ Aktuelle Abstimmung", "de-CH": "🗳️ Aktueui Abstimmig",
en: "🗳️ Current Poll", fr: "🗳️ Sondage en cours", pt: "🗳️ Enquete atual",
},
su_poll_btn_abstimmen: {
de: "Abstimmen", "de-CH": "Abstimme",
en: "Vote", fr: "Voter", pt: "Votar",
},
su_poll_bitte_waehlen: {
de: "Bitte wähle eine Antwort aus.", "de-CH": "Bitte wähl e Antwort us.",
en: "Please choose an answer.", fr: "Merci de choisir une réponse.", pt: "Por favor, escolha uma resposta.",
},
su_poll_danke: {
de: "Danke, deine Stimme wurde gezählt! 💙", "de-CH": "Danke, dini Stimm isch zellt worde! 💙",
en: "Thanks, your vote was counted! 💙", fr: "Merci, ton vote a été pris en compte ! 💙", pt: "Obrigado, seu voto foi contado! 💙",
},
su_poll_stimmen: {
de: "Stimmen", "de-CH": "Stimme",
en: "votes", fr: "votes", pt: "votos",
},
su_poll_fehler: {
de: "Abstimmen hat leider nicht geklappt. Bitte versuch es nochmal.", "de-CH": "Abstimme het leider nid klappt. Bitte versuech's nomol.",
en: "Voting didn't work. Please try again.", fr: "Le vote a échoué. Merci de réessayer.", pt: "A votação falhou. Por favor, tente novamente.",
},
}; };
+4
View File
@@ -19,6 +19,10 @@ window.I18N_PAGE = {
fr: "L'ossature de Team Dogi — de VanVan à Marina.", fr: "L'ossature de Team Dogi — de VanVan à Marina.",
pt: "A espinha dorsal do Team Dogi — de VanVan a Marina.", pt: "A espinha dorsal do Team Dogi — de VanVan a Marina.",
}, },
tm_btn_tiktok_modigang: {
de: "Zur Modi-Gang TikTok-Seite", "de-CH": "Zur Modi-Gang TikTok-Site",
en: "Go to the Modi Gang TikTok page", fr: "Voir la page TikTok de la Modi-Gang", pt: "Ir para a página TikTok da Modi-Gang",
},
tm_tag_hervorgehoben: { tm_tag_hervorgehoben: {
de: "Hervorgehoben", "de-CH": "Ganz vorne dra", de: "Hervorgehoben", "de-CH": "Ganz vorne dra",
en: "Featured", fr: "En vedette", pt: "Em destaque", en: "Featured", fr: "En vedette", pt: "Em destaque",
+54 -81
View File
@@ -24,7 +24,24 @@ window.I18N_PAGE = {
en: "Date coming soon", fr: "Date à venir", pt: "Data em breve", en: "Date coming soon", fr: "Date à venir", pt: "Data em breve",
}, },
/* ---------- Station 1 — Datum folgt ---------- */ /* ---------- "Wächst noch"-Hinweis (20.08.2026) ---------- */
zr_baustelle_h3: {
de: "Diese Zeitreise wächst noch", "de-CH": "Die Ziitreis wachst no",
en: "This timeline is still growing", fr: "Ce voyage dans le temps continue de grandir", pt: "Esta linha do tempo ainda está crescendo",
},
zr_baustelle_p: {
de: "Wir ergänzen laufend weitere Kapitel, Daten und Momente aus der Geschichte von DogFather &amp; Team Dogi — schau gern öfter mal wieder vorbei.",
"de-CH": "Mir ergänzed laufend wiiteri Kapitel, Date und Momänt us de Gschicht vo DogFather &amp; Team Dogi — schau gärn öppis öfters wieder verbi.",
en: "We're continuously adding more chapters, dates, and moments from the story of DogFather &amp; Team Dogi — feel free to check back again soon.",
fr: "Nous ajoutons en permanence de nouveaux chapitres, dates et moments de l'histoire de DogFather &amp; Team Dogi — n'hésite pas à revenir régulièrement.",
pt: "Estamos adicionando continuamente mais capítulos, datas e momentos da história de DogFather &amp; Team Dogi — volte sempre para conferir.",
},
/* ---------- Station 1 — 24. Oktober 2023 ---------- */
zr_ev1_date: {
de: "24. Oktober 2023", "de-CH": "24. Oktober 2023",
en: "October 24, 2023", fr: "24 octobre 2023", pt: "24 de outubro de 2023",
},
zr_ev1_h3: { zr_ev1_h3: {
de: "Start als Content Creator", "de-CH": "Start als Content Creator", de: "Start als Content Creator", "de-CH": "Start als Content Creator",
en: "Start as a Content Creator", fr: "Débuts en tant que créateur de contenu", pt: "Início como criador de conteúdo", en: "Start as a Content Creator", fr: "Débuts en tant que créateur de contenu", pt: "Início como criador de conteúdo",
@@ -37,7 +54,11 @@ window.I18N_PAGE = {
pt: "DogFather começa sua jornada no TikTok. A partir dos primeiros vídeos e lives, um estilo de conteúdo próprio vai surgindo aos poucos, cheio de humor, personalidade e proximidade real com a comunidade.", pt: "DogFather começa sua jornada no TikTok. A partir dos primeiros vídeos e lives, um estilo de conteúdo próprio vai surgindo aos poucos, cheio de humor, personalidade e proximidade real com a comunidade.",
}, },
/* ---------- Station 2 — Datum folgt ---------- */ /* ---------- Station 2 — Anfang 2025 ---------- */
zr_ev2_date: {
de: "Anfang 2025", "de-CH": "Aafang 2025",
en: "Early 2025", fr: "Début 2025", pt: "Início de 2025",
},
zr_ev2_h3: { zr_ev2_h3: {
de: "Team Dogi entsteht", "de-CH": "Team Dogi entstaht", de: "Team Dogi entsteht", "de-CH": "Team Dogi entstaht",
en: "Team Dogi Is Born", fr: "Naissance de Team Dogi", pt: "Nasce a Team Dogi", en: "Team Dogi Is Born", fr: "Naissance de Team Dogi", pt: "Nasce a Team Dogi",
@@ -84,23 +105,6 @@ window.I18N_PAGE = {
pt: "DogFather encara um desafio especial e transmite ao vivo por 28 horas seguidas. Uma experiência intensa, cheia de diversão, momentos especiais e muita união na comunidade.", pt: "DogFather encara um desafio especial e transmite ao vivo por 28 horas seguidas. Uma experiência intensa, cheia de diversão, momentos especiais e muita união na comunidade.",
}, },
/* ---------- Station 5 — Mai 2026 ---------- */
zr_ev5_date: {
de: "Mai 2026", "de-CH": "Mai 2026",
en: "May 2026", fr: "Mai 2026", pt: "Maio de 2026",
},
zr_ev5_h3: {
de: "Ein neues Kapitel beginnt", "de-CH": "Es neus Kapitel fangt a",
en: "A New Chapter Begins", fr: "Un nouveau chapitre commence", pt: "Um novo capítulo começa",
},
zr_ev5_p: {
de: "DogFather beendet die Zusammenarbeit mit seiner bisherigen Agentur und entscheidet sich dazu, seinen weiteren Weg neu auszurichten. Der Fokus liegt dabei auf persönlicher Weiterentwicklung, neuen Zielen und mehr Eigenständigkeit.",
"de-CH": "DogFather beendet d Zämearbeit mit sinere bisherige Agentur und entscheidet sich derfür, sim wiitere Wäg neu uszrichte. De Fokus liit derbi uf persönlicher Wiiterentwicklig, neue Ziel und meh Eigeständigkeit.",
en: "DogFather ends his collaboration with his previous agency and decides to realign his path. The focus shifts to personal growth, new goals, and greater independence.",
fr: "DogFather met fin à sa collaboration avec son agence précédente et décide de réorienter sa trajectoire. L'accent est désormais mis sur son développement personnel, de nouveaux objectifs et plus d'indépendance.",
pt: "DogFather encerra a parceria com sua antiga agência e decide reorientar seu caminho. O foco passa a ser o desenvolvimento pessoal, novas metas e mais independência.",
},
/* ---------- Station 6 — 2. bis 15. Juni 2026 ---------- */ /* ---------- Station 6 — 2. bis 15. Juni 2026 ---------- */
zr_ev6_date: { zr_ev6_date: {
de: "2. bis 15. Juni 2026", "de-CH": "2. bis 15. Juni 2026", de: "2. bis 15. Juni 2026", "de-CH": "2. bis 15. Juni 2026",
@@ -135,21 +139,21 @@ window.I18N_PAGE = {
pt: "DogFather assume o cargo de gerente na Spicy Media. Além de atuar como streamer, ele passa a acompanhar criadores, descobrir talentos e construir sua própria equipe forte.", pt: "DogFather assume o cargo de gerente na Spicy Media. Além de atuar como streamer, ele passa a acompanhar criadores, descobrir talentos e construir sua própria equipe forte.",
}, },
/* ---------- Station 8 — Juli 2026 ---------- */ /* ---------- Station 21 — 25. Juli 2026 (Meilenstein, Nutzer-Wunsch 21.08.2026) ---------- */
zr_ev8_date: { zr_ev21_date: {
de: "Juli 2026", "de-CH": "Juli 2026", de: "25. Juli 2026", "de-CH": "25. Juli 2026",
en: "July 2026", fr: "Juillet 2026", pt: "Julho de 2026", en: "July 25, 2026", fr: "25 juillet 2026", pt: "25 de julho de 2026",
}, },
zr_ev8_h3: { zr_ev21_h3: {
de: "Das Manager-Team wächst", "de-CH": "Ds Manager-Team wachst", de: "Erstes Fan-Treffen: Team TiliDog", "de-CH": "Erschts Fan-Träffe: Team TiliDog",
en: "The Manager Team Grows", fr: "L'équipe de management s'agrandit", pt: "A equipe de gerência cresce", en: "First Fan Meetup: Team TiliDog", fr: "Première rencontre fans : Team TiliDog", pt: "Primeiro encontro de fãs: Team TiliDog",
}, },
zr_ev8_p: { zr_ev21_p: {
de: "Mit VanVan als rechter Hand und Diene als wichtiger Unterstützung entsteht eine klare Teamstruktur. Gemeinsam werden Creator begleitet, Probleme gelöst und neue Projekte geplant.", de: "In Wuppertal kommt die Community von Dogi &amp; Tili zum allerersten Mal persönlich zusammen – ein richtig mega Tag voller guter Laune. Es ist gleichzeitig etwas ganz Besonderes: das erste Aufeinandertreffen von DogFather und Tili überhaupt.",
"de-CH": "Mit VanVan als rechti Hand und Diene als wichtigi Unterstützig entstaht e klari Teamstruktur. Zäme wärded Creator begleitet, Problem glöst und neui Projekt planet.", "de-CH": "In Wuppertal chunnt d Community vo Dogi &amp; Tili zum allererschte Mal persönlich zäme – en richtig mega Tag voll gueter Luune. Gliichzitig isch es öppis ganz Bsundrigs: s erschte Ufeinanderträffe vo DogFather und Tili überhaupt.",
en: "With VanVan as his right hand and Diene as key support, a clear team structure takes shape. Together, they guide creators, solve problems, and plan new projects.", en: "In Wuppertal, the Dogi &amp; Tili community comes together in person for the very first time – a truly awesome day full of good vibes. It's also something very special: the first time DogFather and Tili ever meet face to face.",
fr: "Avec VanVan comme bras droit et Diene comme soutien essentiel, une structure d'équipe claire se met en place. Ensemble, ils accompagnent les créateurs, résolvent les problèmes et planifient de nouveaux projets.", fr: "À Wuppertal, la communauté de Dogi &amp; Tili se retrouve en personne pour la toute première fois – une journée vraiment géniale et pleine de bonne humeur. C'est aussi un moment très particulier : la toute première rencontre entre DogFather et Tili.",
pt: "Com VanVan como braço direito e Diene como apoio fundamental, surge uma estrutura de equipe clara. Juntos, eles acompanham criadores, resolvem problemas e planejam novos projetos.", pt: "Em Wuppertal, a comunidade de Dogi &amp; Tili se reúne pessoalmente pela primeiríssima vez – um dia realmente incrível e cheio de boas vibrações. É também algo muito especial: o primeiro encontro cara a cara entre DogFather e Tili.",
}, },
/* ---------- Station 9 — 27. Juli bis 2. August 2026 (Meilenstein) ---------- */ /* ---------- Station 9 — 27. Juli bis 2. August 2026 (Meilenstein) ---------- */
@@ -162,11 +166,11 @@ window.I18N_PAGE = {
en: "The Creator Cup", fr: "Le Creator Cup", pt: "O Creator Cup", en: "The Creator Cup", fr: "Le Creator Cup", pt: "O Creator Cup",
}, },
zr_ev9_p: { zr_ev9_p: {
de: "Gemeinsam mit Cidem organisiert DogFather den Creator Cup für die Streamer von Spicy Media. Liveminuten, Teamherzen und Coins werden zu Punkten – verbunden mit besonderen Preisen und einer starken gemeinsamen Aktion.", de: "Gemeinsam mit Cidgem organisiert DogFather den Creator Cup für die Streamer von Spicy Media. Liveminuten, Teamherzen und Coins werden zu Punkten – verbunden mit besonderen Preisen und einer starken gemeinsamen Aktion.",
"de-CH": "Zäme mit Cidem organisiert DogFather de Creator Cup für d Streamer vo Spicy Media. Liveminute, Teamherze und Coins wärded zu Punkt — verbunde mit bsundere Priise und ere starke gmeinsame Aktion.", "de-CH": "Zäme mit Cidgem organisiert DogFather de Creator Cup für d Streamer vo Spicy Media. Liveminute, Teamherze und Coins wärded zu Punkt — verbunde mit bsundere Priise und ere starke gmeinsame Aktion.",
en: "Together with Cidem, DogFather organizes the Creator Cup for Spicy Media's streamers. Live minutes, team hearts, and coins become points — combined with special prizes and a strong joint event.", en: "Together with Cidgem, DogFather organizes the Creator Cup for Spicy Media's streamers. Live minutes, team hearts, and coins become points — combined with special prizes and a strong joint event.",
fr: "Avec Cidem, DogFather organise le Creator Cup pour les streamers de Spicy Media. Minutes de live, cœurs d'équipe et coins deviennent des points, avec des récompenses spéciales et un fort esprit collectif.", fr: "Avec Cidgem, DogFather organise le Creator Cup pour les streamers de Spicy Media. Minutes de live, cœurs d'équipe et coins deviennent des points, avec des récompenses spéciales et un fort esprit collectif.",
pt: "Junto com Cidem, DogFather organiza o Creator Cup para os streamers da Spicy Media. Minutos ao vivo, corações da equipe e coins viram pontos, com prêmios especiais e uma forte ação em conjunto.", pt: "Junto com Cidgem, DogFather organiza o Creator Cup para os streamers da Spicy Media. Minutos ao vivo, corações da equipe e coins viram pontos, com prêmios especiais e uma forte ação em conjunto.",
}, },
/* ---------- Station 10 — 28. Juli 2026 ---------- */ /* ---------- Station 10 — 28. Juli 2026 ---------- */
@@ -220,7 +224,11 @@ window.I18N_PAGE = {
pt: "Começa o planejamento de um site próprio. Ele deve unir pela primeira vez os três grandes universos de DogFather: o streamer DogFather, o gerente na Spicy Media e a marca HasiDog.", pt: "Começa o planejamento de um site próprio. Ele deve unir pela primeira vez os três grandes universos de DogFather: o streamer DogFather, o gerente na Spicy Media e a marca HasiDog.",
}, },
/* ---------- Station 13 — Datum folgt ---------- */ /* ---------- Station 13 — November 2025 ---------- */
zr_ev13_date: {
de: "November 2025", "de-CH": "November 2025",
en: "November 2025", fr: "Novembre 2025", pt: "Novembro de 2025",
},
zr_ev13_h3: { zr_ev13_h3: {
de: `Das Buch „HasiDog &amp; Casper"`, "de-CH": `Ds Buech „HasiDog &amp; Casper"`, de: `Das Buch „HasiDog &amp; Casper"`, "de-CH": `Ds Buech „HasiDog &amp; Casper"`,
en: `The Book "HasiDog &amp; Casper"`, fr: `Le livre « HasiDog &amp; Casper »`, pt: `O livro "HasiDog &amp; Casper"`, en: `The Book "HasiDog &amp; Casper"`, fr: `Le livre « HasiDog &amp; Casper »`, pt: `O livro "HasiDog &amp; Casper"`,
@@ -233,7 +241,11 @@ window.I18N_PAGE = {
pt: "A história especial de DogFather, HasiDog e seu husky Casper se transforma em um projeto de livro próprio. Uma história sobre amizade, família, união e aventuras compartilhadas.", pt: "A história especial de DogFather, HasiDog e seu husky Casper se transforma em um projeto de livro próprio. Uma história sobre amizade, família, união e aventuras compartilhadas.",
}, },
/* ---------- Station 14 — Datum folgt ---------- */ /* ---------- Station 14 — 1. Januar 2026 ---------- */
zr_ev14_date: {
de: "1. Januar 2026", "de-CH": "1. Januar 2026",
en: "January 1, 2026", fr: "1er janvier 2026", pt: "1º de janeiro de 2026",
},
zr_ev14_h3: { zr_ev14_h3: {
de: "HasiDog startet auf Spotify", "de-CH": "HasiDog startet uf Spotify", de: "HasiDog startet auf Spotify", "de-CH": "HasiDog startet uf Spotify",
en: "HasiDog Launches on Spotify", fr: "HasiDog arrive sur Spotify", pt: "HasiDog estreia no Spotify", en: "HasiDog Launches on Spotify", fr: "HasiDog arrive sur Spotify", pt: "HasiDog estreia no Spotify",
@@ -259,49 +271,10 @@ window.I18N_PAGE = {
pt: "Começa uma colaboração própria de produtos em torno da VanVan. Estão previstos produtos especiais que refletem sua personalidade e seu papel importante dentro da Team Dogi.", pt: "Começa uma colaboração própria de produtos em torno da VanVan. Estão previstos produtos especiais que refletem sua personalidade e seu papel importante dentro da Team Dogi.",
}, },
/* ---------- Station 16 — Datum folgt ---------- */ /* ---------- Station 19 — 21. August 2026, 21 Uhr (Meilenstein) ---------- */
zr_ev16_h3: {
de: "Die ersten betreuten Creator", "de-CH": "Die erste betreute Creator",
en: "The First Supported Creators", fr: "Les premiers créateurs accompagnés", pt: "Os primeiros criadores acompanhados",
},
zr_ev16_p: {
de: "Die ersten Creator schließen sich dem Team von Manager DogFather an. Sie erhalten persönliche Unterstützung, Hilfe bei Problemen, Profil-Checks, Schulungen und eine langfristige Begleitung auf TikTok.",
"de-CH": "Die erste Creator schliessed sich em Team vom Manager DogFather a. Sie überchömed persönlichi Unterstützig, Hilf bi Problem, Profil-Checks, Schulige und e langfristigi Begleitig uf TikTok.",
en: "The first creators join Manager DogFather's team. They receive personal support, help with problems, profile checks, training, and long-term guidance on TikTok.",
fr: "Les premiers créateurs rejoignent l'équipe du manager DogFather. Ils bénéficient d'un accompagnement personnalisé, d'aide en cas de problème, de vérifications de profil, de formations et d'un suivi durable sur TikTok.",
pt: "Os primeiros criadores se juntam à equipe do gerente DogFather. Eles recebem apoio pessoal, ajuda com problemas, análises de perfil, treinamentos e acompanhamento de longo prazo no TikTok.",
},
/* ---------- Station 17 — Datum folgt ---------- */
zr_ev17_h3: {
de: "Weitere Scouts und Manager kommen hinzu", "de-CH": "Wiiteri Scouts und Manager chömed dezue",
en: "More Scouts and Managers Join", fr: "Plus de scouts et de managers rejoignent l'équipe", pt: "Mais scouts e gerentes se juntam",
},
zr_ev17_p: {
de: "Das Team soll langfristig weiter wachsen. Engagierte Scouts erhalten die Möglichkeit, sich über verschiedene Entwicklungsstufen bis hin zum eigenen Manager weiterzuentwickeln.",
"de-CH": "Ds Team söll langfristig wiiter wachse. Engagierti Scouts überchömed d Möglichkeit, sich über verschideni Entwickligsstufe bis zum eigete Manager wiiterzentwickle.",
en: "The team is set to keep growing long-term. Dedicated scouts get the chance to develop through various stages all the way up to becoming a manager themselves.",
fr: "L'équipe est destinée à continuer de grandir sur le long terme. Les scouts engagés ont la possibilité d'évoluer à travers différents niveaux, jusqu'à devenir eux-mêmes managers.",
pt: "A equipe deve continuar crescendo a longo prazo. Scouts dedicados ganham a chance de evoluir por diferentes níveis até se tornarem gerentes.",
},
/* ---------- Station 18 — Datum folgt ---------- */
zr_ev18_h3: {
de: "Das erste Community-Treffen", "de-CH": "Ds erste Community-Träffe",
en: "The First Community Meetup", fr: "La première rencontre communautaire", pt: "O primeiro encontro da comunidade",
},
zr_ev18_p: {
de: "Team Dogi begegnet sich nicht mehr nur im Livestream oder auf Discord. Beim gemeinsamen Treffen entstehen echte Erinnerungen, die zeigen, dass diese Community weit mehr als nur Zahlen und Likes ist.",
"de-CH": "Team Dogi begägnet sich nid nur im Livestream oder uf Discord. Bim gmeinsame Träffe entstönd echti Erinnerige, wo zeiged, dass die Community wit meh isch als nume Zahle und Likes.",
en: "Team Dogi no longer just meets in livestreams or on Discord. At the shared meetup, real memories are made — proof that this community is far more than just numbers and likes.",
fr: "Team Dogi ne se retrouve plus seulement en live ou sur Discord. Lors de la rencontre commune naissent de vrais souvenirs, preuve que cette communauté est bien plus que des chiffres et des likes.",
pt: "A Team Dogi não se encontra mais só em lives ou no Discord. No encontro presencial nascem memórias reais, mostrando que essa comunidade é muito mais do que números e curtidas.",
},
/* ---------- Station 19 — 2026 – Datum folgt (Meilenstein) ---------- */
zr_ev19_date: { zr_ev19_date: {
de: "2026 – Datum folgt", "de-CH": "2026 – Datum chunnt no", de: "21. August 2026, 21 Uhr", "de-CH": "21. Auguscht 2026, 21 Uhr",
en: "2026 – date coming soon", fr: "2026 – date à venir", pt: "2026 – data em breve", en: "August 21, 2026, 9 PM", fr: "21 août 2026, 21 h", pt: "21 de agosto de 2026, 21h",
}, },
zr_ev19_h3: { zr_ev19_h3: {
de: "DOGFATHER UNIVERSE geht online", "de-CH": "DOGFATHER UNIVERSE gaht online", de: "DOGFATHER UNIVERSE geht online", "de-CH": "DOGFATHER UNIVERSE gaht online",
+304 -19
View File
@@ -76,15 +76,22 @@
const I18N_CORE = { const I18N_CORE = {
nav_start: { de: "Start", "de-CH": "Start", en: "Home", fr: "Accueil", pt: "Início" }, nav_start: { de: "Start", "de-CH": "Start", en: "Home", fr: "Accueil", pt: "Início" },
nav_dogfather: { de: "DogFather", "de-CH": "DogFather", en: "DogFather", fr: "DogFather", pt: "DogFather" }, /* Umbenennungen 20.08.2026 (Nutzer-Wunsch): Kategorie "DogFather" -> "Filipe",
Kategorie "HasiDog" -> "Dogi&Hasi". Die Schlüsselnamen (nav_dogfather...)
bleiben bewusst unverändert — sie sind reine interne Bezeichner und werden
an mehreren Stellen (Nav, Fußzeile, Farbzuordnung) referenziert; ein
Umbenennen würde nur Fehlerquellen schaffen, ohne sichtbaren Nutzen. */
nav_dogfather: { de: "Filipe", "de-CH": "Filipe", en: "Filipe", fr: "Filipe", pt: "Filipe" },
nav_dogfather_person: { de: "Die Person", "de-CH": "D Person", en: "The Person", fr: "La personne", pt: "A pessoa" }, nav_dogfather_person: { de: "Die Person", "de-CH": "D Person", en: "The Person", fr: "La personne", pt: "A pessoa" },
nav_dogfather_streamer: { de: "Streamer", "de-CH": "Streamer", en: "Streamer", fr: "Streamer", pt: "Streamer" }, /* Sitzt seit 20.08.2026 unter "Dogi&Hasi" (nicht mehr unter "Filipe") und heißt
dort "DogFather" statt "Streamer" — zeigt weiterhin auf streamer.html. */
nav_dogfather_streamer: { de: "DogFather", "de-CH": "DogFather", en: "DogFather", fr: "DogFather", pt: "DogFather" },
nav_dogfather_casper: { de: "Casper", "de-CH": "Casper", en: "Casper", fr: "Casper", pt: "Casper" }, nav_dogfather_casper: { de: "Casper", "de-CH": "Casper", en: "Casper", fr: "Casper", pt: "Casper" },
nav_dogfather_vanvan: { de: "VanVan", "de-CH": "VanVan", en: "VanVan", fr: "VanVan", pt: "VanVan" }, nav_dogfather_vanvan: { de: "VanVan", "de-CH": "VanVan", en: "VanVan", fr: "VanVan", pt: "VanVan" },
nav_dogfather_zeitreise: { de: "Zeitreise", "de-CH": "Zytreis", en: "Timeline", fr: "Chronologie", pt: "Linha do tempo" }, nav_dogfather_zeitreise: { de: "Zeitreise", "de-CH": "Zytreis", en: "Timeline", fr: "Chronologie", pt: "Linha do tempo" },
nav_dogfather_ttmuddi: { de: "TT-Muddi", "de-CH": "TT-Muddi", en: "TT-Muddi", fr: "TT-Muddi", pt: "TT-Muddi" }, nav_dogfather_ttmuddi: { de: "TT-Muddi", "de-CH": "TT-Muddi", en: "TT-Muddi", fr: "TT-Muddi", pt: "TT-Muddi" },
nav_hasidog: { de: "HasiDog", "de-CH": "HasiDog", en: "HasiDog", fr: "HasiDog", pt: "HasiDog" }, nav_hasidog: { de: "Dogi&Hasi", "de-CH": "Dogi&Hasi", en: "Dogi&Hasi", fr: "Dogi&Hasi", pt: "Dogi&Hasi" },
nav_hasidog_welt: { de: "HasiDog-Welt", "de-CH": "HasiDog-Wält", en: "HasiDog World", fr: "Monde HasiDog", pt: "Mundo HasiDog" }, nav_hasidog_welt: { de: "HasiDog", "de-CH": "HasiDog", en: "HasiDog", fr: "HasiDog", pt: "HasiDog" },
nav_hasidog_modi: { de: "Modi-Team", "de-CH": "Modi-Team", en: "Modi-Team", fr: "Modi-Team", pt: "Modi-Team" }, nav_hasidog_modi: { de: "Modi-Team", "de-CH": "Modi-Team", en: "Modi-Team", fr: "Modi-Team", pt: "Modi-Team" },
nav_hasidog_community: { de: "Community", "de-CH": "Community", en: "Community", fr: "Communauté", pt: "Comunidade" }, nav_hasidog_community: { de: "Community", "de-CH": "Community", en: "Community", fr: "Communauté", pt: "Comunidade" },
nav_hasidog_stimmen: { de: "Stimmen", "de-CH": "Stimme", en: "Voices", fr: "Témoignages", pt: "Depoimentos" }, nav_hasidog_stimmen: { de: "Stimmen", "de-CH": "Stimme", en: "Voices", fr: "Témoignages", pt: "Depoimentos" },
@@ -104,6 +111,11 @@
nav_cta_abo_anonym: { de: "DogiCrew 👑", "de-CH": "DogiCrew 👑", en: "DogiCrew 👑", fr: "DogiCrew 👑", pt: "DogiCrew 👑" }, nav_cta_abo_anonym: { de: "DogiCrew 👑", "de-CH": "DogiCrew 👑", en: "DogiCrew 👑", fr: "DogiCrew 👑", pt: "DogiCrew 👑" },
nav_cta_abo_aktivieren: { de: "Abo aktivieren 👑", "de-CH": "Abo aktiviere 👑", en: "Activate Subscription 👑", fr: "Activer l'abonnement 👑", pt: "Ativar assinatura 👑" }, nav_cta_abo_aktivieren: { de: "Abo aktivieren 👑", "de-CH": "Abo aktiviere 👑", en: "Activate Subscription 👑", fr: "Activer l'abonnement 👑", pt: "Ativar assinatura 👑" },
nav_cta_abo_bereich: { de: "Supporter-Bereich 👑", "de-CH": "Supporter-Bereich 👑", en: "Supporter Area 👑", fr: "Espace Supporter 👑", pt: "Área do Apoiador 👑" }, nav_cta_abo_bereich: { de: "Supporter-Bereich 👑", "de-CH": "Supporter-Bereich 👑", en: "Supporter Area 👑", fr: "Espace Supporter 👑", pt: "Área do Apoiador 👑" },
/* Kleines Schimmer-Badge auf dem Abo-Knopf, solange die Registrierung
noch gesperrt ist (20.08.2026, Nutzer-Wunsch) — bewusst ein kurzes
Wort statt "Coming soon…", weil in der schmalen Nav-Pille (siehe
Mobil-Menü-Fix vom selben Tag) kaum Platz ist. */
nav_cta_abo_soon: { de: "Bald", "de-CH": "Bald", en: "Soon", fr: "Bientôt", pt: "Em breve" },
nav_toggle_aria: { de: "Menü öffnen", "de-CH": "Menü uftue", en: "Open menu", fr: "Ouvrir le menu", pt: "Abrir menu" }, nav_toggle_aria: { de: "Menü öffnen", "de-CH": "Menü uftue", en: "Open menu", fr: "Ouvrir le menu", pt: "Abrir menu" },
sprache_aria: { de: "Sprache wählen", "de-CH": "Sprach uswähle", en: "Choose language", fr: "Choisir la langue", pt: "Escolher idioma" }, sprache_aria: { de: "Sprache wählen", "de-CH": "Sprach uswähle", en: "Choose language", fr: "Choisir la langue", pt: "Escolher idioma" },
brand_tagline: { brand_tagline: {
@@ -120,7 +132,9 @@
fr: "Streamer. Communauté. HasiDog. Manager. Plus forts ensemble.", fr: "Streamer. Communauté. HasiDog. Manager. Plus forts ensemble.",
pt: "Streamer. Comunidade. HasiDog. Manager. Mais fortes juntos.", pt: "Streamer. Comunidade. HasiDog. Manager. Mais fortes juntos.",
}, },
footer_h4_hasidog_manager: { de: "HasiDog & Manager", "de-CH": "HasiDog & Manager", en: "HasiDog & Manager", fr: "HasiDog et Manager", pt: "HasiDog e Manager" }, /* Fußzeilen-Überschrift zieht mit der Kategorie-Umbenennung mit (20.08.2026):
"HasiDog & Manager" -> "Dogi&Hasi & Manager". */
footer_h4_hasidog_manager: { de: "Dogi&Hasi & Manager", "de-CH": "Dogi&Hasi & Manager", en: "Dogi&Hasi & Manager", fr: "Dogi&Hasi et Manager", pt: "Dogi&Hasi e Manager" },
footer_h4_mehr: { de: "Mehr", "de-CH": "Meh", en: "More", fr: "Plus", pt: "Mais" }, footer_h4_mehr: { de: "Mehr", "de-CH": "Meh", en: "More", fr: "Plus", pt: "Mais" },
footer_scouts: { de: "Scouts", "de-CH": "Scouts", en: "Scouts", fr: "Scouts", pt: "Scouts" }, footer_scouts: { de: "Scouts", "de-CH": "Scouts", en: "Scouts", fr: "Scouts", pt: "Scouts" },
footer_alle_links: { de: "Alle Links", "de-CH": "Alli Links", en: "All links", fr: "Tous les liens", pt: "Todos os links" }, footer_alle_links: { de: "Alle Links", "de-CH": "Alli Links", en: "All links", fr: "Tous les liens", pt: "Todos os links" },
@@ -133,6 +147,7 @@
pt: "© {jahr} DogFather Universe. Team Dogi é família 🩵", pt: "© {jahr} DogFather Universe. Team Dogi é família 🩵",
}, },
footer_impressum: { de: "Impressum & Datenschutz", "de-CH": "Impressum & Datenschutz", en: "Legal notice & privacy", fr: "Mentions légales et confidentialité", pt: "Aviso legal e privacidade" }, footer_impressum: { de: "Impressum & Datenschutz", "de-CH": "Impressum & Datenschutz", en: "Legal notice & privacy", fr: "Mentions légales et confidentialité", pt: "Aviso legal e privacidade" },
footer_app_installieren: { de: "Als App installieren", "de-CH": "Als App installiere", en: "Install as app", fr: "Installer comme application", pt: "Instalar como aplicativo" },
music_title: { de: "Musik von Team Dogi 🎵", "de-CH": "Musig vo Team Dogi 🎵", en: "Music from Team Dogi 🎵", fr: "Musique de Team Dogi 🎵", pt: "Música do Team Dogi 🎵" }, music_title: { de: "Musik von Team Dogi 🎵", "de-CH": "Musig vo Team Dogi 🎵", en: "Music from Team Dogi 🎵", fr: "Musique de Team Dogi 🎵", pt: "Música do Team Dogi 🎵" },
music_close_aria: { de: "Musik-Feld schließen", "de-CH": "Musig-Feld zuemache", en: "Close music panel", fr: "Fermer le panneau musique", pt: "Fechar painel de música" }, music_close_aria: { de: "Musik-Feld schließen", "de-CH": "Musig-Feld zuemache", en: "Close music panel", fr: "Fermer le panneau musique", pt: "Fechar painel de música" },
music_intro: { music_intro: {
@@ -184,6 +199,22 @@
if (text) el.setAttribute(attr, text); if (text) el.setAttribute(attr, text);
}); });
}); });
// 21.08.2026 (Nutzer-Wunsch: "ich will ich titel und namen von
// kategorien und seiten ändern können in der verwaltung"): admin-
// editierbare Text-Overrides (siehe window.dogiSiteTexteLaden() weiter
// unten). Läuft bewusst NACH den normalen data-i18n-Standardtexten
// oben, damit ein gesetzter Override sie überschreiben kann. Fehlt ein
// Override (Normalfall, noch nichts in der Verwaltung eingetragen),
// bleibt einfach der eingebaute Standardtext stehen -- diese Funktion
// hier macht dann schlicht nichts.
const siteTexte = window.__siteTexteCache || {};
document.querySelectorAll("[data-site-text-key]").forEach((el) => {
const eintrag = siteTexte[el.getAttribute("data-site-text-key")];
if (!eintrag) return;
const text = eintrag[sprache] || eintrag.de;
if (text) el.innerHTML = text;
});
} }
/* ---------- Navigationsstruktur (an einer Stelle pflegen) ---------- */ /* ---------- Navigationsstruktur (an einer Stelle pflegen) ---------- */
@@ -193,7 +224,6 @@
key: "nav_dogfather", key: "nav_dogfather",
children: [ children: [
{ key: "nav_dogfather_person", href: "dogfather.html" }, { key: "nav_dogfather_person", href: "dogfather.html" },
{ key: "nav_dogfather_streamer", href: "streamer.html" },
{ key: "nav_dogfather_casper", href: "casper.html" }, { key: "nav_dogfather_casper", href: "casper.html" },
{ key: "nav_dogfather_vanvan", href: "vanvan.html" }, { key: "nav_dogfather_vanvan", href: "vanvan.html" },
{ key: "nav_dogfather_ttmuddi", href: "tt-muddi.html" }, { key: "nav_dogfather_ttmuddi", href: "tt-muddi.html" },
@@ -203,6 +233,10 @@
{ {
key: "nav_hasidog", key: "nav_hasidog",
children: [ children: [
/* "DogFather" (streamer.html) hierher verschoben — Nutzer-Wunsch
20.08.2026: gehört unter "Dogi&Hasi" und dort GANZ NACH OBEN,
direkt über "HasiDog". */
{ key: "nav_dogfather_streamer", href: "streamer.html" },
{ key: "nav_hasidog_welt", href: "hasidog.html" }, { key: "nav_hasidog_welt", href: "hasidog.html" },
{ key: "nav_hasidog_modi", href: "team-modis.html" }, { key: "nav_hasidog_modi", href: "team-modis.html" },
{ key: "nav_hasidog_community", href: "community.html" }, { key: "nav_hasidog_community", href: "community.html" },
@@ -454,6 +488,17 @@
return `${escapeHtml(kuerzen(sauber, 18))} 👑`; return `${escapeHtml(kuerzen(sauber, 18))} 👑`;
} }
/* Kleines Schimmer-Badge "Bald", solange die eigentliche Registrierung auf
abonnieren.html gesperrt ist (20.08.2026, siehe .btn-gold-locked dort).
Nur auf den drei Zielen, die dort ankommen — NICHT auf supporter.html
(echter, bereits funktionierender Bereich für Dogi/VanVan als Test-
Supporter), sonst würde es fälschlich so wirken, als sei der ganze
Supporter-Bereich gesperrt. Reine Wiederverwendung von .btn-silver-text/
-spark (siehe main.css) in kleinerem Maßstab, kein neuer Mechanismus. */
function soonBadge() {
return `<span class="nav-cta-soon"><span class="btn-silver-spark" aria-hidden="true">✨</span><span class="btn-silver-text">${t("nav_cta_abo_soon")}</span></span>`;
}
function renderAboButton() { function renderAboButton() {
const vorschau = getVorschauModus(); const vorschau = getVorschauModus();
if (vorschau === "abonnent") { if (vorschau === "abonnent") {
@@ -469,20 +514,28 @@
return `<a class="nav-cta nav-cta-abo" href="supporter.html"><span>${bereichLabel(echterName || vorschauAnzeigename())}</span></a>`; return `<a class="nav-cta nav-cta-abo" href="supporter.html"><span>${bereichLabel(echterName || vorschauAnzeigename())}</span></a>`;
} }
if (vorschau === "nicht-abonnent") { if (vorschau === "nicht-abonnent") {
return `<a class="nav-cta nav-cta-abo" href="abonnieren.html"><span>${t("nav_cta_abo_anonym")}</span></a>`; return `<a class="nav-cta nav-cta-abo" href="abonnieren.html"><span>${t("nav_cta_abo_anonym")}</span>${soonBadge()}</a>`;
} }
const token = localStorage.getItem("supporterToken"); const token = localStorage.getItem("supporterToken");
const status = localStorage.getItem("supporterSubStatus");
if (!token) { if (!token) {
return `<a class="nav-cta nav-cta-abo" href="abonnieren.html"><span>${t("nav_cta_abo_anonym")}</span></a>`; return `<a class="nav-cta nav-cta-abo" href="abonnieren.html"><span>${t("nav_cta_abo_anonym")}</span>${soonBadge()}</a>`;
} }
const hatBereich = ["active", "pending", "suspended", "cancelled"].includes(status); /* Nutzer-Wunsch 21.08.2026: "meine seite ändert sich nicht und es gibt
if (hatBereich) { keinen button für registrierte sich normal anzumelden und abzumelden.
const name = localStorage.getItem("supporterDisplayName"); wo sie die speziellen sachen sehen als supporter und dass sie auch
return `<a class="nav-cta nav-cta-abo" href="supporter.html"><span>${bereichLabel(name)}</span></a>`; schon den abo sehen aber gesperrt ist." Vorher landeten Leute mit
} Konto, aber ohne bezahltes Abo (status "none"), im Nav-Knopf wieder
return `<a class="nav-cta nav-cta-abo" href="abonnieren.html"><span>${t("nav_cta_abo_aktivieren")}</span></a>`; nur bei abonnieren.html -- man merkte dem Knopf also nie an, dass man
überhaupt eingeloggt ist. supporter.html zeigt für genau diesen Fall
schon lange den Bereich "Mein Abo" -> #supp-keine-abo ("Du hast noch
kein aktives Abo" + Knopf) UND den Ausloggen-Knopf (siehe dort,
#btn-logout) -- das musste also nur noch verlinkt werden. Ab jetzt:
JEDER mit gültigem Token (egal ob "none", "active", "pending", ...)
geht auf supporter.html und sieht dort automatisch den passenden
Zustand samt Logout. */
const name = localStorage.getItem("supporterDisplayName");
return `<a class="nav-cta nav-cta-abo" href="supporter.html"><span>${bereichLabel(name)}</span></a>`;
} }
function renderHeader() { function renderHeader() {
@@ -524,6 +577,24 @@
}); });
}); });
/* Bugfix 20.08.2026 (Nutzer-Report per Screenshot): ein per Klick
geöffnetes Kategorie-Menü blieb offen stehen, bis irgendwo anders
hingeklickt wurde — verlässt man es einfach nur mit der Maus (ohne
zu klicken), sollte es sich von selbst schließen, genau wie das
reine Hover-Aufklappen (siehe main.css @media (hover:hover)) es
nahelegt. Nur auf echten Maus-Geräten (nicht Touch/Tablet, dort
gibt es kein "Verlassen mit der Maus", nur echte Klicks). */
const hatEchteMaus = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
if (hatEchteMaus) {
el.querySelectorAll(".nav-group").forEach((group) => {
group.addEventListener("mouseleave", () => {
if (!group.classList.contains("open")) return;
group.classList.remove("open");
group.querySelector("button")?.setAttribute("aria-expanded", "false");
});
});
}
el.querySelectorAll(".sprach-schalter [data-sprache]").forEach((link) => { el.querySelectorAll(".sprach-schalter [data-sprache]").forEach((link) => {
link.addEventListener("click", (e) => { link.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
@@ -554,12 +625,12 @@
<p class="small" style="margin-top:.8rem;max-width:32ch;"> <p class="small" style="margin-top:.8rem;max-width:32ch;">
${t("footer_tagline")} ${t("footer_tagline")}
</p> </p>
<button type="button" id="pwa-install-btn" class="btn btn-outline" style="display:none;margin-top:1rem;">📲 ${t("footer_app_installieren")}</button>
</div> </div>
<div> <div>
<h4>DogFather</h4> <h4>${t("nav_dogfather")}</h4>
<ul> <ul>
<li><a href="dogfather.html">${t("nav_dogfather_person")}</a></li> <li><a href="dogfather.html">${t("nav_dogfather_person")}</a></li>
<li><a href="streamer.html">${t("nav_dogfather_streamer")}</a></li>
<li><a href="casper.html">${t("nav_dogfather_casper")}</a></li> <li><a href="casper.html">${t("nav_dogfather_casper")}</a></li>
<li><a href="vanvan.html">${t("nav_dogfather_vanvan")}</a></li> <li><a href="vanvan.html">${t("nav_dogfather_vanvan")}</a></li>
<li><a href="tt-muddi.html">${t("nav_dogfather_ttmuddi")}</a></li> <li><a href="tt-muddi.html">${t("nav_dogfather_ttmuddi")}</a></li>
@@ -569,6 +640,10 @@
<div> <div>
<h4>${t("footer_h4_hasidog_manager")}</h4> <h4>${t("footer_h4_hasidog_manager")}</h4>
<ul> <ul>
<!-- "DogFather"/streamer.html steht seit 20.08.2026 auch in der
Fußzeile in dieser Spalte (und ganz oben), damit sie dieselbe
Struktur zeigt wie die Navigation darüber. -->
<li><a href="streamer.html">${t("nav_dogfather_streamer")}</a></li>
<li><a href="hasidog.html">${t("nav_hasidog_welt")}</a></li> <li><a href="hasidog.html">${t("nav_hasidog_welt")}</a></li>
<li><a href="team-modis.html">${t("nav_hasidog_modi")}</a></li> <li><a href="team-modis.html">${t("nav_hasidog_modi")}</a></li>
<li><a href="community.html">${t("nav_hasidog_community")}</a></li> <li><a href="community.html">${t("nav_hasidog_community")}</a></li>
@@ -874,7 +949,22 @@
// erneut laufen, sonst bleibt window.I18N_PAGE nach einem // erneut laufen, sonst bleibt window.I18N_PAGE nach einem
// Zwischenstopp auf einer anderen Seite (die es zurückgesetzt hat) // Zwischenstopp auf einer anderen Seite (die es zurückgesetzt hat)
// fälschlich leer, wenn man z.B. zur Startseite zurückkehrt. // fälschlich leer, wenn man z.B. zur Startseite zurückkehrt.
const istSprachDatei = /\/i18n-[^/]+\.js$/.test(src); //
// BUGFIX 21.08.2026 (Nutzer-Report: rohe Übersetzungs-Schlüssel wie
// "ts_profil_ansehen" sichtbar, aber NUR nachdem man auf einen Link
// geklickt und dann "Zurück" gedrückt hat): das Muster endete bisher
// auf ".js$" (striktes String-Ende). Seit die Cache-Busting-Version
// ("?v=20260821...") an JEDE Skript-URL angehängt wird, endet der
// echte src-Wert aber nie mehr auf ".js", sondern auf ".js?v=...".
// Der Test schlug dadurch für JEDE i18n-Datei fehl -> istSprachDatei
// war immer false -> die i18n-Datei wurde wie eine normale, schon
// geladene Datei übersprungen und NIE erneut ausgeführt, sobald man
// per Klick auf eine andere Seite und wieder zurück navigiert ist.
// window.I18N_PAGE blieb dadurch dauerhaft auf null (von der
// Zwischenseite zurückgesetzt) stehen -- jede Übersetzung fiel auf
// den rohen Schlüsselnamen zurück. Fix: ".js" darf jetzt auch von
// einem "?..." gefolgt werden, nicht nur vom exakten String-Ende.
const istSprachDatei = /\/i18n-[^/]+\.js(\?.*)?$/.test(src);
// Schon vorhanden (z.B. main.js oder eine bereits geladene Datendatei): // Schon vorhanden (z.B. main.js oder eine bereits geladene Datendatei):
// nicht erneut ausführen — sonst gäbe es doppelte Deklarationen. // nicht erneut ausführen — sonst gäbe es doppelte Deklarationen.
@@ -986,6 +1076,7 @@
await skripteUebernehmen(dok); await skripteUebernehmen(dok);
applyTranslations(); // seiteneigene data-i18n-Elemente der neuen Seite übersetzen applyTranslations(); // seiteneigene data-i18n-Elemente der neuen Seite übersetzen
if (window.dogiSiteTexteLaden) window.dogiSiteTexteLaden(); // Kategorie-/Seitentitel-Overrides der neuen Seite
} }
function nahtloseNavigationAktivieren() { function nahtloseNavigationAktivieren() {
@@ -1039,10 +1130,25 @@
renderFooter(); renderFooter();
renderFog(); renderFog();
renderMusicWidget(); renderMusicWidget();
renderVorschauSchalter(); // Bugfix/Nutzer-Wunsch 20.08.2026, vor dem morgigen öffentlichen Start:
// "die leute sollen das ja nicht selbst auswählen können". Der Schalter
// erkennt Dogi/VanVan bisher nur über das rein client-seitig lesbare
// Cookie dogi_role (bewusst NICHT das echte, HttpOnly-geschützte
// Sicherheits-Cookie, siehe Kommentar bei getVorschauRolle() oben) —
// technisch könnte sich also jede Besucherin/jeder Besucher dieses
// Cookie selbst per Browser-Konsole setzen und den Schalter auslösen.
// Echte Daten wären dadurch nie einsehbar (der Schalter zeigt nur
// Beispieldaten für den eigenen Browser), aber "soll weg für die
// Öffentlichkeit" heißt hier bewusst: ganz weg, nicht nur "sicherer".
// renderVorschauSchalter() bleibt unangetastet erhalten (funktioniert
// exakt wie vorher), nur DIESER Aufruf beim Seitenaufbau ist deaktiviert
// — für ein künftiges internes Testen kann diese Zeile jederzeit wieder
// aktiviert werden.
// renderVorschauSchalter();
musikKnopfFarbenAnpassen(); musikKnopfFarbenAnpassen();
nahtloseNavigationAktivieren(); nahtloseNavigationAktivieren();
applyTranslations(); // seiteneigene data-i18n-Elemente (z.B. Startseite) übersetzen applyTranslations(); // seiteneigene data-i18n-Elemente (z.B. Startseite) übersetzen
if (window.dogiSiteTexteLaden) window.dogiSiteTexteLaden(); // Kategorie-/Seitentitel-Overrides (siehe Verwaltung)
}); });
/* Kleine öffentliche Schnittstelle für seiteneigene Inline-Skripte /* Kleine öffentliche Schnittstelle für seiteneigene Inline-Skripte
@@ -1074,6 +1180,142 @@
return feld[sprache] || feld.de || ""; return feld[sprache] || feld.de || "";
}; };
/* =====================================================================
dogiTeilen() — 21.08.2026, Nutzer-Wunsch: "die events des jahres und
speziellen live events soll man auch teilen können." Wiederverwendbare
Teilen-Funktion für einzelne Inhalte (aktuell: Event des Jahres auf
index.html, spezielles Live-Event auf streamplan.html — künftig bei
Bedarf für weitere Stellen nutzbar, ohne den Mechanismus neu zu bauen).
Nutzt zuerst die native Teilen-Funktion des Geräts (Web-Share-API) —
auf dem Handy öffnet das den echten Teilen-Dialog (WhatsApp, TikTok,
Mail, ...). Die meisten Desktop-Browser kennen diese API nicht/zeigen
dort ohnehin kein sinnvolles Menü -- dort stattdessen Fallback: Link in
die Zwischenablage kopieren, der klickende Knopf bekommt kurz ein
"✓ Link kopiert"-Feedback (eigener Text + eigene Klasse, gleiches
Timing wie .vw-copy-btn in der Verwaltung). */
window.dogiTeilen = async function (daten, knopf) {
const payload = { title: daten.title || "", text: daten.text || "", url: daten.url || location.href };
if (navigator.share) {
try {
await navigator.share(payload);
return;
} catch (err) {
if (err && err.name === "AbortError") return; // Dialog selbst abgebrochen -- kein Fallback nötig
// sonst: weiter zum Zwischenablage-Fallback unten
}
}
if (!navigator.clipboard) return;
try {
await navigator.clipboard.writeText(payload.url);
if (knopf) {
const alterText = knopf.textContent;
knopf.textContent = "✓ Link kopiert";
knopf.classList.add("dogi-teilen-kopiert");
setTimeout(() => {
knopf.textContent = alterText;
knopf.classList.remove("dogi-teilen-kopiert");
}, 1600);
}
} catch {
/* Zwischenablage nicht erreichbar (z.B. kein HTTPS-Kontext) -- kein
Blocker, der Link bleibt über die normale Adressleiste erreichbar. */
}
};
/* =====================================================================
dogiTeamLaden() — 21.08.2026, Nutzer-Wunsch: "überall wo Modis sind
oder Scouts oder Manager, oder Creator, will ich dass ich die easy
über meine Verwaltungsseite hinzufüge und die automatisch in der
Website hinzugefügt werden ... auch mit Fotos und TikTok Link."
Holt die über die Verwaltung angelegten Team-Mitglieder einer
Kategorie ("modi" | "scout" | "creator" | "manager") vom Server und
bringt sie in GENAU dieselbe Objektform wie die von Hand gepflegten
Einträge in data-modis.js/data-scouts.js/data-creator.js/
data-manager.js (name/slug/role/photo/bio/quote/social/...) — die
aufrufende Seite hängt das Ergebnis einfach hinten an ihr bestehendes
window.MODIS/SCOUTS/CREATORS/MANAGERS-Array an und ruft ihre eigene,
unveränderte Render-Funktion erneut auf. Kein kategorie-Parameter
liefert ALLE Kategorien zusammen (genutzt von profil.html, das vorab
nicht weiß, zu welcher Kategorie ein slug gehört).
Schlägt die Anfrage fehl (Server nicht erreichbar, Netz weg): leeres
Array statt Fehler -- die von Hand gepflegten Einträge bleiben davon
komplett unberührt sichtbar, nur admin-gepflegte kommen dann halt
(vorübergehend) nicht hinzu. */
const TEAM_API_BASIS = "https://postfach.dogfather-universe.com";
window.dogiTeamLaden = async function (kategorie) {
try {
const url = kategorie ? `${TEAM_API_BASIS}/team?kategorie=${encodeURIComponent(kategorie)}` : `${TEAM_API_BASIS}/team`;
const res = await fetch(url, { cache: "no-store" });
const data = await res.json();
if (!data || !data.ok || !Array.isArray(data.mitglieder)) return [];
return data.mitglieder.map((m) => ({
name: m.name,
slug: m.slug,
kategorie: m.kategorie,
role: m.rolle || "",
photo: m.foto || "",
avatar: m.foto || "",
// 21.08.2026 (Runde 2, "wenn ich eine änderung mache ist es auch
// für jeden"): volle Feld-Parität mit den von Hand gepflegten
// Profilen, damit ein importiertes/bearbeitetes Profil genau so
// reichhaltig sein kann wie die schon bestehenden.
intro: m.intro || "",
bio: m.bio || "",
bioHtml: m.bioHtml || "",
quote: m.zitat || "",
social: m.tiktok ? { tiktok: m.tiktok } : {},
status: "aktiv", // /team liefert ohnehin nur aktive Mitglieder
rank: m.rang || undefined,
rankLabel: m.rangLabel || undefined,
extraCta: m.extraCta || undefined,
// Markiert admin-gepflegte Einträge (z.B. für profil.html, das
// sonst nicht zwischen fest einprogrammierten und dynamischen
// Personen unterscheiden könnte).
istAdminGepflegt: true,
}));
} catch {
return [];
}
};
/* 21.08.2026 (Runde 2, Nutzer-Wunsch: "wenn ich eine änderung mache ist
es auch für jeden für die die schon da sind und die neuen"): fügt eine
per dogiTeamLaden() geladene Liste NICHT einfach hinten an die von Hand
gepflegte Liste an, sondern ÜBERSCHREIBT darin jeden Eintrag mit
demselben slug. So zeigt die Seite nach einem Import + einer
Bearbeitung in der Verwaltung automatisch den aktuellen (DB-)Stand
statt der alten, einprogrammierten Version -- ist das Backend mal
nicht erreichbar, bleibt wenigstens die statische Version sichtbar
(nichts verschwindet einfach). */
window.dogiTeamZusammenfuehren = function (statische, neue) {
if (!neue || !neue.length) return statische;
const neueSlugs = new Set(neue.map((n) => n.slug));
return [...statische.filter((s) => !neueSlugs.has(s.slug)), ...neue];
};
/* 21.08.2026 (Nutzer-Wunsch: "ich will ich titel und namen von
kategorien und seiten ändern können in der verwaltung"): lädt einmalig
alle in der Verwaltung gesetzten Text-Overrides (siehe
server-internal/routes/site-texts.js) und ruft applyTranslations()
danach erneut auf, damit gesetzte Overrides die eingebauten
Standardtexte sofort ersetzen. Ohne Overrides (Normalfall) ändert sich
einfach nichts -- die Seite zeigt weiterhin ihren gewohnten Text. */
window.dogiSiteTexteLaden = async function () {
try {
const res = await fetch(`${TEAM_API_BASIS}/site-texts`, { cache: "no-store" });
const data = await res.json();
if (data && data.ok) {
window.__siteTexteCache = data.texte || {};
applyTranslations();
}
} catch {
/* kein Override verfügbar -> eingebaute Standardtexte bleiben stehen */
}
};
/* =================================================================== /* ===================================================================
PWA: Service-Worker-Registrierung PWA: Service-Worker-Registrierung
Voraussetzung dafür, dass Chrome/Edge auf dem Desktop "App Voraussetzung dafür, dass Chrome/Edge auf dem Desktop "App
@@ -1115,4 +1357,47 @@
appleTitel.content = "DogFather"; appleTitel.content = "DogFather";
document.head.appendChild(appleTitel); document.head.appendChild(appleTitel);
} }
/* ===================================================================
PWA: sichtbarer "App installieren"-Knopf (20.08.2026)
Nutzer-Wunsch: "ich kann sie immer noch nicht runter laden" — reine
Installierbarkeit über Manifest+Service-Worker reicht nicht, wenn
niemand weiß, WO im Browser man das anklickt (verstecktes
Adressleisten-Symbol/3-Punkte-Menü). Jede Seite mit einem Element
#pwa-install-btn (siehe renderFooter() oben + verwaltung.html) bekommt
jetzt einen echten, sichtbaren Knopf, der den nativen Install-Dialog
direkt auslöst — funktioniert pro Seite mit GENAU dem Manifest, das
diese Seite selbst verlinkt (index.html -> manifest.json,
verwaltung.html -> manifest-verwaltung.json), ganz ohne Sonderfall-Code
hier. Zeigt sich NUR, wenn der Browser das überhaupt unterstützt UND
die Seite nicht schon als installierte App läuft (Safari/iOS feuert
"beforeinstallprompt" nie — dort bleibt "Zum Home-Bildschirm" über das
Teilen-Menü weiterhin der einzige Weg, das ist eine Apple-Plattform-
Grenze, kein Bug dieser Seite). */
let installEvent = null;
const laeuftSchonAlsApp = () =>
window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true;
window.addEventListener("beforeinstallprompt", (e) => {
e.preventDefault();
installEvent = e;
const btn = document.getElementById("pwa-install-btn");
if (btn && !laeuftSchonAlsApp()) btn.style.display = "";
});
document.addEventListener("click", (e) => {
if (e.target.closest && e.target.closest("#pwa-install-btn") && installEvent) {
installEvent.prompt();
installEvent.userChoice.finally(() => {
installEvent = null;
const btn = document.getElementById("pwa-install-btn");
if (btn) btn.style.display = "none";
});
}
});
window.addEventListener("appinstalled", () => {
const btn = document.getElementById("pwa-install-btn");
if (btn) btn.style.display = "none";
});
})(); })();
+93
View File
@@ -0,0 +1,93 @@
/* =====================================================================
stimmen.js — NUR für stimmen.html (20.08.2026, Nutzer-Wunsch: "eine
richtig geile kachel ... wo die leute mit namen und tiktok namen mir
eine nachricht schreiben können die ich in die verwaltung kriege").
Formular schickt eine Einreichung ab (landet als Entwurf, siehe
server-internal/routes/testimonials.js), zweiter Teil lädt die bereits
freigegebenen Stimmen und zeigt sie an — bleibt komplett unsichtbar,
solange keine einzige freigegeben wurde. ===================================================================== */
(function () {
const API_BASE_URL = "https://postfach.dogfather-universe.com";
const formular = document.getElementById("stimmen-formular");
if (formular) {
const erfolgEl = document.getElementById("stimmen-erfolg");
const fehlerEl = document.getElementById("stimmen-fehler");
formular.addEventListener("submit", async (e) => {
e.preventDefault();
erfolgEl.classList.remove("zeige");
fehlerEl.classList.remove("zeige");
const name = document.getElementById("st-name").value.trim();
const tiktokUsername = document.getElementById("st-tiktok").value.trim();
const nachricht = document.getElementById("st-nachricht").value.trim();
const absendenBtn = formular.querySelector("button[type=submit]");
absendenBtn.disabled = true;
try {
const res = await fetch(`${API_BASE_URL}/testimonials/submit`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, tiktokUsername, nachricht }),
});
const data = await res.json();
if (!data.ok) {
fehlerEl.textContent = data.error || "Einreichen hat leider nicht geklappt. Bitte versuch es nochmal.";
fehlerEl.classList.add("zeige");
return;
}
formular.hidden = true;
erfolgEl.classList.add("zeige");
} catch {
fehlerEl.textContent = "Verbindung zum Server fehlgeschlagen. Bitte versuch es nochmal.";
fehlerEl.classList.add("zeige");
} finally {
absendenBtn.disabled = false;
}
});
}
function escapeHtml(text) {
return String(text ?? "").replace(/[&<>"']/g, (z) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[z]));
}
/* Erstes sichtbares Zeichen des Namens fürs Avatar-Medaillon. [...name]
statt name[0], damit auch Namen, die mit einem Emoji beginnen, nicht
mitten in einem Zeichen abgeschnitten werden (ein Emoji besteht in
JavaScript aus mehreren "Zeichen" — name[0] lieferte sonst nur ein
kaputtes Halb-Zeichen). */
function erstesZeichen(name) {
const zeichen = [...String(name || "").trim()];
return zeichen.length ? zeichen[0].toUpperCase() : "?";
}
function ladeEchteStimmen() {
fetch(`${API_BASE_URL}/testimonials`, { cache: "no-store" })
.then((res) => res.json())
.then((data) => {
const liste = data && data.ok ? data.testimonials : [];
if (!liste.length) return; // Abschnitt bleibt hidden, kein Platzhalter mehr
const root = document.getElementById("stimmen-echte-root");
const grid = document.getElementById("stimmen-echte-grid");
grid.innerHTML = liste
.map(
(t) => `<div class="stimmen-karte">
<p class="stimmen-karte-text">${escapeHtml(t.nachricht)}</p>
<div class="stimmen-karte-fuss">
<span class="stimmen-karte-avatar" aria-hidden="true">${escapeHtml(erstesZeichen(t.name))}</span>
<span class="stimmen-karte-namen">
<span class="stimmen-karte-name">${escapeHtml(t.name)}</span>
${t.tiktokUsername ? `<span class="stimmen-karte-tiktok">@${escapeHtml(t.tiktokUsername)}</span>` : ""}
</span>
</div>
</div>`
)
.join("");
root.hidden = false;
})
.catch(() => {
/* Abschnitt bleibt einfach unsichtbar -- kein Fehler nötig. */
});
}
ladeEchteStimmen();
})();
+251
View File
@@ -0,0 +1,251 @@
/* =====================================================================
streamplan.js — "Live-Radar" NUR für streamplan.html (20.08.2026,
Nutzer-Wunsch: "soll viel geiler und spezieller sein, überrasch mich",
ergänzt um eine zweite Uhr für ein "spezielles Live-Event": "sehr
wichtig ist dass ich auch spezielle live events da auch eintragen kann
in der verwaltungsseite so dass da eine zweite uhr erscheint wo die
zeit dann für dieses spezielle event läuft").
Zwei 24-Stunden-Ziffernblätter (reines SVG, keine Bild-Assets nötig):
- Uhr 1 (immer sichtbar): der feste 20-Uhr-Termin leuchtet dauerhaft als
Fixpunkt, ein "Jetzt"-Zeiger bewegt sich mit der echten Uhrzeit der
besuchenden Person mit. Live laut /live-status -> Rot/Puls +
"Jetzt anschauen".
- Uhr 2 (nur wenn in der Verwaltung aktiviert): zählt zu einem exakten
Zieldatum+Uhrzeit runter (Tage möglich, nicht nur Stunden), Magenta/
Violett statt Cyan/Gold, damit beide Uhren auf einen Blick
unterscheidbar sind ("bissl Farbe"). ===================================================================== */
(function () {
const API_BASE_URL = "https://postfach.dogfather-universe.com";
const SVG_NS = "http://www.w3.org/2000/svg";
const CX = 150, CY = 150, R_OUT = 130, R_TICK_IN = 116, R_TICK_MAJOR_IN = 106, R_HAND = 100;
function polar(r, stundenAnteil) {
const winkel = (stundenAnteil / 24) * 360 - 90; // 0 Uhr oben, im Uhrzeigersinn
const rad = (winkel * Math.PI) / 180;
return { x: CX + r * Math.cos(rad), y: CY + r * Math.sin(rad) };
}
/** Zeichnet die 24 Stundenstriche + einen Fixpunkt-Leuchtpunkt bei
"fixStunde" (kann eine Dezimalzahl sein, z.B. 19.5 = 19:30) in die
übergebenen <g>-Container. Läuft einmalig pro Uhr. */
function zeichneZiffernblatt(ticksHost, fixHost, fixStunde) {
ticksHost.innerHTML = "";
for (let h = 0; h < 24; h++) {
const major = h % 6 === 0;
const p1 = polar(R_OUT, h);
const p2 = polar(major ? R_TICK_MAJOR_IN : R_TICK_IN, h);
const line = document.createElementNS(SVG_NS, "line");
line.setAttribute("x1", p1.x); line.setAttribute("y1", p1.y);
line.setAttribute("x2", p2.x); line.setAttribute("y2", p2.y);
line.setAttribute("class", major ? "radar-tick radar-tick-major" : "radar-tick");
ticksHost.appendChild(line);
}
fixHost.innerHTML = "";
if (fixStunde != null) {
const fixPos = polar(R_OUT + 11, fixStunde);
const fixDot = document.createElementNS(SVG_NS, "circle");
fixDot.setAttribute("cx", fixPos.x); fixDot.setAttribute("cy", fixPos.y);
fixDot.setAttribute("r", 6.5);
fixDot.setAttribute("class", "radar-fixmarker-dot");
fixHost.appendChild(fixDot);
}
}
function zeigerAktualisieren(handEl, dotEl, stundenAnteil) {
const spitze = polar(R_HAND, stundenAnteil);
handEl.setAttribute("x2", spitze.x); handEl.setAttribute("y2", spitze.y);
dotEl.setAttribute("cx", spitze.x); dotEl.setAttribute("cy", spitze.y);
}
function zweistellig(n) { return String(n).padStart(2, "0"); }
/* ===================================================================
Uhr 1: täglicher 20-Uhr-Fixtermin (bereits bestehend, unverändert im
Verhalten) ================================================================= */
const radar = document.getElementById("stream-radar");
if (radar) {
const FIX_STUNDE = 20;
zeichneZiffernblatt(document.getElementById("radar-ticks"), document.getElementById("radar-fixmarker"), FIX_STUNDE);
const nowHand = document.getElementById("radar-now-hand");
const nowDot = document.getElementById("radar-now-dot");
function zeigerTick() {
const jetzt = new Date();
zeigerAktualisieren(nowHand, nowDot, jetzt.getHours() + jetzt.getMinutes() / 60 + jetzt.getSeconds() / 3600);
}
zeigerTick();
setInterval(zeigerTick, 15000);
function naechsterFixTermin() {
const jetzt = new Date();
const ziel = new Date(jetzt);
ziel.setHours(FIX_STUNDE, 0, 0, 0);
if (ziel <= jetzt) ziel.setDate(ziel.getDate() + 1);
return ziel;
}
const countdownEl = document.getElementById("radar-countdown");
const statusText = document.getElementById("radar-status-text");
const statusSub = document.getElementById("radar-status-sub");
const watchBtn = document.getElementById("radar-watch-btn");
let istLive = false;
let letzterTitel = null;
function countdownTick() {
if (istLive) return;
const diffMs = naechsterFixTermin() - new Date();
const gesamtSek = Math.max(0, Math.floor(diffMs / 1000));
const std = Math.floor(gesamtSek / 3600);
const min = Math.floor((gesamtSek % 3600) / 60);
const sek = gesamtSek % 60;
countdownEl.textContent = `${zweistellig(std)}:${zweistellig(min)}:${zweistellig(sek)}`;
}
setInterval(countdownTick, 1000);
function texteNeuSetzen() {
const t = window.dogiUebersetzen || ((k) => k);
if (istLive) {
statusText.textContent = t("sp_radar_live");
countdownEl.textContent = letzterTitel || t("sp_radar_live_generic");
statusSub.textContent = "";
watchBtn.hidden = false;
} else {
statusText.textContent = t("sp_radar_offline");
statusSub.textContent = t("sp_radar_countdown_label");
watchBtn.hidden = true;
countdownTick();
}
}
function liveStatusAnwenden(data) {
istLive = !!(data && data.live);
letzterTitel = data && data.title ? data.title : null;
radar.classList.toggle("ist-live", istLive);
texteNeuSetzen();
}
function liveStatusLaden() {
return fetch(`${API_BASE_URL}/live-status`, { cache: "no-store" })
.then((res) => res.json())
.then(liveStatusAnwenden)
.catch(() => {
istLive = false;
radar.classList.remove("ist-live");
texteNeuSetzen();
});
}
liveStatusLaden();
setInterval(liveStatusLaden, 30000);
document.addEventListener("visibilitychange", () => { if (!document.hidden) liveStatusLaden(); });
document.addEventListener("dogi-sprache-geaendert", texteNeuSetzen);
}
/* ===================================================================
Uhr 2: "spezielles Live-Event" aus der Verwaltung (20.08.2026, neu).
Nur sichtbar, wenn /special-event ein aktives, noch bevorstehendes
Event liefert. Zieluhrzeit kann Tage entfernt sein -> Countdown zeigt
bei Bedarf "Xd HH:MM:SS" statt nur HH:MM:SS. ================================================================= */
const seWrap = document.getElementById("se-radar-wrap");
if (seWrap) {
const seRadar = document.getElementById("se-radar");
const seNowHand = document.getElementById("se-radar-now-hand");
const seNowDot = document.getElementById("se-radar-now-dot");
const seCountdown = document.getElementById("se-radar-countdown");
const seTitel = document.getElementById("se-radar-titel");
const seLink = document.getElementById("se-radar-link");
const seTeilenBtn = document.getElementById("se-radar-teilen");
let zielDatum = null;
let aktuellesEvent = null; // für dogiTeilen() beim Klick (21.08.2026)
let countdownIntervalId = null;
let zeigerIntervalId = null;
function seCountdownTick() {
if (!zielDatum) return;
const diffMs = zielDatum - new Date();
if (diffMs <= 0) {
seRadar.classList.add("ist-live");
seCountdown.textContent = window.dogiUebersetzen ? window.dogiUebersetzen("sp_radar_live_generic") : "Live!";
clearInterval(countdownIntervalId);
return;
}
const gesamtSek = Math.floor(diffMs / 1000);
const tage = Math.floor(gesamtSek / 86400);
const std = Math.floor((gesamtSek % 86400) / 3600);
const min = Math.floor((gesamtSek % 3600) / 60);
const sek = gesamtSek % 60;
seCountdown.textContent = tage > 0
? `${tage}T ${zweistellig(std)}:${zweistellig(min)}:${zweistellig(sek)}`
: `${zweistellig(std)}:${zweistellig(min)}:${zweistellig(sek)}`;
}
// Der "Jetzt"-Zeiger zeigt wie bei Uhr 1 die ECHTE aktuelle Uhrzeit --
// der magentafarbene Fixpunkt markiert dagegen die Tageszeit, zu der
// das spezielle Event stattfindet. So bleibt die Bildsprache zwischen
// beiden Uhren konsistent (nur der Fixpunkt unterscheidet sich).
function seZeigerTick() {
const jetzt = new Date();
zeigerAktualisieren(seNowHand, seNowDot, jetzt.getHours() + jetzt.getMinutes() / 60 + jetzt.getSeconds() / 3600);
}
function specialEventLaden() {
return fetch(`${API_BASE_URL}/special-event`, { cache: "no-store" })
.then((res) => res.json())
.then((data) => {
const event = data && data.ok ? data.event : null;
if (countdownIntervalId) clearInterval(countdownIntervalId);
if (zeigerIntervalId) clearInterval(zeigerIntervalId);
if (!event) {
seWrap.hidden = true;
zielDatum = null;
return;
}
zielDatum = new Date(event.zieldatum);
aktuellesEvent = event;
seWrap.hidden = false;
seRadar.classList.remove("ist-live");
seTitel.textContent = window.dogiFeld ? window.dogiFeld(event.titel) : event.titel?.de || "";
zeichneZiffernblatt(document.getElementById("se-radar-ticks"), document.getElementById("se-radar-fixmarker"), zielDatum.getHours() + zielDatum.getMinutes() / 60);
seZeigerTick();
seCountdownTick();
zeigerIntervalId = setInterval(seZeigerTick, 15000);
countdownIntervalId = setInterval(seCountdownTick, 1000);
if (event.link) {
seLink.href = event.link;
seLink.hidden = false;
} else {
seLink.hidden = true;
}
})
.catch(() => {
seWrap.hidden = true;
});
}
specialEventLaden();
setInterval(specialEventLaden, 60000); // reicht -- Aenderungen in der Verwaltung muessen nicht sekundengenau ankommen
document.addEventListener("dogi-sprache-geaendert", () => {
if (zielDatum) specialEventLaden();
});
// Teilen (21.08.2026, Nutzer-Wunsch: "... speziellen live events soll man
// auch teilen können") -- teilt die Streamplan-Seite selbst (dort steht
// ja bereits automatisch das aktuell aktive Event), zusammen mit Titel
// im Text, damit klar ist, worum es geht. Nur EIN spezielles Event kann
// je aktiv sein, deshalb reicht ein einziger Knopf ohne Slot-Bezug.
seTeilenBtn?.addEventListener("click", () => {
if (!aktuellesEvent) return;
const titel = window.dogiFeld ? window.dogiFeld(aktuellesEvent.titel) : aktuellesEvent.titel?.de || "";
window.dogiTeilen?.(
{
title: titel,
text: `🔴 ${titel} — spezielles Live-Event bei DogFather!`,
url: `${location.origin}/streamplan.html`,
},
seTeilenBtn
);
});
}
})();
+7 -7
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Du bist Streamer und suchst einen Manager? Bewirb dich als Creator bei Spicy Media." /> <meta name="twitter:description" content="Du bist Streamer und suchst einen Manager? Bewirb dich als Creator bei Spicy Media." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben-creator.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-creator-mobile.jpg');"> <body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben-creator.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-creator-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -70,8 +70,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/forms.js"></script> <script src="assets/js/forms.js?v=20260821o"></script>
<script src="assets/js/i18n-bewerben-creator.js"></script> <script src="assets/js/i18n-bewerben-creator.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+11 -8
View File
@@ -1,6 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de" data-theme="spicymedia"> <html lang="de" data-theme="dogfather">
<head> <head>
<!-- Nutzer-Wunsch 20.08.2026: neues Hero-Bild (Hasi Dog/DogFather/Husky, babyblau statt
rot) + Theme von "spicymedia" (Rot/Orange) auf "dogfather" (Babyblau/Lila) umgestellt,
damit Knopf-/Link-/Eyebrow-Farben zum neuen, blauen Bild passen. -->
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Kooperation anfragen — DOGFATHER UNIVERSE</title> <title>Kooperation anfragen — DOGFATHER UNIVERSE</title>
@@ -19,10 +22,10 @@
<meta name="twitter:description" content="Kooperationsanfrage an Manager DogFather / Spicy Media." /> <meta name="twitter:description" content="Kooperationsanfrage an Manager DogFather / Spicy Media." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-mobile.jpg');"> <body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -65,8 +68,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/forms.js"></script> <script src="assets/js/forms.js?v=20260821o"></script>
<script src="assets/js/i18n-bewerben-kooperation.js"></script> <script src="assets/js/i18n-bewerben-kooperation.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+9 -9
View File
@@ -19,17 +19,17 @@
<meta name="twitter:description" content="Werde Modi bei Team Dogi — DogFathers persönliches Community-Team." /> <meta name="twitter:description" content="Werde Modi bei Team Dogi — DogFathers persönliches Community-Team." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-dogfather.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-mobile.jpg');"> <body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben-modi.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-modi-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
<main> <main>
<section class="hero page-bg"> <section class="hero page-bg">
<div class="hero-banner"><img src="/assets/img/bg-dogfather.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-bewerben-modi.jpg" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="bwm_eyebrow">🩵 Team Dogi — Community</span> <span class="eyebrow" data-i18n="bwm_eyebrow">🩵 Team Dogi — Community</span>
<h1 data-i18n="bwm_h1">Werde Modi bei Team Dogi</h1> <h1 data-i18n="bwm_h1">Werde Modi bei Team Dogi</h1>
@@ -79,8 +79,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/forms.js"></script> <script src="assets/js/forms.js?v=20260821o"></script>
<script src="assets/js/i18n-bewerben-modi.js"></script> <script src="assets/js/i18n-bewerben-modi.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+9 -9
View File
@@ -19,17 +19,17 @@
<meta name="twitter:description" content="Werde Scout für DogFather bei der Spicy Media Agentur — finde neue Talente." /> <meta name="twitter:description" content="Werde Scout für DogFather bei der Spicy Media Agentur — finde neue Talente." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-scouts.jpg');--page-bg-mobile:url('/assets/img/bg-scouts-mobile.jpg');"> <body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben-scout.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-scout-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
<main> <main>
<section class="hero page-bg"> <section class="hero page-bg">
<div class="hero-banner"><img src="/assets/img/bg-scouts.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-bewerben-scout.jpg" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="bws_eyebrow">Manager DogFather — Agentur</span> <span class="eyebrow" data-i18n="bws_eyebrow">Manager DogFather — Agentur</span>
<h1 data-i18n="bws_h1">Werde Scout für DogFather</h1> <h1 data-i18n="bws_h1">Werde Scout für DogFather</h1>
@@ -71,8 +71,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/forms.js"></script> <script src="assets/js/forms.js?v=20260821o"></script>
<script src="assets/js/i18n-bewerben-scout.js"></script> <script src="assets/js/i18n-bewerben-scout.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+7 -7
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Werde Teil vom Team: Modi bei Team Dogi, Scout oder Creator bei Spicy Media, oder Kooperation anfragen." /> <meta name="twitter:description" content="Werde Teil vom Team: Modi bei Team Dogi, Scout oder Creator bei Spicy Media, oder Kooperation anfragen." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben-split.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-split-mobile.jpg');"> <body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben-split.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-split-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -32,7 +32,7 @@
<div class="hero-banner"><img src="/assets/img/bg-bewerben-split.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-bewerben-split.jpg" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="bw_eyebrow">Werde Teil vom Team</span> <span class="eyebrow" data-i18n="bw_eyebrow">Werde Teil vom Team</span>
<h1 data-i18n="bw_h1">Community oder Agentur — such dir deinen Weg</h1> <h1 data-i18n="bw_h1">Community oder Agentur — such dir deinen&nbsp;Weg</h1>
<p class="lead worlds" data-i18n="bw_lead">Vier Bewerbungen, drei ganz unterschiedliche Rollen. Wähl, was zu dir passt.</p> <p class="lead worlds" data-i18n="bw_lead">Vier Bewerbungen, drei ganz unterschiedliche Rollen. Wähl, was zu dir passt.</p>
</div> </div>
</section> </section>
@@ -85,7 +85,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-bewerben.js"></script> <script src="assets/js/i18n-bewerben.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Der Husky an DogFathers Seite — das tierische Gesicht der HasiDog-Welt." /> <meta name="twitter:description" content="Der Husky an DogFathers Seite — das tierische Gesicht der HasiDog-Welt." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-casper.jpg');--page-bg-mobile:url('/assets/img/bg-casper-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-casper.jpg');--page-bg-mobile:url('/assets/img/bg-casper-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -85,7 +85,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-casper.js"></script> <script src="assets/js/i18n-casper.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
@@ -0,0 +1,32 @@
-- =====================================================================
-- Supporter-Abstimmungen (20.08.2026) — "nächste Ausbaustufe" aus
-- Dogfather_VanVan_Supporter_Abo.odt Abschnitt 20 (Bonus-Features, dort
-- bewusst nicht in den verbindlichen Abnahmekriterien Abschnitt 34, siehe
-- Supporter-Abo-System.md "Bewusst nicht Teil dieser ersten Ausbaustufe").
-- Dogi erstellt in der Verwaltung eine Frage + Antwortoptionen auf Deutsch,
-- die anderen Sprachen werden wie bei "Event des Jahres" automatisch beim
-- Speichern übersetzt (lib/translate.js). Jeder DogiCrew-Supporter darf pro
-- Abstimmung genau einmal abstimmen (UNIQUE-Constraint statt Anwendungslogik
-- — robust auch bei gleichzeitigen Anfragen).
-- =====================================================================
CREATE TABLE IF NOT EXISTS supporter_polls (
id TEXT PRIMARY KEY,
question TEXT NOT NULL, -- JSON {de, "de-CH", en, fr, pt}
options TEXT NOT NULL, -- JSON-Array von {de, "de-CH", en, fr, pt}
status TEXT NOT NULL DEFAULT 'active', -- active | closed
created_at TEXT NOT NULL,
created_by TEXT NOT NULL,
closed_at TEXT
);
CREATE TABLE IF NOT EXISTS supporter_poll_votes (
id TEXT PRIMARY KEY,
poll_id TEXT NOT NULL REFERENCES supporter_polls(id),
supporter_id TEXT NOT NULL REFERENCES supporter_users(id),
option_index INTEGER NOT NULL,
voted_at TEXT NOT NULL,
UNIQUE(poll_id, supporter_id)
);
CREATE INDEX IF NOT EXISTS idx_supporter_poll_votes_poll ON supporter_poll_votes (poll_id);
@@ -0,0 +1,23 @@
-- =====================================================================
-- "Stimmen" / Testimonials (20.08.2026) — Fans können auf stimmen.html
-- selbst eine Nachricht mit Name + TikTok-Namen einreichen. Landet erst
-- als Entwurf in der Verwaltung ("eingereicht") -- öffentlich sichtbar
-- wird ein Eintrag NUR, wenn Dogi/VanVan ihn dort ausdrücklich freigeben
-- (Nutzer-Wunsch: "die ausgewählten über die verwaltungsseite auf die
-- website hinzufügen"). Eigene Tabelle statt app_settings, weil dies eine
-- WACHSENDE Liste ist (beliebig viele Einreichungen), keine feste Anzahl
-- Slots wie "Event des Jahres".
-- =====================================================================
CREATE TABLE IF NOT EXISTS testimonials (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
tiktok_username TEXT,
nachricht TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'eingereicht', -- eingereicht | freigegeben | abgelehnt
created_at TEXT NOT NULL,
entschieden_am TEXT,
entschieden_von TEXT
);
CREATE INDEX IF NOT EXISTS idx_testimonials_status ON testimonials (status, created_at);
@@ -0,0 +1,44 @@
-- =====================================================================
-- Team-Mitglieder (21.08.2026) — Nutzer-Wunsch: "überall wo Modis sind
-- oder Scouts oder Manager, oder Creator, will ich dass ich die easy über
-- meine Verwaltungsseite hinzufüge und die automatisch in der Website
-- hinzugefügt werden ... auch mit Fotos und TikTok Link."
--
-- Eine gemeinsame Tabelle für alle vier Kategorien (statt vier eigenen),
-- weil sie strukturell identisch sind (Name/Rolle/Foto/TikTok/Bio/Zitat) --
-- "kategorie" unterscheidet, wo jemand öffentlich erscheint. rang/
-- rang_label sind nur für Modis relevant (Pyramiden-Hierarchie auf
-- team-modis.html), bei den anderen Kategorien bleiben sie einfach leer.
--
-- Ergänzt die weiterhin bestehenden, von Hand gepflegten Einträge in
-- assets/js/data-modis.js/data-scouts.js/data-creator.js/data-manager.js
-- (z.B. VanVan, Patrick, Bananenstift) -- die Website mischt beide Quellen
-- zu einer gemeinsamen Liste (siehe window.dogiTeamLaden() in main.js).
-- Ein Fehler bei einer Kategorie kann so nie die schon bestehenden,
-- fest einprogrammierten Profile verschwinden lassen.
--
-- slug ist GLOBAL eindeutig (nicht nur pro Kategorie), weil profil.html
-- über alle Kategorien hinweg nach genau einem slug sucht.
-- =====================================================================
CREATE TABLE IF NOT EXISTS team_members (
id TEXT PRIMARY KEY,
kategorie TEXT NOT NULL, -- modi | scout | creator | manager
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
rolle_json TEXT NOT NULL, -- {de, "de-CH", en, fr, pt}
foto_url TEXT,
tiktok_url TEXT,
bio_json TEXT, -- {de, "de-CH", en, fr, pt} oder NULL
zitat_json TEXT, -- {de, "de-CH", en, fr, pt} oder NULL
rang INTEGER, -- nur Modi-Pyramide, sonst NULL
rang_label_json TEXT, -- nur Modi-Pyramide, sonst NULL
reihenfolge INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'aktiv', -- aktiv | pausiert
erstellt_am TEXT NOT NULL,
geaendert_am TEXT NOT NULL,
erstellt_von TEXT,
geaendert_von TEXT
);
CREATE INDEX IF NOT EXISTS idx_team_members_kategorie ON team_members (kategorie, status, reihenfolge);
@@ -0,0 +1,18 @@
-- =====================================================================
-- Team-Mitglieder: volle Feld-Parität mit den von Hand gepflegten
-- Profilen (21.08.2026, Nutzer-Wunsch: "wenn ich eine änderung mache
-- ist es auch für jeden für die die schon da sind und die neuen").
--
-- Damit ein bestehendes Profil (Patrick, Bananenstift, Diene, ...) 1:1
-- in die Verwaltung importiert werden kann, OHNE Inhalt zu verlieren,
-- braucht team_members dieselben optionalen Felder wie
-- data-modis.js/data-scouts.js: intro (kurzer Text neben dem Foto),
-- bio_html (ausführliche, mehrabsatzige Vorstellung mit Überschriften),
-- extra_cta (zweiter Button auf der Profilkarte, aktuell nur bei Marina
-- -> manager.html genutzt).
-- =====================================================================
ALTER TABLE team_members ADD COLUMN intro_json TEXT;
ALTER TABLE team_members ADD COLUMN bio_html_json TEXT;
ALTER TABLE team_members ADD COLUMN extra_cta_href TEXT;
ALTER TABLE team_members ADD COLUMN extra_cta_label_json TEXT;
@@ -0,0 +1,27 @@
-- =====================================================================
-- 0009_supporter_access_code.sql — Eigener, fester Zugangscode je Supporter
--
-- Nutzer-Wunsch 21.08.2026: "wieso sollte ich wieder ein code bekommen,
-- mach die anmeldung einfach, die leute sollen nach der registrierung einen
-- festen code erstellen können für sich selbst und fertig. mit dem sie rein
-- kommen."
--
-- Bis hierher lief JEDER Login über einen 6-stelligen Einmalcode per E-Mail
-- (supporter_auth_codes) — auch für längst registrierte Leute. Ab jetzt legt
-- man bei der Registrierung einen eigenen, dauerhaften Zugangscode fest und
-- kommt damit direkt rein.
--
-- Gespeichert wird NIE der Code selbst, sondern ein scrypt-Hash inkl.
-- zufälligem Salt (Format "scrypt$<salt-hex>$<hash-hex>", siehe
-- lib/crypto.js). Damit ist der Code selbst aus der Datenbank heraus nicht
-- rekonstruierbar — auch nicht von uns.
--
-- Der E-Mail-Einmalcode-Weg bleibt vollständig erhalten: er ist jetzt der
-- "Code vergessen?"-Weg, über den man sich einen neuen Zugangscode setzen
-- kann. Deshalb wird an supporter_auth_codes nichts geändert.
--
-- NULL = Konto aus der Zeit vor dieser Änderung, hat noch keinen eigenen
-- Code. Solche Konten kommen über "Code vergessen?" an einen.
-- =====================================================================
ALTER TABLE supporter_users ADD COLUMN access_code_hash TEXT;
ALTER TABLE supporter_users ADD COLUMN access_code_set_at TEXT;
@@ -0,0 +1,23 @@
-- =====================================================================
-- 0010_testimonials_avatar.sql — Profilbild für die "Stimmen"
--
-- Nutzer-Wunsch 21.08.2026: "ich soll auch paar geile bilder zum aussuchen
-- haben als profilbild falls die leute kein bild mit schicken, das soll
-- denen auch als möglichkeit gegeben sein."
--
-- Bis hierher zeigte jede Stimme nur den Anfangsbuchstaben des Namens in
-- einem Kreis. Jetzt kann ein Bild hinterlegt werden.
--
-- Inhalt der Spalte:
-- * eine Auswahlbild-Id ("husky-hoodie", "casper", …) — siehe
-- ERLAUBTE_AVATARE in server-internal/routes/testimonials.js und
-- assets/js/data-stimmen-avatare.js
-- * ODER eine vollständige Adresse eines von der Verwaltung
-- hochgeladenen eigenen Fotos
-- * ODER NULL = kein Bild, dann bleibt es beim Anfangsbuchstaben
--
-- Bewusst nur EINE Spalte statt zweier (Auswahlbild/eigenes Foto): es kann
-- ohnehin immer nur eines von beiden gleichzeitig gelten, zwei Spalten
-- hätten nur die Frage aufgeworfen, welche gewinnt.
-- =====================================================================
ALTER TABLE testimonials ADD COLUMN avatar TEXT;
+8 -8
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Team Dogi ist Familie — die Community rund um DogFather und HasiDog." /> <meta name="twitter:description" content="Team Dogi ist Familie — die Community rund um DogFather und HasiDog." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-community.jpg');--page-bg-mobile:url('/assets/img/bg-community-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-community.jpg');--page-bg-mobile:url('/assets/img/bg-community-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -45,8 +45,8 @@
<p data-i18n="com_mehr_2">Discord ist das Community-Zentrum — hier trifft man sich, tauscht sich aus und ist Teil des Alltags von Team Dogi.</p> <p data-i18n="com_mehr_2">Discord ist das Community-Zentrum — hier trifft man sich, tauscht sich aus und ist Teil des Alltags von Team Dogi.</p>
<a class="btn btn-primary" href="links.html" data-i18n="com_discord_btn">Discord beitreten</a> <a class="btn btn-primary" href="links.html" data-i18n="com_discord_btn">Discord beitreten</a>
</div> </div>
<div class="img-placeholder" style="aspect-ratio:4/3;"> <div class="collage-photo" style="aspect-ratio:3/4;border-radius:var(--radius);">
<span data-i18n="com_bild_platzhalter">Community-Bild</span><br><span class="small" data-i18n="com_foto_folgt">Foto folgt</span> <img src="assets/img/community-team-dogi.jpg" alt="Diene, Ghost, VanVan und Dogi bei einem gemeinsamen Treffen von Team Dogi" style="width:100%;height:100%;object-fit:cover;object-position:center 25%;" />
</div> </div>
</div> </div>
</section> </section>
@@ -84,7 +84,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-community.js"></script> <script src="assets/js/i18n-community.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+31 -10
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Content Creator, die Teil von DogFather / Spicy Media sind." /> <meta name="twitter:description" content="Content Creator, die Teil von DogFather / Spicy Media sind." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-creator.jpg');--page-bg-mobile:url('/assets/img/bg-creator-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-creator.jpg');--page-bg-mobile:url('/assets/img/bg-creator-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -32,7 +32,7 @@
<div class="hero-banner"><img src="/assets/img/bg-creator.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-creator.jpg" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="crt_eyebrow">Manager DogFather — Team</span> <span class="eyebrow" data-i18n="crt_eyebrow">Manager DogFather — Team</span>
<h1 data-i18n="crt_h1">Creator im Team</h1> <h1 data-i18n="crt_h1" data-site-text-key="seitentitel_creator">Creator im Team</h1>
<p class="lead worlds" data-i18n="crt_lead">Content Creator, die bei DogFather / Spicy Media mitmachen.</p> <p class="lead worlds" data-i18n="crt_lead">Content Creator, die bei DogFather / Spicy Media mitmachen.</p>
</div> </div>
</section> </section>
@@ -49,6 +49,7 @@
<span data-i18n="crt_empty">Noch keine Creator-Profile veröffentlicht — bald verfügbar. 🩵</span> <span data-i18n="crt_empty">Noch keine Creator-Profile veröffentlicht — bald verfügbar. 🩵</span>
<span data-i18n="crt_filter_alle">alle</span> <span data-i18n="crt_filter_alle">alle</span>
<span data-i18n="crt_profil_btn">Profil ansehen</span> <span data-i18n="crt_profil_btn">Profil ansehen</span>
<span data-i18n="crt_kategorie_sonstige">Sonstige</span>
</div> </div>
<!-- TODO: Noch keine Creator-Profile final geklärt. Sobald Namen/Infos feststehen, in assets/js/data-creator.js ergänzen — diese Seite rendert automatisch. --> <!-- TODO: Noch keine Creator-Profile final geklärt. Sobald Namen/Infos feststehen, in assets/js/data-creator.js ergänzen — diese Seite rendert automatisch. -->
<div class="btn-row" style="justify-content:center;"> <div class="btn-row" style="justify-content:center;">
@@ -59,14 +60,29 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-creator.js"></script> <script src="assets/js/data-creator.js?v=20260821o"></script>
<script src="assets/js/i18n-creator.js"></script> <script src="assets/js/i18n-creator.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", async () => {
const grid = document.getElementById("creator-grid"); const grid = document.getElementById("creator-grid");
const filterBar = document.getElementById("creator-filters"); const filterBar = document.getElementById("creator-filters");
const list = window.CREATORS || []; let list = window.CREATORS || [];
// 21.08.2026: über die Verwaltung angelegte Creator ergänzen, BEVOR
// entschieden wird, ob die Seite "leer" ist (Nutzer-Wunsch "überall wo
// ... Creator ... sind, will ich dass ich die easy über meine
// Verwaltungsseite hinzufüge"). Vorher gab es direkt darunter ein
// "return", sobald window.CREATORS leer war (bisher der Normalfall, da
// noch keine echten Creator-Profile feststehen) -- dadurch wären
// admin-gepflegte Creator NIE geladen worden.
if (window.dogiTeamLaden) {
const neue = await window.dogiTeamLaden("creator");
// Runde 2 (21.08.2026, "wenn ich eine änderung mache ist es auch für
// jeden"): überschreibt bereits importierte Creator (gleicher slug)
// statt sie zu duplizieren.
list = window.dogiTeamZusammenfuehren(list, neue);
}
// Texte fürs JS-generierte Grid/Filter kommen aus den versteckten // Texte fürs JS-generierte Grid/Filter kommen aus den versteckten
// data-i18n-Elementen oben — main.js übersetzt sie ganz normal (auch bei // data-i18n-Elementen oben — main.js übersetzt sie ganz normal (auch bei
@@ -88,6 +104,11 @@ document.addEventListener("DOMContentLoaded", () => {
return; return;
} }
// 21.08.2026: über die Verwaltung angelegte Creator haben (noch) kein
// eigenes Genre-Feld (category) -- ohne diesen Fallback würde die
// Filterleiste einen kaputten "undefined"-Knopf zeigen.
list = list.map((c) => (c.category ? c : { ...c, category: uebersetzen("crt_kategorie_sonstige") }));
const categories = ["alle", ...new Set(list.map((c) => c.category))]; const categories = ["alle", ...new Set(list.map((c) => c.category))];
let aktiverFilter = "alle"; let aktiverFilter = "alle";
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Wer ist DogFather wirklich? Persönlichkeit, Werte und die Geschichte hinter dem Content." /> <meta name="twitter:description" content="Wer ist DogFather wirklich? Persönlichkeit, Werte und die Geschichte hinter dem Content." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-dogfather-person.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-person-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-dogfather-person.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-person-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -158,7 +158,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-dogfather.js"></script> <script src="assets/js/i18n-dogfather.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+2 -2
View File
@@ -26,8 +26,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="DogFather" /> <meta name="apple-mobile-web-app-title" content="DogFather" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<style> <style>
/* ===================================================================== /* =====================================================================
Zugangsseite — bewusst kein braves Standard-Login-Formular, sondern Zugangsseite — bewusst kein braves Standard-Login-Formular, sondern
+10 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="HasiDog: Maskottchen, Musikprojekt und die verspielte, familiäre Seite von Team Dogi." /> <meta name="twitter:description" content="HasiDog: Maskottchen, Musikprojekt und die verspielte, familiäre Seite von Team Dogi." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-hasidog.jpg');--page-bg-mobile:url('/assets/img/bg-hasidog-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-hasidog.jpg');--page-bg-mobile:url('/assets/img/bg-hasidog-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -56,6 +56,10 @@
<div class="container grid grid-2" style="align-items:center;"> <div class="container grid grid-2" style="align-items:center;">
<div> <div>
<h2 data-i18n="hd_welt_h2">🐰 HasiDog – Eine eigene Welt voller Chaos, Herz und Fantasie</h2> <h2 data-i18n="hd_welt_h2">🐰 HasiDog – Eine eigene Welt voller Chaos, Herz und Fantasie</h2>
<!-- Eigener TikTok-Account von HasiDog (Nutzer-Wunsch 20.08.2026) -->
<p style="margin:.2rem 0 1.2rem;">
<a class="btn-tiktok" href="https://www.tiktok.com/@hasidog0804" target="_blank" rel="noopener"><span class="btn-tiktok-note" aria-hidden="true">♪</span><span class="btn-tiktok-text" data-i18n="hd_btn_tiktok">Zur HasiDog TikTok-Seite</span></a>
</p>
<p data-i18n="hd_welt_1">HasiDog ist mehr als nur eine Figur. Hinter HasiDog steckt eine eigene Welt aus Humor, Musik, Geschichten, Überraschungen und liebenswertem Chaos. 🩵</p> <p data-i18n="hd_welt_1">HasiDog ist mehr als nur eine Figur. Hinter HasiDog steckt eine eigene Welt aus Humor, Musik, Geschichten, Überraschungen und liebenswertem Chaos. 🩵</p>
<p data-i18n="hd_welt_2">Hier treffen freche Ideen auf große Gefühle, verrückte Momente auf echte Freundschaft und Fantasie auf Abenteuer. HasiDog steht vollkommen für sich – mit einem eigenen Charakter, einer eigenen Geschichte und einer eigenen Familie.</p> <p data-i18n="hd_welt_2">Hier treffen freche Ideen auf große Gefühle, verrückte Momente auf echte Freundschaft und Fantasie auf Abenteuer. HasiDog steht vollkommen für sich – mit einem eigenen Charakter, einer eigenen Geschichte und einer eigenen Familie.</p>
<p class="quote" data-i18n="hd_welt_quote" style="margin-top:1.4rem;">Frech im Kopf. Chaotisch im Herzen. Einzigartig in jeder Geschichte.</p> <p class="quote" data-i18n="hd_welt_quote" style="margin-top:1.4rem;">Frech im Kopf. Chaotisch im Herzen. Einzigartig in jeder Geschichte.</p>
@@ -182,7 +186,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-hasidog.js"></script> <script src="assets/js/i18n-hasidog.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+181 -54
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="DOGFATHER UNIVERSE: die Heimat von DogFather als Streamer, der HasiDog-Community und Manager DogFather bei Spicy Media." /> <meta name="twitter:description" content="DOGFATHER UNIVERSE: die Heimat von DogFather als Streamer, der HasiDog-Community und Manager DogFather bei Spicy Media." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="start-seite"> <body class="start-seite">
<div id="site-header"></div> <div id="site-header"></div>
@@ -81,9 +81,57 @@
<p data-i18n="idx_hero_card3_desc">Als Manager bei Spicy Media: Scouts, Creator und faire Zusammenarbeit auf Augenhöhe.</p> <p data-i18n="idx_hero_card3_desc">Als Manager bei Spicy Media: Scouts, Creator und faire Zusammenarbeit auf Augenhöhe.</p>
</a> </a>
</div> </div>
<!-- "Event des Jahres": volle Breite wie die 3 Welt-Kacheln zusammen. Seit
20.08.2026 (Nutzer-Wunsch) NUR sichtbar, wenn Dogi/VanVan mindestens ein
Event in der Verwaltung ausgefüllt UND aktiviert haben -- kein
Standard-/Platzhalterinhalt mehr, kompletter Abschnitt bleibt bis dahin
unsichtbar (siehe zeigeJahresEvents() weiter unten). Klick öffnet ein
großes Detail-Fenster (Bild/Titel/Text größer, optionaler direkter Link)
statt sofort wegzunavigieren. -->
<div class="jahres-events" id="jahres-events" hidden>
<div class="jahres-events-head">
<span class="tag" data-i18n="idx_jahres_events_tag">🏆 Event des Jahres</span>
</div>
<div class="jahres-events-grid" id="jahres-events-grid">
<button type="button" class="jahres-event-card" id="jahres-event-1" data-slot="1" hidden>
<span class="jahres-event-shine" aria-hidden="true"></span>
<img class="jahres-event-bild" alt="" hidden />
<span class="jahres-event-datum"></span>
<h3></h3>
<p></p>
<span class="jahres-event-mehr" data-i18n="idx_jahres_events_mehr">Mehr erfahren →</span>
</button>
<button type="button" class="jahres-event-card" id="jahres-event-2" data-slot="2" hidden>
<span class="jahres-event-shine" aria-hidden="true"></span>
<img class="jahres-event-bild" alt="" hidden />
<span class="jahres-event-datum"></span>
<h3></h3>
<p></p>
<span class="jahres-event-mehr" data-i18n="idx_jahres_events_mehr">Mehr erfahren →</span>
</button>
</div>
</div>
</div> </div>
</section> </section>
<!-- Detail-Fenster für "Event des Jahres" -- bewusst außerhalb der Hero-Sektion
(die hat eigene transform-/overflow-Effekte für die Polarlicht-/Nebel-
Animation, die ein position:fixed-Fenster aus der Zentrierung bringen
könnten). -->
<div class="jahres-event-modal-overlay" id="jahres-event-modal-overlay" hidden>
<div class="jahres-event-modal" role="dialog" aria-modal="true" aria-labelledby="jahres-event-modal-titel">
<button type="button" class="jahres-event-modal-close" id="jahres-event-modal-close" aria-label="Schließen">✕</button>
<img class="jahres-event-modal-bild" id="jahres-event-modal-bild" alt="" hidden />
<div class="jahres-event-modal-body">
<span class="jahres-event-modal-datum" id="jahres-event-modal-datum"></span>
<h2 id="jahres-event-modal-titel"></h2>
<p id="jahres-event-modal-text"></p>
<a class="btn btn-primary jahres-event-modal-link" id="jahres-event-modal-link" href="#" target="_blank" rel="noopener" hidden></a>
</div>
</div>
</div>
<!-- ================= CHARAKTERE: DOGFATHER-HUSKY & HASIDOG-HASE ================= --> <!-- ================= CHARAKTERE: DOGFATHER-HUSKY & HASIDOG-HASE ================= -->
<section class="section-tight"> <section class="section-tight">
<div class="container text-center"> <div class="container text-center">
@@ -180,23 +228,20 @@
<div class="container"> <div class="container">
<h2 class="text-center" data-i18n="idx_team_h2">Team Dogi 🩵</h2> <h2 class="text-center" data-i18n="idx_team_h2">Team Dogi 🩵</h2>
<p class="lead text-center" data-i18n="idx_team_lead" style="max-width:60ch;margin:0 auto;">VanVan & Modis — die Menschen, die DOGFATHER UNIVERSE tragen.</p> <p class="lead text-center" data-i18n="idx_team_lead" style="max-width:60ch;margin:0 auto;">VanVan & Modis — die Menschen, die DOGFATHER UNIVERSE tragen.</p>
<div id="team-preview" class="grid grid-4"></div> <!-- Nutzer-Wunsch 21.08.2026: die vier Personen-Kacheln (VanVan/Diene/
Funny/Miss) hier auf der Startseite entfernt -- Überschrift, Text
und der Knopf zum Modi-Team bleiben. Die Kacheln selbst gibt es
weiterhin vollständig auf team-modis.html. -->
<div class="btn-row" style="justify-content:center;"> <div class="btn-row" style="justify-content:center;">
<a class="btn btn-outline" href="team-modis.html" data-i18n="idx_team_btn_modi">Modi-Team ansehen</a> <a class="btn btn-outline" href="team-modis.html" data-i18n="idx_team_btn_modi">Modi-Team ansehen</a>
</div> </div>
</div> </div>
</section> </section>
<!-- ================= AKTIONEN TEASER ================= --> <!-- Nutzer-Wunsch 21.08.2026: der "Was gerade läuft"-Aktionen-Teaser
<section> (Überschrift + Button "Alle Aktionen & Projekte", direkt hier davor)
<div class="container"> wurde entfernt. data-aktionen.js/aktion-detail.html bleiben unangetastet,
<h2 class="text-center" data-i18n="idx_aktionen_h2">Was gerade läuft</h2> nur die Startseiten-Vorschau ist weg. -->
<div id="aktionen-teaser" class="grid grid-3"></div>
<div class="btn-row" style="justify-content:center;">
<a class="btn btn-outline" href="medien-dogfather.html#events" data-i18n="idx_aktionen_btn">Alle Aktionen &amp; Projekte</a>
</div>
</div>
</section>
<!-- ================= GALERIE TEASER ================= --> <!-- ================= GALERIE TEASER ================= -->
<section class="section-tight"> <section class="section-tight">
@@ -245,11 +290,10 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-modis.js"></script> <script src="assets/js/data-modis.js?v=20260821o"></script>
<script src="assets/js/data-scouts.js"></script> <script src="assets/js/data-scouts.js?v=20260821o"></script>
<script src="assets/js/data-aktionen.js"></script> <script src="assets/js/i18n-index.js?v=20260821o"></script>
<script src="assets/js/i18n-index.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script src="assets/js/main.js"></script>
<script> <script>
// Gleiche URL wie in assets/js/forms.js / postfach.html — läuft seit // Gleiche URL wie in assets/js/forms.js / postfach.html — läuft seit
// 05.08.2026 auf dem eigenen Server statt auf Cloudflare Workers. // 05.08.2026 auf dem eigenen Server statt auf Cloudflare Workers.
@@ -331,46 +375,129 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
} }
/* "Event des Jahres": seit 20.08.2026 in der Verwaltung pflegbar (Bild,
Text, Link, Aktiv-Schalter je Slot, siehe verwaltung.html +
server-internal/routes/events.js). Nutzer-Wunsch 20.08.2026: "die
kisten sollen leer sein solang wie ich nichts rein setze, am besten
sollen die sogar immer nur erscheinen wenn ich was rein setze und sie
aktiviere" — der Server liefert über /events-of-year ohnehin NUR
aktivierte Slots (inaktive/leere kommen als null), hier also einfach:
kein Eintrag -> Kachel bleibt versteckt, kompletter Abschnitt bleibt
versteckt, wenn beide Slots leer sind. Klick öffnet ein großes
Detail-Fenster statt sofort zu navigieren. */
const feldJahresEvent = (x) => (window.dogiFeld ? window.dogiFeld(x) : x || "");
let jahresEventsDaten = [null, null];
function zeigeJahresEventKachel(slot, daten) {
const karte = document.getElementById(`jahres-event-${slot}`);
if (!karte) return;
if (!daten) {
karte.hidden = true;
return;
}
const bild = karte.querySelector(".jahres-event-bild");
if (daten.bild) {
bild.src = daten.bild;
bild.alt = feldJahresEvent(daten.titel);
bild.hidden = false;
} else {
bild.hidden = true;
}
karte.querySelector(".jahres-event-datum").textContent = daten.datum || "";
karte.querySelector("h3").textContent = feldJahresEvent(daten.titel);
karte.querySelector("p").textContent = feldJahresEvent(daten.text);
karte.hidden = false;
}
function aktualisiereJahresEventsSichtbarkeit() {
const aktiveSlots = jahresEventsDaten.filter(Boolean).length;
const abschnitt = document.getElementById("jahres-events");
const grid = document.getElementById("jahres-events-grid");
abschnitt.hidden = aktiveSlots === 0;
// Genau 1 aktives Event: Kachel zentriert und etwas größer statt halb
// leerem Raster (Nutzer-Wunsch: "kacheln sollen bissl spezieller sein").
grid.classList.toggle("jahres-events-grid-einzeln", aktiveSlots === 1);
}
function jahresEventModalOeffnen(slot) {
const daten = jahresEventsDaten[slot - 1];
if (!daten) return;
const bild = document.getElementById("jahres-event-modal-bild");
if (daten.bild) {
bild.src = daten.bild;
bild.alt = feldJahresEvent(daten.titel);
bild.hidden = false;
} else {
bild.hidden = true;
}
document.getElementById("jahres-event-modal-datum").textContent = daten.datum || "";
document.getElementById("jahres-event-modal-titel").textContent = feldJahresEvent(daten.titel);
document.getElementById("jahres-event-modal-text").textContent = feldJahresEvent(daten.text);
const linkEl = document.getElementById("jahres-event-modal-link");
if (daten.link) {
linkEl.href = daten.link;
linkEl.textContent = feldJahresEvent(window.I18N_PAGE?.idx_jahres_event_modal_link) || "Direkt ansehen →";
linkEl.hidden = false;
} else {
linkEl.hidden = true;
}
const overlay = document.getElementById("jahres-event-modal-overlay");
overlay.hidden = false;
requestAnimationFrame(() => overlay.classList.add("offen"));
document.body.classList.add("modal-offen");
}
function jahresEventModalSchliessen() {
const overlay = document.getElementById("jahres-event-modal-overlay");
overlay.classList.remove("offen");
document.body.classList.remove("modal-offen");
setTimeout(() => { overlay.hidden = true; }, 220);
}
document.getElementById("jahres-event-1")?.addEventListener("click", () => jahresEventModalOeffnen(1));
document.getElementById("jahres-event-2")?.addEventListener("click", () => jahresEventModalOeffnen(2));
document.getElementById("jahres-event-modal-close")?.addEventListener("click", jahresEventModalSchliessen);
document.getElementById("jahres-event-modal-overlay")?.addEventListener("click", (e) => {
if (e.target.id === "jahres-event-modal-overlay") jahresEventModalSchliessen();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !document.getElementById("jahres-event-modal-overlay").hidden) jahresEventModalSchliessen();
});
if (API_BASE_URL) {
const ladeJahresEvents = () =>
fetch(`${API_BASE_URL}/events-of-year`, { cache: "no-store" })
.then((res) => res.json())
.then((data) => {
if (!data?.ok) return;
jahresEventsDaten = data.events;
zeigeJahresEventKachel(1, data.events[0]);
zeigeJahresEventKachel(2, data.events[1]);
aktualisiereJahresEventsSichtbarkeit();
})
.catch(() => {
/* Kein Fehler nötig -- der Abschnitt bleibt einfach unsichtbar. */
});
ladeJahresEvents();
// Bei Sprachwechsel mit den zuletzt geladenen Daten neu rendern (kein
// erneuter Server-Aufruf nötig) — Titel/Text stehen ja bereits in allen
// Sprachen im geladenen Objekt.
document.addEventListener("dogi-sprache-geaendert", () => {
zeigeJahresEventKachel(1, jahresEventsDaten[0]);
zeigeJahresEventKachel(2, jahresEventsDaten[1]);
});
}
// Stats: Team-Größe aus den Datenobjekten berechnen // Stats: Team-Größe aus den Datenobjekten berechnen
const teamCount = (window.MODIS || []).length + (window.SCOUTS || []).length; const teamCount = (window.MODIS || []).length + (window.SCOUTS || []).length;
const statTeam = document.getElementById("stat-team"); const statTeam = document.getElementById("stat-team");
if (statTeam) statTeam.textContent = teamCount > 0 ? `${teamCount}+` : "–"; if (statTeam) statTeam.textContent = teamCount > 0 ? `${teamCount}+` : "–";
// Team-Vorschau: VanVan zuerst, dann die nächsten nach Rang, plus 1-2 Scouts // Nutzer-Wunsch 21.08.2026: die Team-Vorschau (vier Personen-Kacheln unter
const teamPreview = document.getElementById("team-preview"); // "Team Dogi 🩵") wurde entfernt -- der zugehörige Render-Code stand hier.
if (teamPreview && window.MODIS) { // data-modis.js/data-scouts.js bleiben eingebunden, weil die Statistik-Zeile
const feld = (x) => (window.dogiFeld ? window.dogiFeld(x) : x || ""); // weiter oben ("Menschen im Team Dogi") sie weiterhin auszählt.
const people = [...window.MODIS].sort((a, b) => a.rank - b.rank).slice(0, 4);
const renderTeamPreview = () => {
teamPreview.innerHTML = people
.map(
(p) => `
<a class="profile-card" href="${p.page || `profil.html?p=${p.slug}`}">
<div class="avatar">${(p.avatar || p.photo) ? `<img src="${p.avatar || p.photo}" alt="${p.name}" loading="lazy">` : `<span class="avatar-initial">${p.name.charAt(0).toUpperCase()}</span>`}</div>
<h3>${p.name}</h3>
<div class="role">${feld(p.role)}</div>
</a>`
)
.join("");
};
renderTeamPreview();
document.addEventListener("dogi-sprache-geaendert", renderTeamPreview);
}
// Aktionen-Teaser: die ersten 3 Einträge aus data-aktionen.js
const aktionenTeaser = document.getElementById("aktionen-teaser");
if (aktionenTeaser && window.AKTIONEN) {
aktionenTeaser.innerHTML = window.AKTIONEN.slice(0, 3)
.map(
(a) => `
<a class="card" href="aktion-detail.html?id=${a.id}">
<span class="tag">${a.tag} · ${a.status}</span>
<h3>${a.title}</h3>
<p>${a.summary}</p>
</a>`
)
.join("");
}
}); });
</script> </script>
</body> </body>
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Kontakt, Impressum und Datenschutz von DOGFATHER UNIVERSE." /> <meta name="twitter:description" content="Kontakt, Impressum und Datenschutz von DOGFATHER UNIVERSE." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body> <body>
<div id="site-header"></div> <div id="site-header"></div>
@@ -126,7 +126,7 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-kontakt.js"></script> <script src="assets/js/i18n-kontakt.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+8 -8
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Alle Social-Media- und Musik-Links von DogFather an einem Ort — plus QR-Codes." /> <meta name="twitter:description" content="Alle Social-Media- und Musik-Links von DogFather an einem Ort — plus QR-Codes." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
<style> <style>
/* Rabattcodes: an das Supporter-Abo gekoppelt (Nutzer-Feedback /* Rabattcodes: an das Supporter-Abo gekoppelt (Nutzer-Feedback
03.08.2026: "verbinde es mit dem Abo... wenn die Leute drauf drücken 03.08.2026: "verbinde es mit dem Abo... wenn die Leute drauf drücken
@@ -46,12 +46,12 @@
.rabatt-gesperrt-hinweis p { color: #ffe9a8; margin: 0; flex: 1 1 260px; font-size: .95rem; } .rabatt-gesperrt-hinweis p { color: #ffe9a8; margin: 0; flex: 1 1 260px; font-size: .95rem; }
</style> </style>
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-links.jpg');--page-bg-mobile:url('/assets/img/bg-links-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-links-seite.jpg');--page-bg-mobile:url('/assets/img/bg-links-seite-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
<main> <main>
<section class="hero page-bg"> <section class="hero page-bg">
<div class="hero-banner"><img src="/assets/img/bg-links.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-links-seite.jpg" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="lk_eyebrow">🩵 Alle Links</span> <span class="eyebrow" data-i18n="lk_eyebrow">🩵 Alle Links</span>
<h1 data-i18n="lk_h1">Alles an einem Ort</h1> <h1 data-i18n="lk_h1">Alles an einem Ort</h1>
@@ -140,8 +140,8 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-links.js"></script> <script src="assets/js/i18n-links.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
(function () { (function () {
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
+66 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="DogFather als Manager bei der Spicy Media Agency: Scouts, Creator und faire Zusammenarbeit." /> <meta name="twitter:description" content="DogFather als Manager bei der Spicy Media Agency: Scouts, Creator und faire Zusammenarbeit." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-manager.jpg');--page-bg-mobile:url('/assets/img/bg-manager-mobile.jpg');"> <body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-manager.jpg');--page-bg-mobile:url('/assets/img/bg-manager-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -96,6 +96,19 @@
</div> </div>
</section> </section>
<!-- "Weitere Manager" (21.08.2026, Nutzer-Wunsch: "überall wo ... Manager
... sind, will ich dass ich die easy über meine Verwaltungsseite
hinzufüge"). Aktuell gibt es nur DogFather selbst als Manager -- diese
Sektion bleibt deshalb komplett unsichtbar (hidden), bis über die
Verwaltung mindestens ein weiterer Manager angelegt wurde. Gleiches
flaches Karten-Raster wie team-scouts.html. -->
<section class="section-tight" id="weitere-manager-root" hidden>
<div class="container">
<h2 class="text-center" data-i18n="mgr_weitere_h2" data-site-text-key="seitentitel_manager">Weitere Manager</h2>
<div id="weitere-manager-grid" class="grid grid-4"></div>
</div>
</section>
<section> <section>
<div class="container"> <div class="container">
<h2 class="text-center" data-i18n="mgr_team_h2">Willst du dazugehören?</h2> <h2 class="text-center" data-i18n="mgr_team_h2">Willst du dazugehören?</h2>
@@ -126,7 +139,54 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/i18n-manager.js"></script> <script src="assets/js/data-manager.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/i18n-manager.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821o"></script>
<script>
// "Weitere Manager" (21.08.2026) -- siehe Kommentar bei #weitere-manager-root
// oben. Aktuell IMMER window.MANAGERS === [] (siehe data-manager.js), Inhalt
// kommt also ausschließlich über die Verwaltung (dogiTeamLaden). Bleibt
// komplett unsichtbar, solange niemand eingetragen ist.
document.addEventListener("DOMContentLoaded", () => {
const root = document.getElementById("weitere-manager-root");
const grid = document.getElementById("weitere-manager-grid");
if (!root || !grid) return;
const feld = (x) => (window.dogiFeld ? window.dogiFeld(x) : x || "");
const t = (schluessel) => (window.dogiUebersetzen ? window.dogiUebersetzen(schluessel) : schluessel);
const renderWeitereManager = () => {
const liste = window.MANAGERS || [];
if (!liste.length) {
root.hidden = true;
return;
}
grid.innerHTML = liste.map((m) => {
const quoteText = feld(m.quote);
return `
<a class="profile-card" href="profil.html?p=${m.slug}">
<div class="avatar">${(m.avatar || m.photo) ? `<img src="${m.avatar || m.photo}" alt="${m.name}" loading="lazy">` : `<span class="avatar-initial">${m.name.charAt(0).toUpperCase()}</span>`}</div>
<h3>${m.name}</h3>
<div class="role">${feld(m.role)}</div>
${quoteText ? `<p class="quote">„${quoteText}"</p>` : ""}
<span class="btn btn-outline" style="margin-top:.7rem;pointer-events:none;">${t("mgr_weitere_profil_btn")}</span>
</a>`;
}).join("");
root.hidden = false;
};
document.addEventListener("dogi-sprache-geaendert", renderWeitereManager);
if (window.dogiTeamLaden) {
window.dogiTeamLaden("manager").then((neue) => {
if (!neue.length) return;
// Runde 2 (21.08.2026, "wenn ich eine änderung mache ist es auch für
// jeden"): überschreibt bereits importierte Manager statt zu duplizieren.
window.MANAGERS = window.dogiTeamZusammenfuehren(window.MANAGERS || [], neue);
renderWeitereManager();
});
}
});
</script>
</body> </body>
</html> </html>
+40
View File
@@ -0,0 +1,40 @@
{
"name": "DogiCrew-Verwaltung",
"short_name": "Verwaltung",
"description": "Interner Admin-Bereich: DogiCrew-Supporter, Prämien, Event des Jahres, Abstimmungen. Nur mit Zugangscode.",
"start_url": "/verwaltung.html",
"id": "/verwaltung.html",
"scope": "/verwaltung.html",
"display": "standalone",
"orientation": "any",
"background_color": "#0e0a16",
"theme_color": "#0e0a16",
"lang": "de",
"dir": "ltr",
"icons": [
{
"src": "/assets/img/icon-192-admin.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/icon-512-admin.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/assets/img/icon-192-maskable-admin.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/assets/img/icon-512-maskable-admin.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
+11 -11
View File
@@ -19,17 +19,17 @@
<meta name="twitter:description" content="Fotos und Videos rund um Husky Casper an einem Ort." /> <meta name="twitter:description" content="Fotos und Videos rund um Husky Casper an einem Ort." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" data-welt="casper" style="--page-bg:url('/assets/img/bg-casper.jpg');--page-bg-mobile:url('/assets/img/bg-casper-mobile.jpg');"> <body class="mit-hintergrund" data-welt="casper" style="--page-bg:url('/assets/img/bg-medien-casper.jpg');--page-bg-mobile:url('/assets/img/bg-medien-casper-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
<main> <main>
<section class="hero page-bg"> <section class="hero page-bg">
<div class="hero-banner"><img src="/assets/img/bg-casper.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-medien-casper.jpg" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="mc_eyebrow">Welt 2 — HasiDog</span> <span class="eyebrow" data-i18n="mc_eyebrow">Welt 2 — HasiDog</span>
<h1 data-i18n="mc_h1">Medien — Casper</h1> <h1 data-i18n="mc_h1">Medien — Casper</h1>
@@ -59,10 +59,10 @@
</div> </div>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-media.js"></script> <script src="assets/js/data-media.js?v=20260821o"></script>
<script src="assets/js/data-videos.js"></script> <script src="assets/js/data-videos.js?v=20260821o"></script>
<script src="assets/js/medien-welt.js"></script> <script src="assets/js/medien-welt.js?v=20260821o"></script>
<script src="assets/js/i18n-medien-casper.js"></script> <script src="assets/js/i18n-medien-casper.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+10 -10
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Fotos, Videos und Events aus DogFathers Welt an einem Ort." /> <meta name="twitter:description" content="Fotos, Videos und Events aus DogFathers Welt an einem Ort." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" data-welt="dogfather" style="--page-bg:url('/assets/img/bg-dogfather.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-mobile.jpg');"> <body class="mit-hintergrund" data-welt="dogfather" style="--page-bg:url('/assets/img/bg-dogfather.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
@@ -65,11 +65,11 @@
</div> </div>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-media.js"></script> <script src="assets/js/data-media.js?v=20260821o"></script>
<script src="assets/js/data-videos.js"></script> <script src="assets/js/data-videos.js?v=20260821o"></script>
<script src="assets/js/data-aktionen.js"></script> <script src="assets/js/data-aktionen.js?v=20260821o"></script>
<script src="assets/js/medien-welt.js"></script> <script src="assets/js/medien-welt.js?v=20260821o"></script>
<script src="assets/js/i18n-medien-dogfather.js"></script> <script src="assets/js/i18n-medien-dogfather.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+12 -12
View File
@@ -19,17 +19,17 @@
<meta name="twitter:description" content="Fotos, Videos und Events aus der HasiDog-Welt an einem Ort." /> <meta name="twitter:description" content="Fotos, Videos und Events aus der HasiDog-Welt an einem Ort." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
</head> </head>
<body class="mit-hintergrund" data-welt="hasidog" style="--page-bg:url('/assets/img/bg-hasidog.jpg');--page-bg-mobile:url('/assets/img/bg-hasidog-mobile.jpg');"> <body class="mit-hintergrund" data-welt="hasidog" style="--page-bg:url('/assets/img/bg-medien-hasidog.jpg');--page-bg-mobile:url('/assets/img/bg-medien-hasidog-mobile.jpg');">
<div id="site-header"></div> <div id="site-header"></div>
<main> <main>
<section class="hero page-bg"> <section class="hero page-bg">
<div class="hero-banner"><img src="/assets/img/bg-hasidog.jpg" alt="" loading="eager" /></div> <div class="hero-banner"><img src="/assets/img/bg-medien-hasidog.jpg" alt="" loading="eager" /></div>
<div class="container"> <div class="container">
<span class="eyebrow" data-i18n="mh_eyebrow">Welt 2 — HasiDog</span> <span class="eyebrow" data-i18n="mh_eyebrow">Welt 2 — HasiDog</span>
<h1 data-i18n="mh_h1">Medien — HasiDog</h1> <h1 data-i18n="mh_h1">Medien — HasiDog</h1>
@@ -65,11 +65,11 @@
</div> </div>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-media.js"></script> <script src="assets/js/data-media.js?v=20260821o"></script>
<script src="assets/js/data-videos.js"></script> <script src="assets/js/data-videos.js?v=20260821o"></script>
<script src="assets/js/data-aktionen.js"></script> <script src="assets/js/data-aktionen.js?v=20260821o"></script>
<script src="assets/js/medien-welt.js"></script> <script src="assets/js/medien-welt.js?v=20260821o"></script>
<script src="assets/js/i18n-medien-hasidog.js"></script> <script src="assets/js/i18n-medien-hasidog.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js?v=20260821o"></script>
</body> </body>
</html> </html>
+33 -15
View File
@@ -16,10 +16,10 @@
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="robots" content="noindex" /> <!-- generische URL, echte Team-Seiten verlinken hierher --> <meta name="robots" content="noindex" /> <!-- generische URL, echte Team-Seiten verlinken hierher -->
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821o" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" /> <link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821o" />
<style> <style>
.profil-photo { aspect-ratio: 4/5; max-width: 420px; margin: 0 auto; } .profil-photo { aspect-ratio: 4/5; max-width: 420px; margin: 0 auto; }
.profil-photo img { width: 100%; height: 100%; object-fit: cover; display: block; } .profil-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
@@ -130,18 +130,25 @@
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-modis.js"></script> <script src="assets/js/data-modis.js?v=20260821o"></script>
<script src="assets/js/data-scouts.js"></script> <script src="assets/js/data-scouts.js?v=20260821o"></script>
<script src="assets/js/data-creator.js"></script> <script src="assets/js/data-creator.js?v=20260821o"></script>
<script src="assets/js/i18n-profil.js"></script> <script src="assets/js/data-manager.js?v=20260821o"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/i18n-profil.js?v=20260821o"></script>
<script src="assets/js/main.js?v=20260821o"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", async () => {
const slug = new URLSearchParams(location.search).get("p") || ""; const slug = new URLSearchParams(location.search).get("p") || "";
const modis = window.MODIS || []; const modis = window.MODIS || [];
const scouts = window.SCOUTS || []; const scouts = window.SCOUTS || [];
const creators = window.CREATORS || []; const creators = window.CREATORS || [];
const alle = [...modis, ...scouts, ...creators]; const managers = window.MANAGERS || [];
// 21.08.2026: über die Verwaltung angelegte Mitglieder ALLER Kategorien
// dazuholen -- profil.html weiß vorab nicht, zu welcher Kategorie ein
// slug gehört, deshalb kommt hier (ohne Kategorie-Filter) einfach alles
// zusammen zurück und wird unten normal mitgesucht.
const admins = window.dogiTeamLaden ? await window.dogiTeamLaden() : [];
const alle = [...modis, ...scouts, ...creators, ...managers, ...admins];
const person = alle.find((p) => p.slug === slug); const person = alle.find((p) => p.slug === slug);
// Für alle Rahmentexte, die rein per JS gesetzt werden (Name/Rolle/Bio // Für alle Rahmentexte, die rein per JS gesetzt werden (Name/Rolle/Bio
@@ -170,16 +177,22 @@ document.addEventListener("DOMContentLoaded", () => {
return; return;
} }
const istScout = scouts.includes(person); // 21.08.2026: admin-gepflegte Personen (aus dogiTeamLaden) stecken NICHT
const istCreator = creators.includes(person); // in den statischen modis/scouts/creators/managers-Arrays selbst (die
const welt = istScout || istCreator ? "spicymedia" : "dogfather"; // reference-Prüfung .includes(person) würde für sie also fälschlich
// "false" liefern) -- deshalb zusätzlich über das von dogiTeamLaden
// gesetzte person.kategorie-Feld prüfen.
const istScout = scouts.includes(person) || person.kategorie === "scout";
const istCreator = creators.includes(person) || person.kategorie === "creator";
const istManager = managers.includes(person) || person.kategorie === "manager";
const welt = istScout || istCreator || istManager ? "spicymedia" : "dogfather";
document.documentElement.setAttribute("data-theme", welt); document.documentElement.setAttribute("data-theme", welt);
// Passendes Weltmotiv nur noch als weichgezeichnete Stimmung ganz im // Passendes Weltmotiv nur noch als weichgezeichnete Stimmung ganz im
// Hintergrund (body.mit-hintergrund) — bewusst KEIN scharfes Banner-Bild // Hintergrund (body.mit-hintergrund) — bewusst KEIN scharfes Banner-Bild
// mehr oben im Kopfbereich, damit das Profil der Person (Foto/Name/Bio) // mehr oben im Kopfbereich, damit das Profil der Person (Foto/Name/Bio)
// sofort beim Öffnen der Seite im Fokus steht, ohne Banner davor. // sofort beim Öffnen der Seite im Fokus steht, ohne Banner davor.
const bgBasis = istScout || istCreator ? "bg-scouts" : "bg-dogfather"; const bgBasis = istScout || istCreator || istManager ? "bg-scouts" : "bg-dogfather";
document.body.style.setProperty("--page-bg", `url('/assets/img/${bgBasis}.jpg')`); document.body.style.setProperty("--page-bg", `url('/assets/img/${bgBasis}.jpg')`);
document.body.style.setProperty("--page-bg-mobile", `url('/assets/img/${bgBasis}-mobile.jpg')`); document.body.style.setProperty("--page-bg-mobile", `url('/assets/img/${bgBasis}-mobile.jpg')`);
@@ -241,6 +254,8 @@ document.addEventListener("DOMContentLoaded", () => {
? t("pr_eyebrow_scout") ? t("pr_eyebrow_scout")
: istCreator : istCreator
? t("pr_eyebrow_creator") ? t("pr_eyebrow_creator")
: istManager
? t("pr_eyebrow_manager")
: t("pr_eyebrow_default"); : t("pr_eyebrow_default");
document.getElementById("profil-hero-name").textContent = person.name; document.getElementById("profil-hero-name").textContent = person.name;
@@ -286,6 +301,9 @@ document.addEventListener("DOMContentLoaded", () => {
} else if (istCreator) { } else if (istCreator) {
zurueck.href = "creator.html"; zurueck.href = "creator.html";
zurueck.textContent = t("pr_back_creator"); zurueck.textContent = t("pr_back_creator");
} else if (istManager) {
zurueck.href = "manager.html";
zurueck.textContent = t("pr_back_manager");
} else { } else {
zurueck.href = "team-modis.html"; zurueck.href = "team-modis.html";
zurueck.textContent = t("pr_back_modis"); zurueck.textContent = t("pr_back_modis");

Some files were not shown because too many files have changed in this diff Show More