(() => { const $ = (selector, root = document) => root.querySelector(selector); const $$ = (selector, root = document) => Array.from(root.querySelectorAll(selector)); const toggle = $('[data-nav-toggle]'); const nav = $('[data-nav]'); if (toggle && nav) toggle.addEventListener('click', () => nav.classList.toggle('open')); const speakButtons = $$('[data-speak]'); let activeAudio = null; let activeAudioUrl = ''; let activeSpeech = null; let activeSpeechButton = null; let activeRequest = null; const speechLabels = () => document.documentElement.lang === 'en' ? {play:'Read interpretation', stop:'Stop reading', loading:'Loading premium voice…', ready:'Play now'} : {play:'Deutung vorlesen', stop:'Vorlesen stoppen', loading:'Premium-Stimme wird geladen…', ready:'Jetzt abspielen'}; const setSpeechVisualState = (button, state='idle') => { const stage = button && button.closest ? button.closest('.pv-voice-stage') : null; if (!stage) return; stage.classList.toggle('is-loading', state === 'loading'); stage.classList.toggle('is-speaking', state === 'speaking'); }; const resetSpeechButton = () => { if (activeSpeechButton) { setSpeechVisualState(activeSpeechButton, 'idle'); activeSpeechButton.textContent = speechLabels().play; activeSpeechButton.classList.remove('is-speaking','is-loading'); activeSpeechButton.setAttribute('aria-pressed','false'); } if (activeAudioUrl) URL.revokeObjectURL(activeAudioUrl); activeAudioUrl = ''; activeAudio = null; activeSpeech = null; activeSpeechButton = null; activeRequest = null; }; const stopSpeech = () => { if (activeRequest) activeRequest.abort(); if (activeAudio) { activeAudio.pause(); activeAudio.currentTime = 0; } if ('speechSynthesis' in window) window.speechSynthesis.cancel(); resetSpeechButton(); }; const browserFallback = (speak, text) => { if (!('speechSynthesis' in window)) { resetSpeechButton(); return; } const utterance = new SpeechSynthesisUtterance(text); utterance.lang = document.documentElement.lang === 'en' ? 'en-US' : 'de-DE'; utterance.rate = 0.94; utterance.onend = resetSpeechButton; utterance.onerror = resetSpeechButton; activeSpeech = utterance; activeSpeechButton = speak; speak.textContent = speechLabels().stop; speak.classList.remove('is-loading'); speak.classList.add('is-speaking'); setSpeechVisualState(speak, 'speaking'); speak.setAttribute('aria-pressed','true'); window.speechSynthesis.speak(utterance); }; speakButtons.forEach((speak) => { const scope = speak.closest('.interpretation') || speak.closest('.daily-interpretation') || speak.closest('.soulmate-compass-interpretation') || speak.closest('.pv-search-summary') || document; const speechText = $('[data-speech-text]', scope); if (!speechText) { speak.hidden = true; return; } speak.setAttribute('aria-pressed','false'); speak.addEventListener('click', async () => { if (activeSpeechButton === speak && activeAudio && activeAudio.paused && activeAudio.src) { try { await activeAudio.play(); speak.textContent = speechLabels().stop; speak.classList.add('is-speaking'); setSpeechVisualState(speak, 'speaking'); return; } catch (_) {} } if (activeSpeechButton === speak) { stopSpeech(); return; } stopSpeech(); const text = speechText.textContent.trim(); if (!text) return; const endpoint = $('meta[name="pv-tts-endpoint"]')?.content || ''; const csrf = $('meta[name="pv-csrf"]')?.content || ''; if (!endpoint || !csrf || !window.fetch) { browserFallback(speak, text); return; } activeSpeechButton = speak; speak.textContent = speechLabels().loading; speak.classList.add('is-loading'); setSpeechVisualState(speak, 'loading'); speak.setAttribute('aria-pressed','true'); const controller = new AbortController(); activeRequest = controller; const form = new FormData(); form.set('csrf', csrf); form.set('text', text); form.set('context', speak.dataset.ttsContext || 'general'); form.set('lang', document.documentElement.lang === 'en' ? 'en' : 'de'); try { const response = await fetch(endpoint, {method:'POST', body:form, credentials:'same-origin', signal:controller.signal, headers:{'X-CSRF-Token':csrf}}); if (!response.ok || !(response.headers.get('content-type') || '').includes('audio/')) { if (response.status !== 419) browserFallback(speak, text); else resetSpeechButton(); return; } const blob = await response.blob(); if (activeSpeechButton !== speak) return; activeAudioUrl = URL.createObjectURL(blob); const audio = new Audio(activeAudioUrl); activeAudio = audio; activeRequest = null; audio.onended = resetSpeechButton; audio.onerror = () => browserFallback(speak, text); speak.classList.remove('is-loading'); speak.classList.add('is-speaking'); setSpeechVisualState(speak, 'speaking'); speak.textContent = speechLabels().stop; try { await audio.play(); } catch (_) { speak.classList.remove('is-speaking'); setSpeechVisualState(speak, 'idle'); speak.textContent = speechLabels().ready; } } catch (error) { if (error?.name === 'AbortError') return; browserFallback(speak, text); } }); }); window.addEventListener('pagehide', stopSpeech); const dailyPicker = $('[data-daily-picker]'); if (dailyPicker) { const choiceInput = $('[data-daily-choice-input]', dailyPicker); const revealButton = $('[data-daily-reveal]', dailyPicker); const hint = $('[data-daily-pick-hint]', dailyPicker); $$('[data-daily-choice]', dailyPicker).forEach((cardButton) => { cardButton.addEventListener('click', () => { $$('[data-daily-choice]', dailyPicker).forEach((button) => { const selected = button === cardButton; button.classList.toggle('selected', selected); button.setAttribute('aria-pressed', selected ? 'true' : 'false'); }); if (choiceInput) choiceInput.value = cardButton.dataset.dailyChoice || ''; if (revealButton) revealButton.disabled = false; if (hint) hint.textContent = document.documentElement.lang === 'en' ? 'Your card is selected. Reveal it when you are ready.' : 'Deine Karte ist gewählt. Decke sie auf, wenn du bereit bist.'; }); }); } const wizard = $('[data-reading-wizard]'); if (wizard) { let config = {}; try { config = JSON.parse($('[data-reading-config]', wizard)?.textContent || '{}'); } catch (_) {} const categoryInput = $('[data-reading-category]', wizard); const subjectLabel = $('[data-subject-label]', wizard); const subjectInput = $('[data-subject-input]', wizard); const questionInput = $('[data-question-input]', wizard); const examplesBox = $('[data-question-examples]', wizard); const shortHint = $('[data-question-short]', wizard); let selected = ''; const showStep = (step) => { $$('[data-reading-pane]', wizard).forEach((pane) => { const active = Number(pane.dataset.readingPane) === Number(step); pane.hidden = !active; pane.classList.toggle('active', active); }); $$('[data-reading-step-pill]', wizard).forEach((pill) => pill.classList.toggle('active', Number(pill.dataset.readingStepPill) === Number(step))); wizard.scrollIntoView({behavior:'smooth', block:'start'}); }; const configureCategory = () => { const item = config[selected] || {}; if (subjectLabel) subjectLabel.textContent = item.name_label || (document.documentElement.lang === 'en' ? 'How would you like to name this?' : 'Wie möchtest du das bezeichnen?'); if (subjectInput) subjectInput.placeholder = item.name_placeholder || ''; if (questionInput) questionInput.placeholder = item.question_placeholder || ''; if (examplesBox) { examplesBox.innerHTML = ''; (item.examples || []).forEach((example) => { const button = document.createElement('button'); button.type = 'button'; button.textContent = example; button.addEventListener('click', () => { questionInput.value = example; questionInput.focus(); questionInput.dispatchEvent(new Event('input')); }); examplesBox.appendChild(button); }); } }; $$('[data-reading-choice]', wizard).forEach((button) => button.addEventListener('click', () => { selected = button.dataset.readingChoice || ''; if (categoryInput) categoryInput.value = selected; $$('[data-reading-choice]', wizard).forEach((b) => b.classList.toggle('selected', b === button)); configureCategory(); showStep(2); })); $$('[data-reading-next]', wizard).forEach((button) => button.addEventListener('click', () => showStep(Number(button.dataset.readingNext)))); $$('[data-reading-back]', wizard).forEach((button) => button.addEventListener('click', () => showStep(Number(button.dataset.readingBack)))); questionInput?.addEventListener('input', () => { if (shortHint) shortHint.hidden = questionInput.value.trim().length >= 8 || questionInput.value.trim().length === 0; }); wizard.addEventListener('submit', (event) => { if (!categoryInput?.value) { event.preventDefault(); showStep(1); return; } if ((questionInput?.value.trim().length || 0) < 4) { event.preventDefault(); showStep(3); questionInput?.focus(); } }); } $$('[data-reading-result]').forEach((result) => { const reveal = $('[data-reveal-reading]', result); if (!reveal) return; reveal.addEventListener('click', () => { $('[data-reveal-cards]', result)?.classList.remove('cards-concealed'); $('[data-reveal-interpretation]', result)?.classList.remove('interpretation-concealed'); const prompt = $('[data-reveal-prompt]', result); if (prompt) prompt.hidden = true; const actions = $('[data-reveal-actions]', result); if (actions) actions.hidden = false; $('[data-reveal-cards]', result)?.scrollIntoView({behavior:'smooth', block:'start'}); }); }); const tableau = $('[data-tableau-wizard]'); if (tableau) { let config = {}; try { config = JSON.parse($('[data-tableau-config]', tableau)?.textContent || '{}'); } catch (_) {} const themeInput = $('[data-tableau-theme-input]', tableau); const personInput = $('[data-tableau-person-input]', tableau); const secondInput = $('[data-tableau-second-input]', tableau); const ownName = $('#tableau-own-name', tableau); const subjectName = $('[data-tableau-subject-input]', tableau); const subjectLabel = $('[data-tableau-subject-label]', tableau); const subjectExamples = $('[data-tableau-subject-examples]', tableau); const question = $('[data-tableau-question]', tableau); const questionExamples = $('[data-tableau-question-examples]', tableau); const kicker = $('.wizard-kicker', tableau); let step = 1, theme = '', person = 0, second = ''; const text = (de, en) => document.documentElement.lang === 'en' ? en : de; const showStep = (next) => { step = Number(next); $$('[data-tableau-pane]', tableau).forEach((pane) => { const active = Number(pane.dataset.tableauPane) === step; pane.hidden = !active; pane.classList.toggle('active', active); }); $$('[data-tableau-step-pill]', tableau).forEach((pill) => pill.classList.toggle('active', Number(pill.dataset.tableauStepPill) === step)); if (kicker) kicker.textContent = text(`Schritt ${step} von 5`, `Step ${step} of 5`); if (step === 5) updateSummary(); tableau.scrollIntoView({behavior:'smooth', block:'start'}); }; const selectOnly = (selector, selectedEl) => $$(selector, tableau).forEach((el) => el.classList.toggle('selected', el === selectedEl)); const nextButton = (fromStep) => $(`[data-tableau-pane="${fromStep}"] [data-tableau-next]`, tableau); const configureTheme = () => { const item = config[theme] || {}; if (subjectLabel) subjectLabel.textContent = item.subject_label || text('Name oder Bezeichnung','Name or designation'); if (subjectExamples) subjectExamples.textContent = (item.subject_examples || []).length ? text('Zum Beispiel: ','For example: ') + item.subject_examples.join(' · ') : ''; if (subjectName) subjectName.placeholder = (item.subject_examples || [])[0] || ''; if (questionExamples) { questionExamples.innerHTML = ''; (item.examples || []).forEach((example) => { const b = document.createElement('button'); b.type='button'; b.textContent=example; b.addEventListener('click', () => { question.value=example; question.dispatchEvent(new Event('input')); question.focus(); }); questionExamples.appendChild(b); }); } }; $$('[data-tableau-theme]', tableau).forEach((button) => button.addEventListener('click', () => { theme = button.dataset.tableauTheme || ''; themeInput.value = theme; $$('[data-tableau-theme]', tableau).forEach((b) => b.classList.toggle('selected', b === button)); configureTheme(); const n = nextButton(1); if (n) n.disabled = !theme; })); const otherButton = $('[data-tableau-second-other]', tableau); const otherLabel = $('[data-tableau-second-label]', tableau); const otherHint = $('[data-tableau-second-hint]', tableau); $$('[data-tableau-person]', tableau).forEach((button) => button.addEventListener('click', () => { person = Number(button.dataset.tableauPerson); personInput.value = String(person); second = ''; secondInput.value=''; selectOnly('[data-tableau-person]', button); $$('[data-tableau-second]', tableau).forEach((b) => b.classList.remove('selected')); otherButton?.classList.remove('selected'); if (otherButton) { const other = person === 29 ? 28 : 29; otherButton.disabled = false; otherButton.dataset.tableauSecond = String(other); if (otherLabel) otherLabel.textContent = person === 29 ? text('Herr','Gentleman') : text('Dame','Lady'); if (otherHint) otherHint.textContent = text(`Karte ${other} als zweite Person`,`Card ${other} as second person`); } const n = nextButton(2); if (n) n.disabled = true; })); const chooseSecondPerson = (button) => { if (!person || button.disabled) return; second = button.dataset.tableauSecond || ''; secondInput.value = second; $$('[data-tableau-second]', tableau).forEach((b) => b.classList.toggle('selected', b === button)); otherButton?.classList.toggle('selected', button === otherButton); const n = nextButton(2); if (n) n.disabled = !second; }; $$('[data-tableau-second]', tableau).forEach((button) => button.addEventListener('click', () => chooseSecondPerson(button))); // The "other person" button receives its concrete card number only after // the user's own person card is selected. Its listener therefore has to // be registered explicitly instead of relying on the initial selector. otherButton?.addEventListener('click', () => chooseSecondPerson(otherButton)); question?.addEventListener('input', () => { const n = nextButton(4); if (n) n.disabled = question.value.trim().length < 5; const warning = $('[data-tableau-question-warning]', tableau); if (warning) warning.hidden = question.value.trim().length >= 5; }); const personName = (n) => n === 29 ? text('Dame','Lady') : n === 28 ? text('Herr','Gentleman') : '–'; const updateSummary = () => { const item = config[theme] || {}; const values = { '[data-summary-theme]': item.label || '–', '[data-summary-person]': personName(person), '[data-summary-second]': second === 'none' ? text('Keine konkrete zweite Person','No specific second person') : personName(Number(second)), '[data-summary-own]': ownName?.value.trim() || '–', '[data-summary-subject]': subjectName?.value.trim() || '–', '[data-summary-question]': question?.value.trim() || '–' }; Object.entries(values).forEach(([sel,val]) => { const el=$(sel,tableau); if(el) el.textContent=val; }); const sl=$('[data-summary-subject-label]',tableau); if(sl) sl.textContent=item.subject_label || text('Name oder Bezeichnung','Name or designation'); }; $$('[data-tableau-next]', tableau).forEach((button) => button.addEventListener('click', () => { const next = Number(button.dataset.tableauNext); if (next === 2 && !theme) return; if (next === 3 && (!person || !second)) return; if (next === 5 && (question?.value.trim().length || 0) < 5) return; showStep(next); })); $$('[data-tableau-back]', tableau).forEach((button) => button.addEventListener('click', () => showStep(Number(button.dataset.tableauBack)))); tableau.addEventListener('submit', (event) => { if (!theme) {event.preventDefault(); showStep(1); return;} if (!person || !second) {event.preventDefault(); showStep(2); return;} if ((question?.value.trim().length || 0) < 5) {event.preventDefault(); showStep(4); question?.focus();} }); } const yesnoForm = $('[data-yesno-form]'); if (yesnoForm) { const question = $('[data-yesno-question]', yesnoForm); const box = $('[data-yesno-validation]', yesnoForm); const message = $('[data-yesno-message]', yesnoForm); const suggestionsBox = $('[data-yesno-suggestions]', yesnoForm); const isEn = document.documentElement.lang === 'en'; const normalize = (value) => value.toLowerCase().replace(/[^a-z0-9äöüß ]/giu, ' ').replace(/\s+/g, ' ').trim(); const sentence = (value) => { const clean = String(value || '').trim().replace(/[?!.;,:]+$/u, '').trim(); if (!clean) return ''; return clean.charAt(0).toLocaleUpperCase() + clean.slice(1) + '?'; }; const compoundQuestion = (raw) => { const explicit = raw.split(/\?+/u).map((x) => x.trim()).filter(Boolean); if (explicit.length >= 2) { return { text: isEn ? 'Your entry contains more than one question. For a clear yes/no answer, please focus on exactly one.' : 'In deiner Eingabe stecken mehrere Fragen. Für eine klare Ja/Nein-Antwort konzentriere dich bitte auf genau eine davon.', suggestions: explicit.slice(0, 2).map(sentence), }; } if (isEn) { let m = raw.match(/^(.*?)\s+(?:and|or)\s+((?:will|would|is|are|was|were|do|does|did|can|could|should|has|have|had|may|might|must)\b.+)$/iu); if (m) return {text:'Your question combines two separately answerable points. Please ask them one at a time.', suggestions:[sentence(m[1]), sentence(m[2])]}; m = raw.match(/^((?:will|would|is|are|can|could|should|has|have|may|might|must))\s+([^\s?]+)\s+(.+?)\s+(?:and|or)\s+((?:successful|popular|profitable|happy|healthy|stable|safe|known|famous|return|stay|leave|work|succeed|grow|win)\b.*?)\??$/iu); if (m) return {text:'Your question combines two different goals. Please separate them so each reading can give an unambiguous answer.', suggestions:[sentence(`${m[1]} ${m[2]} ${m[3]}`), sentence(`${m[1]} ${m[2]} ${m[4]}`)]}; return null; } const verbs = 'wird|werden|werde|wirst|ist|sind|bist|bin|hat|haben|hast|kann|können|kannst|kommt|kommen|kommst|geht|gehen|gehst|bleibt|bleiben|bleibst|soll|sollen|sollst|darf|dürfen|darfst|muss|müssen|musst|will|wollen|willst|möchte|möchten|möchtest|gibt|führt|führen|bekommt|bekommen|meldet|melden|liebt|lieben|klappt|funktioniert|gelingt'; let m = raw.match(new RegExp(`^(.*?)\\s+(?:und|oder)\\s+((?:${verbs})\\b.+)$`, 'iu')); if (m) return {text:'In deiner Frage stecken zwei getrennt beantwortbare Anliegen. Bitte stelle sie nacheinander, damit die Karten nicht zwei unterschiedliche Dinge mit einer einzigen Ja/Nein-Antwort beantworten müssen.', suggestions:[sentence(m[1]), sentence(m[2])]}; const outcomes = 'erfolgreich|beliebt|glücklich|zufrieden|profitabel|bekannt|berühmt|gesund|stabil|sicher|treu|ehrlich|verliebt|schwanger|angenommen|bewilligt|verkauft|zurückkommen|zurückkehren|kommen|bleiben|gehen|melden|heiraten|zusammenkommen|funktionieren|klappen|gelingen|steigen|wachsen|gewinnen'; m = raw.match(new RegExp(`^((?:wird|werden|ist|sind|hat|haben|kann|können|soll|sollen|darf|dürfen|muss|müssen|will|wollen|möchte|möchten))\\s+([^\\s?]+)\\s+(.+?)\\s+(?:und|oder)\\s+((?:${outcomes})\\b.*?)\\??$`, 'iu')); if (m) return {text:'Deine Frage verbindet zwei unterschiedliche Ziele. Für eine verlässliche Ja/Nein-Antwort trenne sie bitte – beide könnten unterschiedlich beantwortet werden.', suggestions:[sentence(`${m[1]} ${m[2]} ${m[3]}`), sentence(`${m[1]} ${m[2]} ${m[4]}`)]}; return null; }; const validate = () => { const raw = question?.value.trim() || ''; const normalized = normalize(raw); if (normalized.split(' ').filter(Boolean).length < 3) { return {ok:false, text:isEn ? 'Your question is too short. Please phrase it as a complete yes/no question.' : 'Deine Frage ist noch sehr kurz. Formuliere sie bitte als vollständige Ja/Nein-Frage.', suggestions:[]}; } const compound = compoundQuestion(raw); if (compound) return {ok:false, ...compound}; const openers = isEn ? ['how long','how much','how many','how often','when','why','where','who','whom','what','which'] : ['wie lange','wie viel','wie viele','wie oft','wann','warum','wieso','weshalb','wohin','woher','wo','wer','wen','wem','was','welche','welcher','welches','welchen']; const opener = openers.find((word) => normalized === word || normalized.startsWith(word + ' ')); if (!opener) return {ok:true, text:'', suggestions:[]}; const proposal = isEn ? (opener === 'when' ? 'Will this situation change within the next four weeks?' : opener === 'why' ? 'Is there an important reason for this that I do not yet see?' : opener === 'where' ? 'Is there a sign that this situation will clear up soon?' : 'Is my current path in this matter leading to a good outcome?') : (opener === 'wann' ? 'Verändert sich meine Lage innerhalb der nächsten vier Wochen?' : ['warum','wieso','weshalb'].includes(opener) ? 'Gibt es dabei einen wesentlichen Grund, den ich derzeit noch nicht sehe?' : ['wo','wohin','woher'].includes(opener) ? 'Gibt es einen Hinweis darauf, dass sich diese Lage bald klärt?' : 'Führt mein derzeitiger Weg in diesem Thema zu einem guten Ergebnis?'); return {ok:false, text:isEn ? `A question beginning with “${opener}” cannot be answered with yes or no.` : `Eine Frage mit „${opener}“ lässt sich nicht mit Ja oder Nein beantworten.`, suggestions:[proposal]}; }; const hideValidation = () => { if (box) box.hidden = true; if (suggestionsBox) suggestionsBox.replaceChildren(); }; const showValidation = (result) => { if (box) box.hidden = false; if (message) message.textContent = result.text; if (!suggestionsBox) return; suggestionsBox.replaceChildren(); (result.suggestions || []).filter(Boolean).slice(0, 2).forEach((proposal) => { const button = document.createElement('button'); button.className = 'choice-chip'; button.type = 'button'; button.textContent = proposal; button.addEventListener('click', () => { if (question) { question.value = proposal; question.focus(); } hideValidation(); }); suggestionsBox.appendChild(button); }); }; $$('[data-yesno-example]', yesnoForm).forEach((button) => button.addEventListener('click', () => { if (question) { question.value = button.textContent.trim(); question.focus(); } hideValidation(); })); question?.addEventListener('input', hideValidation); yesnoForm.addEventListener('submit', (event) => { const result = validate(); if (result.ok) return; event.preventDefault(); showValidation(result); }); } const soulmate = $('[data-soulmate-wizard]'); if (soulmate) { let step = 1; const totalSteps = Number(soulmate.dataset.soulmateSteps || 6); const progress = $('[data-soulmate-progress]', soulmate); const kicker = $('[data-soulmate-kicker]', soulmate); const isEn = document.documentElement.lang === 'en'; const showStep = (next) => { step = Number(next); $$('[data-soulmate-pane]', soulmate).forEach((pane) => { const active = Number(pane.dataset.soulmatePane) === step; pane.hidden = !active; pane.classList.toggle('active', active); }); if (progress) progress.style.width = `${(step / totalSteps) * 100}%`; if (kicker) kicker.textContent = isEn ? `Question ${step} of ${totalSteps}` : `Frage ${step} von ${totalSteps}`; soulmate.scrollIntoView({behavior:'smooth', block:'start'}); }; const checkedCount = (name) => $$(`input[name="${name}[]"]:checked`, soulmate).length; const showError = (text) => { const pane = $(`[data-soulmate-pane="${step}"]`, soulmate); const error = pane ? $('[data-soulmate-error]', pane) : null; if (error) { error.textContent = text; error.hidden = false; } }; const clearErrors = () => $$('[data-soulmate-error]', soulmate).forEach((e) => {e.hidden=true;e.textContent='';}); const validateStep = () => { clearErrors(); if (step === 2) { const age = Number($('input[name="age"]', soulmate)?.value || 0); if (!Number.isInteger(age) || age < 18 || age > 99) { showError(isEn ? 'Please enter your age between 18 and 99 years.' : 'Bitte trage dein Alter zwischen 18 und 99 Jahren ein.'); return false; } } if (step === 4 && checkedCount('presence') < 1) { showError(isEn ? 'Please choose at least one presence.' : 'Bitte wähle mindestens eine Ausstrahlung aus.'); return false; } if (step === 5 && checkedCount('connection') < 1) { showError(isEn ? 'Please choose at least one quality for the connection.' : 'Bitte wähle mindestens eine Qualität für die Verbindung aus.'); return false; } if (step === 6) { const influence = $('input[name="visual_influence"]:checked', soulmate)?.value || 'wenig'; if (influence === 'deutlich' && checkedCount('visual') < 1) { showError(isEn ? 'Please choose at least one visual feature.' : 'Bitte wähle mindestens ein optisches Merkmal aus.'); return false; } } if (step === 7 && checkedCount('mood') < 1) { showError(isEn ? 'Please choose at least one mood.' : 'Bitte wähle mindestens eine Stimmung aus.'); return false; } return true; }; const updateVisual = () => { const influence = $('input[name="visual_influence"]:checked', soulmate)?.value || 'wenig'; const options = $('[data-soulmate-visual-options]', soulmate); if (options) options.hidden = influence === 'keine'; if (influence === 'keine') $$('input[name="visual[]"]', soulmate).forEach((input) => {input.checked=false;}); }; const updateChips = () => { $$('.soulmate-chip', soulmate).forEach((label) => label.classList.toggle('selected', Boolean($('input', label)?.checked))); }; soulmate.addEventListener('change', () => { clearErrors(); updateVisual(); updateChips(); }); $$('[data-soulmate-next]', soulmate).forEach((button) => button.addEventListener('click', () => { if (validateStep()) showStep(Number(button.dataset.soulmateNext)); })); $$('[data-soulmate-back]', soulmate).forEach((button) => button.addEventListener('click', () => { clearErrors(); showStep(Number(button.dataset.soulmateBack)); })); soulmate.addEventListener('submit', (event) => { step = totalSteps; if (!validateStep()) { event.preventDefault(); showStep(totalSteps); return; } const submit = $('[data-soulmate-submit]', soulmate); if (submit) { submit.disabled = true; submit.textContent = isEn ? 'Your portrait is being created …' : 'Dein Porträt entsteht …'; } }); updateVisual(); updateChips(); } // v1.54: use the visitor's browser timezone silently and explain it in everyday language. try { const horoscopeTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; $$('.pv-horoscope-timezone').forEach((input) => { input.value = horoscopeTimezone; }); const locale = document.documentElement.lang === 'en' ? 'en' : 'de'; const localClock = new Intl.DateTimeFormat(locale === 'en' ? 'en-GB' : 'de-DE', { hour: '2-digit', minute: '2-digit', timeZone: horoscopeTimezone }).format(new Date()); $$('.pv-horoscope-localtime').forEach((node) => { node.textContent = locale === 'en' ? `Automatically detected from your device: local time ${localClock}. No UTC setting is needed.` : `Automatisch über dein Gerät erkannt: Ortszeit ${localClock} Uhr. Du musst keine UTC-Zeit auswählen.`; }); } catch (error) { // Keep the server-side safe fallback and the explanatory default text. } // v1.66: Direct horoscope entry must land on the actual calculation form even after large hero images finish loading. try { const directUrl = new URL(window.location.href); if (directUrl.searchParams.get('start') === '1') { window.addEventListener('load', () => { const form = document.getElementById('berechnen'); if (form) window.setTimeout(() => form.scrollIntoView({behavior:'auto', block:'start'}), 120); }, {once:true}); } } catch (error) {} const dialog = $('[data-tableau-dialog]'); if (dialog) { $$('[data-tableau-card]').forEach((card) => card.addEventListener('click', () => { const set = (sel, value) => { const el=$(sel,dialog); if(el) el.textContent=value || ''; }; set('[data-dialog-position]', `${document.documentElement.lang === 'en' ? 'Position' : 'Position'} ${card.dataset.position}`); set('[data-dialog-number]', card.dataset.number); set('[data-dialog-card]', card.dataset.card); set('[data-dialog-house]', card.dataset.house); if (typeof dialog.showModal === 'function') dialog.showModal(); else dialog.setAttribute('open',''); })); $('[data-tableau-dialog-close]', dialog)?.addEventListener('click', () => dialog.close ? dialog.close() : dialog.removeAttribute('open')); dialog.addEventListener('click', (event) => { if (event.target === dialog && dialog.close) dialog.close(); }); } })(); // v1.69 – animal reading example questions addEventListener('click',function(e){const b=e.target.closest('[data-animal-question]');if(!b)return;const form=b.closest('.animal-reading-form');const q=form&&form.querySelector('textarea[name="question"]');if(q){q.value=b.textContent.trim();q.focus();}}); // v1.70 – Chinese five-element explorer (() => { document.querySelectorAll('[data-element-explorer]').forEach((explorer) => { const tabs = Array.from(explorer.querySelectorAll('[data-element-tab]')); const panels = Array.from(explorer.querySelectorAll('[data-element-panel]')); if (!tabs.length || !panels.length) return; const activate = (key, focus = false) => { tabs.forEach((tab) => { const active = tab.dataset.elementTab === key; tab.classList.toggle('is-active', active); tab.setAttribute('aria-selected', active ? 'true' : 'false'); tab.tabIndex = active ? 0 : -1; if (active && focus) tab.focus(); }); panels.forEach((panel) => { const active = panel.dataset.elementPanel === key; panel.hidden = !active; panel.classList.toggle('is-active', active); }); }; tabs.forEach((tab, index) => { tab.addEventListener('click', () => activate(tab.dataset.elementTab || 'wood')); tab.addEventListener('keydown', (event) => { if (!['ArrowLeft','ArrowRight','Home','End'].includes(event.key)) return; event.preventDefault(); let next = index; if (event.key === 'ArrowRight') next = (index + 1) % tabs.length; if (event.key === 'ArrowLeft') next = (index - 1 + tabs.length) % tabs.length; if (event.key === 'Home') next = 0; if (event.key === 'End') next = tabs.length - 1; activate(tabs[next].dataset.elementTab || 'wood', true); }); }); }); })(); // v1.80 · Tier-Suchkompass: Fotoausschnitt + robuste Profilvorschau (() => { const q = (selector, root=document) => root.querySelector(selector); // v1.262 · Globaler Wartehinweis fuer echte Auswertungen / Generierungen. // Login, Profil, PayPal, Admin-Aktionen usw. werden bewusst nicht ueberlagert. const pvEvaluationActions = new Set([ 'prepare-animal-search-followup', 'prepare-animal-search', 'prepare-animal-reading', 'prepare-yesno', 'prepare-tableau', 'draw', 'prepare-reading', 'reveal-yesno', 'reveal-reading', 'reveal-animal-reading', 'reveal-animal-search', 'reanalyze-animal-search', 'reveal-tableau', 'soulmate', 'soulmate-compass', 'horoscope-generate', 'natal-chart-generate', 'chinese-horoscope-generate', 'reading-chat-advisor', 'reading-chat-send', 'reading-chat-draw' ]); let pvEvaluationTimer = 0; const pvEvaluationOverlay = () => document.getElementById('pv-evaluation-wait'); const pvHideEvaluationWait = () => { const overlay = pvEvaluationOverlay(); if (pvEvaluationTimer) { window.clearInterval(pvEvaluationTimer); pvEvaluationTimer = 0; } if (overlay) overlay.hidden = true; document.documentElement.classList.remove('pv-evaluation-wait-active'); }; const pvShowEvaluationWait = () => { const overlay = pvEvaluationOverlay(); if (!overlay) return; const stage = overlay.querySelector('.pv-evaluation-wait-stage'); const stages = stage ? [1,2,3,4].map((n) => stage.getAttribute(`data-stage-${n}`)).filter(Boolean) : []; let index = 0; if (stage && stages.length) stage.textContent = stages[0]; overlay.hidden = false; document.documentElement.classList.add('pv-evaluation-wait-active'); if (stages.length > 1) { if (pvEvaluationTimer) window.clearInterval(pvEvaluationTimer); pvEvaluationTimer = window.setInterval(() => { index = Math.min(index + 1, stages.length - 1); if (stage) stage.textContent = stages[index]; if (index >= stages.length - 1 && pvEvaluationTimer) { window.clearInterval(pvEvaluationTimer); pvEvaluationTimer = 0; } }, 2200); } }; window.addEventListener('pageshow', pvHideEvaluationWait); const pvAnimalSearchAutoResultActions = new Set(['reveal-animal-search']); const pvAnimalSearchResultUrl = (rawUrl) => { try { const url = new URL(rawUrl, window.location.href); return url.searchParams.get('id') ? `${url.pathname}${url.search}#ergebnis` : ''; } catch (_) { return ''; } }; const pvPollAnimalSearchCompletion = async () => { const pollUrl = window.location.pathname; const response = await fetch(pollUrl, { method: 'GET', credentials: 'same-origin', cache: 'no-store', headers: { 'X-PV-Animal-Search-Poll': '1' } }); const resultUrl = pvAnimalSearchResultUrl(response.url); if (resultUrl) { window.location.assign(resultUrl); return true; } return false; }; document.addEventListener('submit', (ev) => { if (ev.defaultPrevented) return; const form = ev.target.closest && ev.target.closest('form'); if (!form) return; const button = form.querySelector('button[type="submit"]'); const hasWaitFlag = form.matches('[data-pv-wait-form]'); if (hasWaitFlag && button && !button.disabled) { button.dataset.originalLabel = button.textContent; const wait = button.getAttribute('data-wait-label'); if (wait) button.textContent = wait; button.disabled = true; } const action = form.querySelector('input[name="action"]')?.value || ''; if (!hasWaitFlag && !pvEvaluationActions.has(action)) return; if (button && !button.disabled) button.disabled = true; pvShowEvaluationWait(); // The animal-search interpretation can finish on the server while the original POST // is still waiting for the PHP/FastCGI response. Keep the current page alive, submit // asynchronously, and poll the compass page until the freshly saved result is visible. // This removes the need for a manual browser refresh without drawing new cards. if (pvAnimalSearchAutoResultActions.has(action) && form.method.toLowerCase() === 'post') { ev.preventDefault(); const actionUrl = form.getAttribute('action') || window.location.href; let finished = false; let pollTimer = 0; let stopTimer = 0; const cleanup = () => { finished = true; if (pollTimer) window.clearInterval(pollTimer); if (stopTimer) window.clearTimeout(stopTimer); }; const check = async () => { if (finished || document.hidden) return; try { if (await pvPollAnimalSearchCompletion()) cleanup(); } catch (_) {} }; window.setTimeout(check, 2500); pollTimer = window.setInterval(check, 4000); stopTimer = window.setTimeout(() => { cleanup(); // Fallback only after the server's normal processing window. If a result was // saved meanwhile, this ordinary reload will open it via last_completed. window.location.reload(); }, 430000); fetch(actionUrl, { method: 'POST', body: new FormData(form), credentials: 'same-origin', cache: 'no-store', redirect: 'follow' }).then((response) => { if (finished) return; const resultUrl = pvAnimalSearchResultUrl(response.url); if (resultUrl) { cleanup(); window.location.assign(resultUrl); return; } // A detached FastCGI request normally returns the processing page immediately. // Polling continues until the completed result has been stored. check(); }).catch(() => { // Do not discard a server-side job merely because the browser request ended. // Polling can still pick up the saved result. check(); }); } }); const preview=q('#pv-animal-photo-preview'); const fileInput=q('#pv-animal-photo-input'); const editButton=q('#pv-animal-photo-edit'); const removeButton=q('#pv-animal-photo-remove'); const removeInput=q('#pv-remove-animal-photo'); const dialog=q('#pv-animal-photo-editor'); const canvas=q('#pv-photo-crop-canvas'); const zoomInput=q('#pv-photo-zoom'); const closeButton=q('[data-photo-editor-close]'); const centerButton=q('[data-photo-editor-center]'); const originalButton=q('[data-photo-editor-original]'); const applyButton=q('[data-photo-editor-apply]'); const ctx=canvas && canvas.getContext ? canvas.getContext('2d') : null; let originalFile=null; let sourceImage=null; let objectUrl=''; let previewUrl=''; let zoom=1; let offsetX=0; let offsetY=0; let dragging=false; let dragPointer=null; let dragStartX=0; let dragStartY=0; let dragOriginX=0; let dragOriginY=0; const revoke = (url) => { if(url) try{URL.revokeObjectURL(url);}catch(_){} }; const setPreview = (src, isPlaceholder=false) => { if(!preview || !src) return; if(previewUrl && previewUrl!==src) revoke(previewUrl); previewUrl = src.startsWith('blob:') ? src : ''; preview.src=src; preview.classList.toggle('is-placeholder', !!isPlaceholder); }; const placeholderFor = (speciesValue) => { if(!preview) return ''; const key=['cat','dog','other'].includes(speciesValue)?speciesValue:'cat'; return preview.getAttribute('data-'+key) || preview.getAttribute('data-cat') || ''; }; const updateAnimalPreview = (speciesValue, explicitPhoto='', hasPhoto=false) => { if(!preview) return; if(explicitPhoto){ setPreview(explicitPhoto, !hasPhoto); return; } setPreview(placeholderFor(speciesValue), true); }; const canvasMetrics = () => { if(!canvas || !sourceImage) return null; const cw=canvas.width, ch=canvas.height; const iw=sourceImage.naturalWidth || sourceImage.width; const ih=sourceImage.naturalHeight || sourceImage.height; if(!iw || !ih) return null; const base=Math.max(cw/iw,ch/ih); const scale=base*zoom; const dw=iw*scale, dh=ih*scale; const maxX=Math.max(0,(dw-cw)/2); const maxY=Math.max(0,(dh-ch)/2); offsetX=Math.max(-maxX,Math.min(maxX,offsetX)); offsetY=Math.max(-maxY,Math.min(maxY,offsetY)); return {cw,ch,dw,dh,x:(cw-dw)/2+offsetX,y:(ch-dh)/2+offsetY}; }; const drawCrop = () => { if(!ctx || !sourceImage || !canvas) return; const m=canvasMetrics(); if(!m) return; ctx.clearRect(0,0,m.cw,m.ch); const grd=ctx.createRadialGradient(m.cw*.5,m.ch*.46,0,m.cw*.5,m.ch*.5,m.cw*.72); grd.addColorStop(0,'#281735'); grd.addColorStop(1,'#0d0715'); ctx.fillStyle=grd; ctx.fillRect(0,0,m.cw,m.ch); ctx.drawImage(sourceImage,m.x,m.y,m.dw,m.dh); }; const openEditor = () => { if(!dialog || !sourceImage) return; drawCrop(); if(typeof dialog.showModal==='function' && !dialog.open) dialog.showModal(); else dialog.setAttribute('open',''); }; const closeEditor = () => { if(!dialog) return; if(typeof dialog.close==='function' && dialog.open) dialog.close(); else dialog.removeAttribute('open'); }; const assignFile = (file) => { if(!fileInput || !file) return false; try{ const transfer=new DataTransfer(); transfer.items.add(file); fileInput.files=transfer.files; return true; }catch(_){ return false; } }; const showOriginalPreview = () => { if(!originalFile) return; const url=URL.createObjectURL(originalFile); setPreview(url,false); assignFile(originalFile); if(editButton) editButton.hidden=false; }; const loadPhotoFile = (file, autoOpen=true) => { if(!file || !file.type || !file.type.startsWith('image/')) return; if(removeInput) removeInput.value='0'; originalFile=file; revoke(objectUrl); objectUrl=URL.createObjectURL(file); const img=new Image(); img.onload=()=>{ sourceImage=img; zoom=1; offsetX=0; offsetY=0; if(zoomInput) zoomInput.value='1'; showOriginalPreview(); drawCrop(); if(autoOpen) openEditor(); }; img.onerror=()=>{ sourceImage=null; }; img.src=objectUrl; }; if(fileInput) fileInput.addEventListener('change',()=>{ const file=fileInput.files && fileInput.files[0]; if(file) loadPhotoFile(file,true); }); if(editButton) editButton.addEventListener('click',openEditor); if(closeButton) closeButton.addEventListener('click',closeEditor); if(centerButton) centerButton.addEventListener('click',()=>{offsetX=0;offsetY=0;zoom=1;if(zoomInput)zoomInput.value='1';drawCrop();}); if(originalButton) originalButton.addEventListener('click',()=>{showOriginalPreview();closeEditor();}); if(zoomInput) zoomInput.addEventListener('input',()=>{zoom=Math.max(1,Math.min(3,Number(zoomInput.value)||1));drawCrop();}); if(canvas){ canvas.addEventListener('pointerdown',(ev)=>{ if(!sourceImage) return; dragging=true; dragPointer=ev.pointerId; canvas.setPointerCapture?.(ev.pointerId); dragStartX=ev.clientX; dragStartY=ev.clientY; dragOriginX=offsetX; dragOriginY=offsetY; canvas.classList.add('is-dragging'); }); canvas.addEventListener('pointermove',(ev)=>{ if(!dragging || ev.pointerId!==dragPointer) return; const rect=canvas.getBoundingClientRect(); const sx=canvas.width/Math.max(1,rect.width), sy=canvas.height/Math.max(1,rect.height); offsetX=dragOriginX+(ev.clientX-dragStartX)*sx; offsetY=dragOriginY+(ev.clientY-dragStartY)*sy; drawCrop(); }); const endDrag=(ev)=>{ if(!dragging || (ev.pointerId!==undefined && ev.pointerId!==dragPointer)) return; dragging=false; dragPointer=null; canvas.classList.remove('is-dragging'); }; canvas.addEventListener('pointerup',endDrag); canvas.addEventListener('pointercancel',endDrag); } if(applyButton) applyButton.addEventListener('click',()=>{ if(!canvas || !sourceImage || !originalFile) return; drawCrop(); applyButton.disabled=true; canvas.toBlob((blob)=>{ applyButton.disabled=false; if(!blob){ closeEditor(); return; } const base=(originalFile.name||'tierfoto').replace(/\.[^.]+$/,'').slice(0,80) || 'tierfoto'; const cropped=new File([blob],base+'-ausschnitt.jpg',{type:'image/jpeg',lastModified:Date.now()}); if(assignFile(cropped)){ if(removeInput) removeInput.value='0'; const url=URL.createObjectURL(blob); setPreview(url,false); if(editButton) editButton.hidden=false; } closeEditor(); },'image/jpeg',0.92); }); if(removeButton) removeButton.addEventListener('click',()=>{ if(fileInput) fileInput.value=''; originalFile=null; sourceImage=null; zoom=1; offsetX=0; offsetY=0; if(removeInput) removeInput.value='1'; if(editButton) editButton.hidden=true; const species=q('#pv-animal-species')?.value||'cat'; updateAnimalPreview(species,'',false); closeEditor(); }); document.addEventListener('change', (ev) => { if (ev.target && ev.target.id === 'pv-animal-profile') { const opt = ev.target.options[ev.target.selectedIndex]; const name=q('#pv-animal-name'), species=q('#pv-animal-species'); if(opt && opt.value){ if(name) name.value=opt.dataset.name||''; if(species && opt.dataset.species) species.value=opt.dataset.species; updateAnimalPreview(opt.dataset.species||'cat',opt.dataset.photo||'',opt.dataset.hasPhoto==='1'); if(fileInput) fileInput.value=''; originalFile=null; sourceImage=null; if(removeInput) removeInput.value='0'; if(editButton) editButton.hidden=true; }else{ if(removeInput) removeInput.value='0'; updateAnimalPreview(species?.value||'cat'); } } if (ev.target && ev.target.id === 'pv-animal-species') { if(!originalFile && (!preview || preview.classList.contains('is-placeholder'))) updateAnimalPreview(ev.target.value); } }); window.addEventListener('pagehide',()=>{revoke(objectUrl);revoke(previewUrl);}); })(); // v1.89 · Historical search cases: blind-test outcome is revealed only after interpretation. (() => { const form=document.querySelector('.pv-animal-search-page .pv-search-form'); if(!form) return; const historicalFields=form.querySelector('[data-historical-fields]'); const radios=[...form.querySelectorAll('input[name="search_mode"]')]; const required=historicalFields ? [...historicalFields.querySelectorAll('[data-historical-required]')] : []; const sync=()=>{ const mode=radios.find(r=>r.checked)?.value||'current'; const historical=mode==='historical'; if(historicalFields) historicalFields.hidden=!historical; required.forEach(el=>{el.required=historical; if(!historical && el.tagName==='SELECT') el.value='';}); }; radios.forEach(r=>r.addEventListener('change',sync)); sync(); })(); window.pvInitAnimalSearchMapbox = function () { const page=document.querySelector('.pv-animal-search-page[data-mapbox-token]'); const token=page ? (page.getAttribute('data-mapbox-token')||'').trim() : ''; if (!window.mapboxgl || !token) return; mapboxgl.accessToken = token; const serverCountry = () => { const direct=(page?.getAttribute('data-country-hint')||'').trim().toUpperCase(); return /^[A-Z]{2}$/.test(direct) ? direct : ''; }; const neutralFallback = () => ({center:[20,35],zoom:3.45}); const applyRegionalStart = async (map, canApply=()=>true) => { const code=serverCountry(); if(!code){ const f=neutralFallback(); if(canApply()) map.jumpTo(f); return; } try{ const display=typeof Intl.DisplayNames==='function' ? new Intl.DisplayNames([document.documentElement.lang||'en'],{type:'region'}).of(code) : code; const url='https://api.mapbox.com/search/geocode/v6/forward?q='+encodeURIComponent(display||code)+'&types=country&limit=1&autocomplete=false&access_token='+encodeURIComponent(token); const res=await fetch(url,{headers:{'Accept':'application/json'}}); const data=res.ok?await res.json():null; const f=data&&data.features&&data.features[0]; const bbox=f&&f.properties&&f.properties.bbox || f&&f.bbox; if(Array.isArray(bbox)&&bbox.length===4){ if(canApply()) map.fitBounds([[Number(bbox[0]),Number(bbox[1])],[Number(bbox[2]),Number(bbox[3])]],{padding:36,duration:0,maxZoom:6.8}); return; } const c=f&&f.geometry&&f.geometry.coordinates; if(Array.isArray(c)&&c.length>=2){ if(canApply()) map.jumpTo({center:[Number(c[0]),Number(c[1])],zoom:4.6}); return; } }catch(_){ } const f=neutralFallback(); if(canApply()) map.jumpTo(f); }; const destination = (lat, lng, bearing, meters) => { const R = 6378137; const br = bearing * Math.PI / 180; const p1 = lat * Math.PI / 180; const l1 = lng * Math.PI / 180; const dr = meters / R; const p2 = Math.asin(Math.sin(p1) * Math.cos(dr) + Math.cos(p1) * Math.sin(dr) * Math.cos(br)); const l2 = l1 + Math.atan2(Math.sin(br) * Math.sin(dr) * Math.cos(p1), Math.cos(dr) - Math.sin(p1) * Math.sin(p2)); return [l2 * 180 / Math.PI, p2 * 180 / Math.PI]; }; const circleLine = (lat, lng, meters, steps=96) => { const coords=[]; for(let a=0;a<=360;a+=360/steps) coords.push(destination(lat,lng,a,meters)); return {type:'Feature',properties:{},geometry:{type:'LineString',coordinates:coords}}; }; const sectorPolygon = (lat,lng,index,meters) => { const angle=Number(index)*30; const coords=[[lng,lat]]; for(let a=angle-15;a<=angle+15;a+=2.5) coords.push(destination(lat,lng,a,meters)); coords.push([lng,lat]); return {type:'Feature',properties:{},geometry:{type:'Polygon',coordinates:[coords]}}; }; const addSearchOverlay = (map, d) => { const lat=Number(d.lat), lng=Number(d.lng); if(!Number.isFinite(lat)||!Number.isFinite(lng)) return; const radiusKey=['near','medium','far'].includes(d.radius)?d.radius:'medium'; const radii={near:550,medium:1600,far:4200}; const selectedIndex={near:0,medium:1,far:2}[radiusKey]; [radii.near,radii.medium,radii.far].forEach((r,i)=>{ const id='pv-radius-'+i; map.addSource(id,{type:'geojson',data:circleLine(lat,lng,r)}); map.addLayer({id,type:'line',source:id,paint:{'line-color':'#C3A56B','line-width':i===selectedIndex?3:1.2,'line-opacity':i===selectedIndex?.9:.28,'line-dasharray':i===selectedIndex?[1,0.01]:[2,2]}}); }); const addSector=(index,secondary=false)=>{ if(index===null||index===undefined||!Number.isFinite(Number(index))) return; const id=secondary?'pv-sector-secondary':'pv-sector-primary'; map.addSource(id,{type:'geojson',data:sectorPolygon(lat,lng,Number(index),radii[radiusKey])}); map.addLayer({id,type:'fill',source:id,paint:{'fill-color':'#C3A56B','fill-opacity':secondary?.10:.31}}); map.addLayer({id:id+'-outline',type:'line',source:id,paint:{'line-color':'#D8BA75','line-width':secondary?1.5:3.2,'line-opacity':secondary?.62:1}}); }; addSector(d.primary,false); addSector(d.secondary,true); }; const picker=document.getElementById('pv-animal-search-picker-map'); if(picker && !picker.dataset.mapReady){ picker.dataset.mapReady='1'; const latInput=document.getElementById('pv-origin-lat'); const lngInput=document.getElementById('pv-origin-lng'); const labelInput=document.getElementById('pv-origin-label'); const status=document.getElementById('pv-origin-point-status'); const useCenterBtn=document.getElementById('pv-map-use-center'); const initial=neutralFallback(); const map=new mapboxgl.Map({container:picker,style:'mapbox://styles/mapbox/streets-v12',center:initial.center,zoom:initial.zoom,bearing:0,pitch:0,projection:'mercator',attributionControl:true,interactive:true,dragPan:true,scrollZoom:true,doubleClickZoom:true,touchZoomRotate:true,keyboard:true,dragRotate:false,pitchWithRotate:false}); map.addControl(new mapboxgl.NavigationControl({visualizePitch:false}),'top-right'); map.dragPan.enable(); map.scrollZoom.enable(); map.doubleClickZoom.enable(); map.touchZoomRotate.enable(); if(map.touchZoomRotate&&typeof map.touchZoomRotate.disableRotation==='function') map.touchZoomRotate.disableRotation(); map.keyboard.enable(); map.setBearing(0); let userPositioned=false; map.once('load',()=>applyRegionalStart(map,()=>!userPositioned)); let marker=null; const updateStatus=(lat,lng)=>{ if(status){ const prefix=status.dataset.setLabel||'Punkt gesetzt'; status.textContent=`${prefix} · ${Number(lat).toFixed(5)}, ${Number(lng).toFixed(5)}`; status.classList.add('is-set'); } }; const setPoint=(lng,lat,fromClick=false)=>{ userPositioned=true; if(!marker){ marker=new mapboxgl.Marker({color:'#C3A56B',draggable:true}).setLngLat([lng,lat]).addTo(map); marker.on('dragend',()=>{ const p=marker.getLngLat(); setPoint(p.lng,p.lat,false); }); }else marker.setLngLat([lng,lat]); if(latInput) latInput.value=Number(lat).toFixed(7); if(lngInput) lngInput.value=Number(lng).toFixed(7); updateStatus(lat,lng); if(fromClick && labelInput && !labelInput.value.trim()) labelInput.value=`Kartenpunkt ${Number(lat).toFixed(5)}, ${Number(lng).toFixed(5)}`; }; map.on('click',(e)=>setPoint(e.lngLat.lng,e.lngLat.lat,true)); useCenterBtn?.addEventListener('click',()=>{ const c=map.getCenter(); setPoint(c.lng,c.lat,true); }); const btn=document.getElementById('pv-map-geocode'); if(btn) btn.addEventListener('click',async()=>{ const q=(labelInput&&labelInput.value||'').trim(); if(!q) return; const original=btn.textContent; btn.disabled=true; btn.textContent=btn.dataset.waitLabel||'…'; try{ const url='https://api.mapbox.com/search/geocode/v6/forward?q='+encodeURIComponent(q)+'&limit=1&autocomplete=false&access_token='+encodeURIComponent(token); const res=await fetch(url,{headers:{'Accept':'application/json'}}); if(!res.ok) throw new Error('geocoding'); const data=await res.json(); const f=data&&data.features&&data.features[0]; const coords=f&&f.geometry&&f.geometry.coordinates; if(!Array.isArray(coords)||coords.length<2) throw new Error('not-found'); const lng=Number(coords[0]),lat=Number(coords[1]); setPoint(lng,lat,false); map.flyTo({center:[lng,lat],zoom:16,essential:true}); }catch(err){ btn.classList.add('is-error'); window.setTimeout(()=>btn.classList.remove('is-error'),1600); }finally{ btn.disabled=false;btn.textContent=original; } }); } const tracePicker=document.getElementById('pv-animal-search-trace-map'); if(tracePicker && !tracePicker.dataset.mapReady){ tracePicker.dataset.mapReady='1'; const latInput=document.getElementById('pv-trace-origin-lat'); const lngInput=document.getElementById('pv-trace-origin-lng'); const labelInput=document.getElementById('pv-trace-origin-label'); const status=document.getElementById('pv-trace-point-status'); const useCenterBtn=document.getElementById('pv-trace-map-use-center'); const initial=neutralFallback(); const startLat=Number(tracePicker.dataset.startLat),startLng=Number(tracePicker.dataset.startLng); const hasStart=Number.isFinite(startLat)&&Number.isFinite(startLng); if(hasStart){ initial.center=[startLng,startLat]; initial.zoom=15; } const map=new mapboxgl.Map({container:tracePicker,style:'mapbox://styles/mapbox/streets-v12',center:initial.center,zoom:initial.zoom,bearing:0,pitch:0,projection:'mercator',attributionControl:true,interactive:true,dragPan:true,scrollZoom:true,doubleClickZoom:true,touchZoomRotate:true,keyboard:true,dragRotate:false,pitchWithRotate:false}); map.addControl(new mapboxgl.NavigationControl({visualizePitch:false}),'top-right'); map.dragPan.enable(); map.scrollZoom.enable(); map.doubleClickZoom.enable(); map.touchZoomRotate.enable(); if(map.touchZoomRotate&&typeof map.touchZoomRotate.disableRotation==='function') map.touchZoomRotate.disableRotation(); map.keyboard.enable(); map.setBearing(0); let userPositioned=false; map.once('load',()=>{ if(!hasStart) applyRegionalStart(map,()=>!userPositioned); }); let marker=null; const updateStatus=(lat,lng)=>{ if(status){ const prefix=status.dataset.setLabel||'Punkt gesetzt'; status.textContent=`${prefix} · ${Number(lat).toFixed(5)}, ${Number(lng).toFixed(5)}`; status.classList.add('is-set'); } }; const setPoint=(lng,lat,fromClick=false)=>{ userPositioned=true; if(!marker){ marker=new mapboxgl.Marker({color:'#C3A56B',draggable:true}).setLngLat([lng,lat]).addTo(map); marker.on('dragend',()=>{ const p=marker.getLngLat(); setPoint(p.lng,p.lat,false); }); }else marker.setLngLat([lng,lat]); if(latInput) latInput.value=Number(lat).toFixed(7); if(lngInput) lngInput.value=Number(lng).toFixed(7); updateStatus(lat,lng); if(fromClick && labelInput && !labelInput.value.trim()) labelInput.value=`Kartenpunkt ${Number(lat).toFixed(5)}, ${Number(lng).toFixed(5)}`; }; map.on('click',(e)=>setPoint(e.lngLat.lng,e.lngLat.lat,true)); useCenterBtn?.addEventListener('click',()=>{ const c=map.getCenter(); setPoint(c.lng,c.lat,true); }); const btn=document.getElementById('pv-trace-map-geocode'); if(btn) btn.addEventListener('click',async()=>{ const q=(labelInput&&labelInput.value||'').trim(); if(!q) return; const original=btn.textContent; btn.disabled=true; btn.textContent=btn.dataset.waitLabel||'…'; try{ const url='https://api.mapbox.com/search/geocode/v6/forward?q='+encodeURIComponent(q)+'&limit=1&autocomplete=false&access_token='+encodeURIComponent(token); const res=await fetch(url,{headers:{'Accept':'application/json'}}); if(!res.ok) throw new Error('geocoding'); const data=await res.json(); const f=data&&data.features&&data.features[0]; const coords=f&&f.geometry&&f.geometry.coordinates; if(!Array.isArray(coords)||coords.length<2) throw new Error('not-found'); const lng=Number(coords[0]),lat=Number(coords[1]); setPoint(lng,lat,false); map.flyTo({center:[lng,lat],zoom:16,essential:true}); }catch(err){ btn.classList.add('is-error'); window.setTimeout(()=>btn.classList.remove('is-error'),1600); }finally{ btn.disabled=false;btn.textContent=original; } }); } const resultEl=document.getElementById('pv-animal-search-result-map'); if(resultEl && !resultEl.dataset.mapReady){ let d={}; try{d=JSON.parse(resultEl.dataset.map||'{}');}catch(e){} const lat=Number(d.lat),lng=Number(d.lng); if(Number.isFinite(lat)&&Number.isFinite(lng)){ resultEl.dataset.mapReady='1'; const map=new mapboxgl.Map({container:resultEl,style:'mapbox://styles/mapbox/streets-v12',center:[lng,lat],zoom:14,bearing:0,pitch:0,projection:'mercator',attributionControl:true,interactive:true,dragPan:true,scrollZoom:true,doubleClickZoom:true,touchZoomRotate:true,keyboard:true,dragRotate:false,pitchWithRotate:false}); map.addControl(new mapboxgl.NavigationControl({visualizePitch:false}),'top-right'); map.addControl(new mapboxgl.FullscreenControl({container:resultEl.parentElement||undefined}),'top-right'); map.dragPan.enable(); map.scrollZoom.enable(); map.doubleClickZoom.enable(); map.touchZoomRotate.enable(); if(map.touchZoomRotate&&typeof map.touchZoomRotate.disableRotation==='function') map.touchZoomRotate.disableRotation(); map.keyboard.enable(); map.setBearing(0); new mapboxgl.Marker({color:'#C3A56B'}).setLngLat([lng,lat]).setPopup(new mapboxgl.Popup({offset:20}).setText(resultEl.dataset.originLabel||'Ausgangspunkt')).addTo(map); const radii={near:550,medium:1600,far:4200}; const radiusKey=['near','medium','far'].includes(d.radius)?d.radius:'medium'; const fitDistance=(meters,padding=54,duration=500)=>{ const bounds=new mapboxgl.LngLatBounds([lng,lat],[lng,lat]); [0,45,90,135,180,225,270,315].forEach(a=>bounds.extend(destination(lat,lng,a,meters))); map.fitBounds(bounds,{padding,duration,maxZoom:17}); }; const fitSelected=()=>fitDistance(Math.max(700,radii[radiusKey]*1.45),58,450); const fitAll=()=>fitDistance(radii.far*1.08,48,450); map.on('load',()=>{ addSearchOverlay(map,d); fitSelected(); }); const wrap=resultEl.closest('.pv-search-result-map-wrap'); const focusBtn=wrap?.querySelector('[data-pv-map-focus]'); const allBtn=wrap?.querySelector('[data-pv-map-all]'); const fullBtn=wrap?.querySelector('[data-pv-map-fullscreen]'); focusBtn?.addEventListener('click',fitSelected); allBtn?.addEventListener('click',fitAll); fullBtn?.addEventListener('click',async()=>{ try{ const target=wrap||resultEl; if(document.fullscreenElement){ await document.exitFullscreen(); } else if(target.requestFullscreen){ await target.requestFullscreen(); } else { target.classList.toggle('is-map-expanded'); } }catch(_){ (wrap||resultEl).classList.toggle('is-map-expanded'); } window.setTimeout(()=>map.resize(),80); }); document.addEventListener('fullscreenchange',()=>window.setTimeout(()=>map.resize(),80)); const resizeObserver=('ResizeObserver' in window)?new ResizeObserver(()=>map.resize()):null; if(resizeObserver) resizeObserver.observe(wrap||resultEl); } } }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => window.pvInitAnimalSearchMapbox && window.pvInitAnimalSearchMapbox()); } else { window.pvInitAnimalSearchMapbox && window.pvInitAnimalSearchMapbox(); } // v1.244 · Natal chart: resolve birthplace coordinates automatically via configured Mapbox public token. (() => { const form = document.querySelector('form[data-natal-place-geocode]'); if (!form || form.dataset.natalPlaceReady === '1') return; form.dataset.natalPlaceReady = '1'; const token = (form.dataset.mapboxToken || '').trim(); const place = form.querySelector('#pv-natal-birth-place'); const lat = form.querySelector('#pv-natal-birth-lat'); const lon = form.querySelector('#pv-natal-birth-lon'); const status = form.querySelector('#pv-natal-place-status'); const country = form.querySelector('#pv-natal-birth-country'); if (!place || !lat || !lon) return; const lang = document.documentElement.lang === 'en' ? 'en' : 'de'; const labels = lang === 'en' ? { searching: 'Finding birthplace…', ok: 'Place found – coordinates filled automatically.', fail: 'Place could not be matched automatically. Please add the country or use the optional advanced location data.' } : { searching: 'Geburtsort wird gesucht …', ok: 'Ort gefunden – Koordinaten wurden automatisch übernommen.', fail: 'Ort konnte nicht automatisch zugeordnet werden. Bitte ergänze das Land oder nutze freiwillig die erweiterten Ortsdaten.' }; let resolvedPlace = ''; let pending = null; const hasCoordinates = () => Number.isFinite(Number(lat.value)) && Number.isFinite(Number(lon.value)) && lat.value.trim() !== '' && lon.value.trim() !== ''; const setStatus = (text, state = '') => { if (!status) return; status.textContent = text; status.classList.remove('is-searching', 'is-ok', 'is-error'); if (state) status.classList.add('is-' + state); }; const geocode = async (force = false) => { const q = place.value.trim(); if (!token || !q) return false; if (!force && resolvedPlace === q && hasCoordinates()) return true; if (pending) return pending; setStatus(labels.searching, 'searching'); pending = (async () => { try { const url = 'https://api.mapbox.com/search/geocode/v6/forward?q=' + encodeURIComponent(q) + '&limit=1&autocomplete=false&language=' + encodeURIComponent(lang) + '&access_token=' + encodeURIComponent(token); const res = await fetch(url, { headers: { 'Accept': 'application/json' } }); if (!res.ok) throw new Error('geocoding'); const data = await res.json(); const feature = data && data.features && data.features[0]; const coords = feature && feature.geometry && feature.geometry.coordinates; if (!Array.isArray(coords) || coords.length < 2) throw new Error('not-found'); const lng = Number(coords[0]); const ltd = Number(coords[1]); if (!Number.isFinite(lng) || !Number.isFinite(ltd)) throw new Error('invalid'); lon.value = lng.toFixed(6); lat.value = ltd.toFixed(6); if (country) { const ctx = feature && feature.properties && feature.properties.context; const countryCtx = ctx && ctx.country ? ctx.country : null; const rawCountry = countryCtx && (countryCtx.country_code || countryCtx.country_code_alpha_2 || countryCtx.short_code) ? String(countryCtx.country_code || countryCtx.country_code_alpha_2 || countryCtx.short_code) : ''; country.value = rawCountry.replace(/^country\./i, '').slice(-2).toUpperCase(); } resolvedPlace = q; setStatus(labels.ok, 'ok'); return true; } catch (_) { setStatus(labels.fail, 'error'); return false; } finally { pending = null; } })(); return pending; }; place.addEventListener('input', () => { if (place.value.trim() !== resolvedPlace) { resolvedPlace = ''; lat.value = ''; lon.value = ''; if (country) country.value = ''; } }); place.addEventListener('blur', () => { if (place.value.trim()) geocode(false); }); if (token && place.value.trim() && !hasCoordinates()) window.setTimeout(() => geocode(false), 120); let resubmitting = false; form.addEventListener('submit', async (event) => { if (resubmitting || hasCoordinates() || !token) return; event.preventDefault(); const ok = await geocode(true); if (!ok) return; resubmitting = true; if (typeof form.requestSubmit === 'function') form.requestSubmit(); else form.submit(); }); })(); // v1.347 · Leave the Digistore24 thank-you state with native navigation. // The official Digistore24 badge/footer live outside Paravital's modal. // Therefore X and CTA are real links to the clean product URL. This guarantees // a full page load, so the thank-you badge script is not present afterwards. (() => { const overlay=document.querySelector('[data-pv-purchase-success-modal]'); if(!overlay) return; const exitUrl=overlay.getAttribute('data-pv-purchase-success-exit-url')||window.location.pathname; const continueBtn=overlay.querySelector('[data-pv-purchase-success-continue]'); document.body.classList.add('pv-success-modal-open'); // Do not intercept X or CTA clicks: their href values perform a native // document navigation and remove Digistore24's injected badge/footer too. document.addEventListener('keydown',(event)=>{ if(event.key!=='Escape') return; event.preventDefault(); window.location.assign(exitUrl); }); window.setTimeout(()=>continueBtn?.focus({preventScroll:true}),60); })();