PageSourceSearch

https://www.lotte.de/typo3temp/assets/compressed/a11y-71c74757fea73b522f63d7fd6613945d.js?1788781776

js lotte.de collected 2026-10-02 18:59:28 UTC 49,741 bytes, 1,127 lines download raw bytes

1/* ==========================================================================
2   A11Y.JS – Einheitliche Barrierefreiheit-Toolbar
3   Dark Mode · Schriftgroessen · Kontrast · Leichte Sprache · Vorlesen
4   EINZIGE A11y-Toolbar - ersetzt den separaten Text-Switcher aus includes.js
5   ========================================================================== */
6
7(function() {
8  'use strict';
9
10  var STORAGE_KEYS = {
11    theme: 'lotte-theme',
12    fontSize: 'lotte-font-size',
13    contrast: 'lotte-contrast',
14    easyText: 'lotte-easy-text',
15    layout: 'lotte-layout'
16  };
17
18  var FONT_SIZES = ['normal', 'large', 'xlarge'];
19
20  var currentTheme = localStorage.getItem(STORAGE_KEYS.theme) || 'light';
21  var currentFontSize = localStorage.getItem(STORAGE_KEYS.fontSize) || 'normal';
22  var highContrast = localStorage.getItem(STORAGE_KEYS.contrast) === 'true';
23  var easyTextActive = localStorage.getItem(STORAGE_KEYS.easyText) === 'true';
24  // 'full' = no max-width constraint (default), 'boxed' = 1300px centered
25  var currentLayout = localStorage.getItem(STORAGE_KEYS.layout) || 'full';
26  var VOICE_READER_ENDPOINT = '/lotte-voicereader/synthesize';
27  var VOICE_READER_WARM_ENDPOINT = '/lotte-voicereader/warm';
28  var VOICE_READER_MAX_TEXT_LENGTH = 0;
29  var voiceReaderAudio = null;
30  var voiceReaderObjectUrl = null;
31  var voiceReaderLoading = false;
32  var voiceReaderRequestController = null;
33  var voiceReaderWarmTimer = null;
34  var voiceReaderWarmSignature = '';
35  var voiceReaderSpeechUtterance = null;
36  var voiceReaderSpeechQueue = [];
37  var voiceReaderSpeechIndex = 0;
38  var voiceReaderSpeechActive = false;
39  var voiceReaderExtensionQueue = [];
40  var voiceReaderExtensionIndex = 0;
41  var voiceReaderExtensionActive = false;
42  var voiceReaderPlayback = null;
43  var voiceReaderStatusMessage = '';
44  var voiceReaderStatusKind = 'idle';
45  var voiceReaderStatusTimer = null;
46  var voiceReaderGeneration = 0;
47  var voiceReaderPrefetchController = null;
48  var voiceReaderPrefetch = null;
49  var voiceReaderVoiceWaiter = null;
50  var voiceReaderNeedsResume = false;
51
52  // ==========================================================================
53  // APPLY SETTINGS
54  // ==========================================================================
55
56  function applyTheme(theme) {
57    document.documentElement.setAttribute('data-theme', theme);
58    currentTheme = theme;
59    localStorage.setItem(STORAGE_KEYS.theme, theme);
60    updateToolbarState();
61  }
62
63  function applyFontSize(size) {
64    if (size === 'normal') {
65      document.documentElement.removeAttribute('data-font-size');
66    } else {
67      document.documentElement.setAttribute('data-font-size', size);
68    }
69    currentFontSize = size;
70    localStorage.setItem(STORAGE_KEYS.fontSize, size);
71    updateToolbarState();
72  }
73
74  function applyContrast(enabled) {
75    if (enabled) {
76      document.documentElement.setAttribute('data-high-contrast', 'true');
77    } else {
78      document.documentElement.removeAttribute('data-high-contrast');
79    }
80    highContrast = enabled;
81    localStorage.setItem(STORAGE_KEYS.contrast, String(enabled));
82    updateToolbarState();
83  }
84
85  function applyLayout(layout) {
86    // 'boxed' sets data-layout="boxed" on <html>, 'full' removes the attribute
87    if (layout === 'boxed') {
88      document.documentElement.setAttribute('data-layout', 'boxed');
89    } else {
90      document.documentElement.removeAttribute('data-layout');
91    }
92    currentLayout = layout;
93    localStorage.setItem(STORAGE_KEYS.layout, layout);
94    updateToolbarState();
95  }
96
97  function hasEasytextContent(block) {
98    return Boolean(block && ((block.textContent || '').replace(/\s+/g, ' ').trim() || block.querySelector('img, svg, picture, video, iframe')));
99  }
100
101  function getEasytextBlock(group, selector) {
102    var block = group.querySelector(selector);
103    return hasEasytextContent(block) ? block : null;
104  }
105
106  /*
107   * An Easy-language variant must not silently remove an editorial call to
108   * action. Compare the actual link targets that belong to this group; empty
109   * a lone hash placeholder and javascript pseudo-links are presentation
110   * controls, not editorial destinations. Real fragment targets such as
111   * #kontakt remain protected. This intentionally fails closed: an editor can
112   * still use Easy Language for all other complete groups on the page.
113   */
114  function collectSemanticLinkTargets(block, group) {
115    var targets = {};
116    if (!block) {
117      return targets;
118    }
119
120    block.querySelectorAll('a[href]').forEach(function(link) {
121      if (link.closest('[data-mincore-easytext-group]') !== group) {
122        return;
123      }
124
125      var href = (link.getAttribute('href') || '').trim();
126      if (!href || href === '#' || /^javascript:/i.test(href)) {
127        return;
128      }
129
130      try {
131        targets[new URL(href, window.location.href).href] = true;
132      } catch (error) {
133        targets[href] = true;
134      }
135    });
136
137    return targets;
138  }
139
140  function hasCompleteSemanticLinkSet(normal, easy, group) {
141    var originalTargets = collectSemanticLinkTargets(normal, group);
142    var easyTargets = collectSemanticLinkTargets(easy, group);
143
144    return Object.keys(originalTargets).every(function(target) {
145      return easyTargets[target] === true;
146    });
147  }
148
149  function setEasytextBlockVisible(block, visible) {
150    if (block) {
151      block.hidden = !visible;
152    }
153  }
154
155  function applyEasyTextBlocks(enabled) {
156    document.querySelectorAll('[data-mincore-easytext-group]').forEach(function(group) {
157      var normal = getEasytextBlock(group, '[data-level-6="true"]');
158      var easy = getEasytextBlock(group, '[data-simple="true"]');
159      var hasSafeEasyAlternative = group.getAttribute('data-has-easy') === 'true' && normal && easy && hasCompleteSemanticLinkSet(normal, easy, group) && !group.closest('[data-no-simple-language], [data-legal-page], [data-ce-type="min_legal"], [data-easytext-policy="external"], [data-mincore-easytext-policy="external"], [data-external-policy]');
160      var target = enabled && hasSafeEasyAlternative ? easy : normal;
161      var source = target === easy ? 'simple' : 'original';
162
163      group.setAttribute('data-easytext-current-source', source);
164      setEasytextBlockVisible(group.querySelector('[data-level-6="true"]'), target === normal);
165      setEasytextBlockVisible(group.querySelector('[data-level-3="true"]'), false);
166      setEasytextBlockVisible(group.querySelector('[data-simple="true"]'), target === easy);
167    });
168  }
169
170  function isManagedEasytextBlockElement(el) {
171    if (el.closest('[data-mincore-easytext-group]')) {
172      return true;
173    }
174
175    return el.getAttribute('data-level-6') === 'true' ||
176      el.getAttribute('data-level-3') === 'true' ||
177      el.getAttribute('data-simple') === 'true';
178  }
179
180  function applyEasyText(enabled) {
181    easyTextActive = enabled;
182    document.documentElement.setAttribute('data-easy-text', String(enabled));
183    localStorage.setItem(STORAGE_KEYS.easyText, String(enabled));
184    document.body.classList.toggle('simple-language-active', enabled);
185
186    applyEasyTextBlocks(enabled);
187
188    document.querySelectorAll('[data-text-easy], [data-simple], [data-level-3], [data-level-6]').forEach(function(el) {
189      if (isManagedEasytextBlockElement(el)) {
190        return;
191      }
192
193      var normalText = el.getAttribute('data-text-normal') || el.getAttribute('data-lotte-normal-html');
194      if (!normalText) {
195        normalText = el.innerHTML;
196        el.setAttribute('data-lotte-normal-html', normalText);
197      }
198      var easyText = el.getAttribute('data-text-easy') || el.getAttribute('data-simple') || el.getAttribute('data-level-3') || el.getAttribute('data-level-6');
199      if (easyText) {
200        el.innerHTML = enabled ? easyText : normalText;
201      }
202    });
203
204    updateToolbarState();
205  }
206
207  function findVoiceReaderControl() {
208    return document.querySelector('.voicereader-pro[data-vr-initialized="1"] [data-action="play"]');
209  }
210
211  function getVoiceReaderArea() {
212    var areaButton = document.getElementById('voiceReaderArea');
213    return areaButton && areaButton.getAttribute('aria-pressed') === 'true' ? 'main' : 'whole';
214  }
215
216  function getVoiceReaderSource(area) {
217    if (area === 'whole') return document.body;
218    var mainContent = document.getElementById('main-content') || document.querySelector('main') || document.querySelector('article');
219    if (!mainContent) {
220      return null;
221    }
222    return area === 'main' ? mainContent : document.body;
223  }
224
225  function removeUnreadableElements(root, includeSiteChrome) {
226    var selectors = 'script, style, noscript, svg, button, form, [aria-hidden="true"], [hidden], [data-no-readaloud], [data-no-voice-reader], .lotte-a11y-toolbar, .min-style-switcher, .styleswitcher, .voicereader-pro, .visually-hidden, .sr-only';
227    if (!includeSiteChrome) { selectors += ', header, nav, footer, aside, [role="navigation"], [role="banner"], [role="contentinfo"]'; }
228    root.querySelectorAll(selectors).forEach(function(el) { el.remove(); });
229  }
230
231  function extractReadableText(node, includeSiteChrome, visibleOnly) {
232    if (!node) {
233      return '';
234    }
235    var clone = node.cloneNode(true);
236    if (visibleOnly && typeof window.getComputedStyle === 'function') {
237      var originals = [node].concat(Array.prototype.slice.call(node.querySelectorAll('*')));
238      var copies = [clone].concat(Array.prototype.slice.call(clone.querySelectorAll('*')));
239      originals.forEach(function(original, index) {
240        var style = window.getComputedStyle(original);
241        if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse') {
242          if (index === 0) clone.textContent = '';
243          else copies[index].remove();
244        }
245      });
246    }
247    removeUnreadableElements(clone, includeSiteChrome);
248    return (clone.innerText || clone.textContent || '').replace(/\s+/g, ' ').trim();
249  }
250
251  function getStoredEasyText(source) {
252    if (!source || isLegalVoiceReaderPage()) return '';
253    var blocks = [];
254    var exclusions = '[data-no-simple-language], [data-legal-page], [data-ce-type="min_legal"], [data-easytext-policy="external"], [data-mincore-easytext-policy="external"], [data-external-policy]';
255    source.querySelectorAll('[data-mincore-easytext-group]').forEach(function(group) {
256      if (group.closest(exclusions) || group.getAttribute('data-has-easy') !== 'true') return;
257      var normal = getEasytextBlock(group, '[data-level-6="true"]');
258      var easy = getEasytextBlock(group, '[data-simple="true"]');
259      if (normal && easy && hasCompleteSemanticLinkSet(normal, easy, group)) blocks.push(extractReadableText(easy));
260    });
261    source.querySelectorAll('[data-simple="true"]').forEach(function(element) {
262      if (!element.closest('[data-mincore-easytext-group]') && !element.closest(exclusions)) blocks.push(extractReadableText(element));
263    });
264    source.querySelectorAll('[data-text-easy]').forEach(function(element) {
265      if (element.closest('[data-mincore-easytext-group]') || element.closest(exclusions)) return;
266      var storedText = element.getAttribute('data-text-easy');
267      if (storedText && storedText.trim()) {
268        var storedRoot = document.createElement('div');
269        storedRoot.innerHTML = storedText;
270        blocks.push(extractReadableText(storedRoot));
271      }
272    });
273    return blocks.filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
274  }
275
276  function updateVoiceReaderModeAvailability() {
277    var select = document.getElementById('voiceReaderMode');
278    if (!select) return;
279    var available = Boolean(getStoredEasyText(getVoiceReaderSource(getVoiceReaderArea())));
280    var summaryAvailable = Boolean(getStoredPageSummary());
281    if (select.value === 'summary' && summaryAvailable) prepareOriginalSummary();
282    Array.prototype.forEach.call(select.options, function(option) {
283      if (option.value === 'leichteSprache') { option.disabled = !available; option.hidden = !available; }
284      if (option.value === 'summary') { option.textContent = 'Zusammenfassung (Originalauszug)'; option.disabled = !summaryAvailable; option.hidden = !summaryAvailable; }
285      if (isLegalVoiceReaderPage() && option.value !== 'standard') option.disabled = true;
286    });
287    if ((!available && select.value === 'leichteSprache') || (!summaryAvailable && select.value === 'summary') || isLegalVoiceReaderPage()) select.value = 'standard';
288  }
289
290  function isLegalVoiceReaderPage() {
291    var path = (window.location.pathname || '').toLowerCase();
292    return /\/(impressum|datenschutz|datenschutzerklaerung|agb|widerruf|haftung|barrierefreiheitserklaerung)(\/|$)/.test(path) ||
293      Boolean(document.querySelector('[data-legal-page="true"], [data-no-simple-language="true"], [data-ce-type="min_legal"]'));
294  }
295
296  // Summaries contain complete original blocks only. Provider prose is never used.
297  var SUMMARY_EXCLUSIONS = 'script, style, noscript, svg, button, nav, header, footer, aside, form, [role="dialog"], [role="navigation"], [data-no-readaloud], [data-no-voice-reader], [data-no-simple-language], [data-legal-page], [data-ce-type="min_legal"], [data-easytext-policy="external"], [data-mincore-easytext-policy="external"], [data-external-policy], [data-voicereader-summary], [data-voicereader-variant="summary"], .styleswitcher, .voicereader-pro, .sr-only, .visually-hidden';
298
299  function getOriginalSummaryUnits() {
300    var source = getVoiceReaderSource('main');
301    if (!source || isLegalVoiceReaderPage() || source.closest(SUMMARY_EXCLUSIONS)) return [];
302    var legalTitle = /bekanntmachung|satzung|hebesaetz|hebesätz|widerruf|haftung|datenschutz|impressum/i;
303    var pageHeading = source.querySelector('h1');
304    if (/bekanntmachung|satzung|hebesaetz/i.test(window.location.pathname || '') || (pageHeading && legalTitle.test(pageHeading.textContent || ''))) return [];
305    var exclusions = SUMMARY_EXCLUSIONS + ', [hidden], [aria-hidden="true"], [data-simple="true"], [data-level-3="true"]';
306    var nodes = Array.prototype.filter.call(source.querySelectorAll('p, li'), function(node) {
307      return !node.closest(exclusions) && !(node.tagName === 'P' && node.parentElement && node.parentElement.closest('li'));
308    });
309    if (!nodes.length) nodes = Array.prototype.filter.call(source.querySelectorAll('h1, h2, h3'), function(node) { return !node.closest(exclusions); });
310    var units = [], length = 0;
311    nodes.forEach(function(node) {
312      if (units.length >= 64) return;
313      var section = node.closest('section, article');
314      if (section && section !== source) {
315        var heading = section.querySelector('h2, h3');
316        if (heading && legalTitle.test(heading.textContent || '')) return;
317      }
318      // Compare textContent on a sanitized clone; layout never supplies new text.
319      var clone = node.cloneNode(true);
320      var originals = Array.prototype.slice.call(node.querySelectorAll('*'));
321      var copies = Array.prototype.slice.call(clone.querySelectorAll('*'));
322      originals.forEach(function(child, index) {
323        var style = window.getComputedStyle(child);
324        if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse') copies[index].remove();
325      });
326      clone.querySelectorAll(exclusions).forEach(function(child) { child.remove(); });
327      var text = (clone.textContent || '').replace(/\s+/g, ' ').trim();
328      if (!text || units.indexOf(text) !== -1 || length + text.length > 6000) return;
329      // A CSS-hidden block must never become a fallback summary.
330      for (var parent = node; parent && parent !== source.parentElement; parent = parent.parentElement) {
331        var style = window.getComputedStyle(parent);
332        if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse') return;
333      }
334      units.push(text); length += text.length;
335    });
336    return units;
337  }
338
339  function getOriginalSummaryFallback(units) {
340    var substantive = units.filter(function(unit) { return unit.length >= 60 || unit.split(/\s+/).length >= 8 || /[.!?]$/.test(unit); });
341    if (substantive.length) units = substantive;
342    var selected = [], length = 0;
343    units.some(function(unit) {
344      if (selected.length && (selected.length >= 3 || length + unit.length > 900)) return true;
345      selected.push(unit); length += unit.length;
346      return false;
347    });
348    return selected.join(' ');
349  }
350
351  function getStoredPageSummary() {
352    // Recompute from the current DOM on every use: no stale stored summary.
353    return getOriginalSummaryFallback(getOriginalSummaryUnits());
354  }
355
356  function prepareOriginalSummary() {}
357
358  function getReadableText(options) {
359    options = options || {};
360    var area = options.area || getVoiceReaderArea(), source = getVoiceReaderSource(area);
361    if (!source) return '';
362    if (options.mode === 'summary') return getStoredPageSummary(source);
363    if (options.mode === 'leichteSprache') return getStoredEasyText(source);
364    if (area === 'whole') {
365      return [document.querySelector('header'), getVoiceReaderSource('main'), document.querySelector('footer')].filter(function(region, index, all) { return region && all.indexOf(region) === index; }).map(function(region) { return extractReadableText(region, true, true); }).filter(Boolean).join(' ');
366    }
367    return extractReadableText(source, false, true);
368  }
369
370  function releaseVoiceReaderAudio() {
371    if (voiceReaderObjectUrl) {
372      URL.revokeObjectURL(voiceReaderObjectUrl);
373      voiceReaderObjectUrl = null;
374    }
375  }
376
377  function isVoiceReaderActive() {
378    return voiceReaderLoading ||
379      voiceReaderExtensionActive ||
380      voiceReaderSpeechActive ||
381      Boolean(voiceReaderAudio && !voiceReaderAudio.paused && !voiceReaderAudio.ended);
382  }
383
384  function setVoiceReaderStatus(message, kind, timeout) {
385    voiceReaderStatusMessage = message || '';
386    voiceReaderStatusKind = kind || 'idle';
387    window.clearTimeout(voiceReaderStatusTimer);
388    if (timeout) {
389      voiceReaderStatusTimer = window.setTimeout(function() {
390        voiceReaderStatusMessage = '';
391        voiceReaderStatusKind = 'idle';
392        updateToolbarState();
393      }, timeout);
394    }
395    updateToolbarState();
396  }
397
398  function resetVoiceReaderSpeech() {
399    voiceReaderSpeechUtterance = null;
400    voiceReaderSpeechQueue = [];
401    voiceReaderSpeechIndex = 0;
402    voiceReaderSpeechActive = false;
403  }
404
405  function resetVoiceReaderAudio() {
406    releaseVoiceReaderAudio();
407    voiceReaderAudio = null;
408    voiceReaderLoading = false;
409    voiceReaderRequestController = null;
410    updateToolbarState();
411  }
412
413  function resetVoiceReaderExtension() {
414    voiceReaderExtensionQueue = [];
415    voiceReaderExtensionIndex = 0;
416    voiceReaderExtensionActive = false;
417  }
418
419  function stopVoiceReader(silent) {
420    voiceReaderGeneration += 1;
421    voiceReaderNeedsResume = false;
422    if (voiceReaderVoiceWaiter) { window.speechSynthesis.removeEventListener('voiceschanged', voiceReaderVoiceWaiter.listener); window.clearTimeout(voiceReaderVoiceWaiter.timer); voiceReaderVoiceWaiter = null; }
423    if (voiceReaderPrefetchController) { voiceReaderPrefetchController.abort(); voiceReaderPrefetchController = null; }
424    voiceReaderPrefetch = null;
425    if (voiceReaderRequestController) {
426      voiceReaderRequestController.abort();
427      voiceReaderRequestController = null;
428    }
429
430    if (voiceReaderAudio) {
431      voiceReaderAudio.pause();
432      voiceReaderAudio.currentTime = 0;
433    }
434
435    if (window.speechSynthesis) {
436      window.speechSynthesis.cancel();
437    }
438
439    resetVoiceReaderExtension();
440    resetVoiceReaderSpeech();
441    voiceReaderPlayback = null;
442    resetVoiceReaderAudio();
443    if (!silent) {
444      setVoiceReaderStatus('Vorlesen gestoppt.', 'stopped');
445    }
446  }
447
448  function getVoiceReaderSpeed() {
449    var speedSelect = document.getElementById('voiceReaderSpeed') || document.querySelector('.voicereader-pro [data-action="speed"]');
450    var speed = speedSelect ? parseFloat(speedSelect.value) : 1.0;
451
452    return Math.max(0.5, Math.min(2.0, Number.isFinite(speed) ? speed : 1.0));
453  }
454
455  function getVoiceReaderVoice() {
456    var voiceSelect = document.getElementById('voiceReaderVoice');
457    return voiceSelect && voiceSelect.value ? voiceSelect.value : 'eva_k';
458  }
459
460  function getVoiceReaderMode() {
461    if (isLegalVoiceReaderPage()) {
462      return 'standard';
463    }
464    var modeSelect = document.getElementById('voiceReaderMode');
465    if (modeSelect && modeSelect.value) {
466      return modeSelect.value;
467    }
468
469    return easyTextActive ? 'leichteSprache' : 'standard';
470  }
471
472  function getVoiceReaderPlayback() {
473    var mode = getVoiceReaderMode();
474    return {
475      voice: getVoiceReaderVoice(),
476      speed: getVoiceReaderSpeed(),
477      mode: mode,
478      area: getVoiceReaderArea(),
479      allowFallback: mode === 'standard' || mode === 'leichteSprache'
480    };
481  }
482
483  // Deliberately exposes only QA metadata. Extracted page text never leaves this
484  // closure through the public A11y helper.
485  function getVoiceReaderState() {
486    var playback = voiceReaderPlayback || getVoiceReaderPlayback();
487    var standardText = getVoiceReaderRequestText({ area: playback.area, mode: '
487standard' });
488    var easyText = getVoiceReaderRequestText({ area: playback.area, mode: 'leichteSprache' });
489    return {
490      active: isVoiceReaderActive(),
491      loading: voiceReaderLoading,
492      paused: false,
493      status: voiceReaderStatusKind,
494      statusMessage: voiceReaderStatusMessage,
495      voice: playback.voice,
496      speed: playback.speed,
497      audioPlaybackRate: voiceReaderAudio ? voiceReaderAudio.playbackRate : null,
498      mode: playback.mode,
499      area: playback.area,
500      legalPage: isLegalVoiceReaderPage(),
501      standardTextLength: standardText.length,
502      easyTextAvailable: Boolean(easyText),
503      easyTextLength: easyText.length
504    };
505  }
506
507  function splitSpeechText(text) {
508    var chunks = [], remaining = text.replace(/\s+/g, ' ').trim();
509    while (remaining.length) {
510      var limit = chunks.length ? 600 : 120;
511      if (remaining.length <= limit) { chunks.push(remaining); break; }
512      var breakAt = Math.max(remaining.lastIndexOf('. ', limit - 1), remaining.lastIndexOf('! ', limit - 1), remaining.lastIndexOf('? ', limit - 1), remaining.lastIndexOf('; ', limit - 1), remaining.lastIndexOf(', ', limit - 1));
513      if (breakAt < Math.floor(limit / 3)) {
514        breakAt = remaining.lastIndexOf(' ', limit);
515        if (breakAt < 0) breakAt = remaining.indexOf(' ', limit);
516        if (breakAt < 0) { chunks.push(remaining); break; }
517      }
518      chunks.push(remaining.slice(0, breakAt + 1).trim());
519      remaining = remaining.slice(breakAt + 1).trim();
520    }
521    return chunks.filter(Boolean);
522  }
523
524  function getVoiceReaderRequestText(playback) {
525    var text = getReadableText(playback);
526    if (!text) {
527      return '';
528    }
529
530    return VOICE_READER_MAX_TEXT_LENGTH ? text.slice(0, VOICE_READER_MAX_TEXT_LENGTH).trim() : text;
531  }
532
533  function scheduleVoiceReaderWarm() {}
534
535  function resolveSpeechVoice(playback) {
536    if (!window.speechSynthesis || typeof window.speechSynthesis.getVoices !== 'function') {
537      return null;
538    }
539
540    var selectedVoice = (playback && playback.voice ? playback.voice : getVoiceReaderVoice()).toLowerCase();
541    var voices = window.speechSynthesis.getVoices();
542    var matchingName = voices.find(function(voice) {
543      return /^de([-_]|$)/i.test(voice.lang || '') && voice.name && voice.name.toLowerCase().indexOf(selectedVoice) !== -1;
544    });
545
546    if (matchingName) {
547      return matchingName;
548    }
549
550    return voices.find(function(voice) {
551      return /^de([-_]|$)/i.test(voice.lang || '');
552    }) || null;
553  }
554
555  function speakNextLocalChunk() {
556    if (!window.speechSynthesis || !voiceReaderSpeechActive) {
557      resetVoiceReaderSpeech();
558      updateToolbarState();
559      return;
560    }
561
562    if (voiceReaderSpeechIndex >= voiceReaderSpeechQueue.length) {
563      resetVoiceReaderSpeech();
564      voiceReaderPlayback = null;
565      setVoiceReaderStatus('Vorlesen beendet.', 'complete', 5000);
566      updateToolbarState();
567      return;
568    }
569
570    var speechGeneration = voiceReaderGeneration;
571    var utterance = new SpeechSynthesisUtterance(voiceReaderSpeechQueue[voiceReaderSpeechIndex]);
572    var voice = resolveSpeechVoice(voiceReaderPlayback);
573    utterance.lang = voice && voice.lang ? voice.lang : 'de-DE';
574    utterance.rate = voiceReaderPlayback ? voiceReaderPlayback.speed : getVoiceReaderSpeed();
575    utterance.pitch = 1;
576    if (voice) {
577      utterance.voice = voice;
578    }
579
580    utterance.onend = function() {
581      if (speechGeneration !== voiceReaderGeneration || voiceReaderSpeechUtterance !== utterance) return;
582      voiceReaderSpeechIndex += 1;
583      speakNextLocalChunk();
584    };
585    utterance.onerror = function(event) {
586      if (speechGeneration !== voiceReaderGeneration || voiceReaderSpeechUtterance !== utterance) return;
587      if (!event || event.error !== 'interrupted') {
588        console.warn('VoiceReader local speech failed.', event);
589      }
590      resetVoiceReaderSpeech();
591      voiceReaderPlayback = null;
592      setVoiceReaderStatus('Vorlesen im Browser ist fehlgeschlagen.', 'error');
593      updateToolbarState();
594    };
595
596    voiceReaderSpeechUtterance = utterance;
597    window.speechSynthesis.speak(utterance);
598    updateToolbarState();
599  }
600
601  function playWithLocalSpeech(text, playback) {
602    stopVoiceReader(true);
603    if (!('speechSynthesis' in window) || typeof window.SpeechSynthesisUtterance === 'undefined') {
604      console.warn('VoiceReader fallback unavailable: speechSynthesis is not supported.');
605      voiceReaderPlayback = null;
606      setVoiceReaderStatus('Vorlesen ist in diesem Browser nicht verfügbar.', 'error');
607      return;
608    }
609
610    stopVoiceReader(true);
611    voiceReaderPlayback = playback || getVoiceReaderPlayback();
612    voiceReaderSpeechQueue = splitSpeechText(text);
613    voiceReaderSpeechIndex = 0;
614    voiceReaderSpeechActive = voiceReaderSpeechQueue.length > 0;
615    voiceReaderLoading = false;
616
617    if (!voiceReaderSpeechActive) {
618      voiceReaderPlayback = null;
619      setVoiceReaderStatus('Für diesen Bereich ist kein vorlesbarer Text vorhanden.', 'error');
620      updateToolbarState();
621      return;
622    }
623
624    var generation = voiceReaderGeneration;
625    function finishWaiting() {
626      if (voiceReaderVoiceWaiter && voiceReaderVoiceWaiter.generation === generation) {
627        window.speechSynthesis.removeEventListener('voiceschanged', voiceReaderVoiceWaiter.listener);
628        window.clearTimeout(voiceReaderVoiceWaiter.timer);
629        voiceReaderVoiceWaiter = null;
630      }
631    }
632    function startLocal(timedOut) {
633      if (generation !== voiceReaderGeneration || !voiceReaderSpeechActive || voiceReaderSpeechUtterance) return;
634      if (resolveSpeechVoice(voiceReaderPlayback)) {
635        finishWaiting();
636        speakNextLocalChunk();
637        setVoiceReaderStatus('Vorlesen läuft im Browser.', 'playing');
638      } else if (timedOut) {
639        finishWaiting();
640        resetVoiceReaderSpeech();
641        voiceReaderPlayback = null;
642        setVoiceReaderStatus('Keine deutsche Browserstimme ist verfügbar.', 'error');
643      }
644    }
645    if (resolveSpeechVoice(voiceReaderPlayback)) {
646      startLocal(false);
647    } else {
648      setVoiceReaderStatus('Deutsche Browserstimme wird vorbereitet.', 'loading');
649      var onVoices = function() { startLocal(false); };
650      voiceReaderVoiceWaiter = { generation: generation, listener: onVoices, timer: null };
651      window.speechSynthesis.addEventListener('voiceschanged', onVoices);
652      voiceReaderVoiceWaiter.timer = window.setTimeout(function() { startLocal(true); }, 1500);
653    }
654  }
655
656  function failVoiceReaderPlayback(error, fallbackText) {
657    if (voiceReaderSpeechActive || (error && error.name === 'AbortError')) return;
658    var playback = voiceReaderPlayback;
659    stopVoiceReader(true);
660    console.warn('VoiceReader playback failed.', error);
661    if (playback && playback.allowFallback && fallbackText) {
662      playWithLocalSpeech(fallbackText, playback);
663    } else {
664      setVoiceReaderStatus('Vorlesen ist derzeit nicht verfügbar. Bitte erneut versuchen.', 'error');
665    }
666  }
667
668  function fetchExtensionChunk(index, playback, generation, controller) {
669    if (voiceReaderExtensionQueue[index].length > 600) return Promise.reject(new Error('VoiceReader text segment exceeds the request limit.'));
670    return fetch(VOICE_READER_ENDPOINT, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, signal: controller ? controller.signal : undefined, body: JSON.stringify({ text: voiceReaderExtensionQueue[index], voice: playback.voice, speed: 1, language: 'de', mode: playback.mode }) }).then(function(response) { if (!response.ok) throw new Error('VoiceReader endpoint returned ' + response.status + '.'); return response.blob(); }).then(function(blob) { if (!blob || !blob.size) throw new Error('VoiceReader returned empty audio.'); if (generation !== voiceReaderGeneration) throw new DOMException('stale', 'AbortError'); return blob; });
671  }
672  function playNextExtensionChunk() {
673    var playback = voiceReaderPlayback, generation = playback && playback.generation, index = voiceReaderExtensionIndex;
674    if (!voiceReaderExtensionActive || generation !== voiceReaderGeneration) return Promise.resolve();
675    if (index >= voiceReaderExtensionQueue.length) { resetVoiceReaderExtension(); voiceReaderPlayback = null; resetVoiceReaderAudio(); setVoiceReaderStatus('Vorlesen beendet.', 'complete', 5000); return Promise.resolve(); }
676    voiceReaderLoading = true; updateToolbarState();
677    var controller = 'AbortController' in window ? new AbortController() : null;
678    voiceReaderRequestController = controller;
679    var cached = voiceReaderPrefetch && voiceReaderPrefetch.index === index ? voiceReaderPrefetch : null;
680    var current = cached ? cached.promise : fetchExtensionChunk(index, playback, generation, controller);
681    if (cached) { voiceReaderRequestController = cached.controller; voiceReaderPrefetch = null; voiceReaderPrefetchController = null; }
682    return current.then(function(blob) { if (generation !== voiceReaderGeneration) return; if (index + 1 < voiceReaderExtensionQueue.length && !voiceReaderPrefetch) { voiceReaderPrefetchController = 'AbortController' in window ? new AbortController() : null; voiceReaderPrefetch = { index: index + 1, controller: voiceReaderPrefetchController, promise: fetchExtensionChunk(index + 1, playback, generation, voiceReaderPrefetchController) }; voiceReaderPrefetch.promise.catch(function() {}); } releaseVoiceReaderAudio(); voiceReaderObjectUrl = URL.createObjectURL(blob); voiceReaderAudio = new Audio(voiceReaderObjectUrl); voiceReaderAudio.playbackRate = playback.speed; voiceReaderAudio.preservesPitch = true; if ('webkitPreservesPitch' in voiceReaderAudio) voiceReaderAudio.webkitPreservesPitch = true; voiceReaderAudio.addEventListener('ended', function() { if (generation === voiceReaderGeneration) { releaseVoiceReaderAudio(); voiceReaderAudio = null; voiceReaderExtensionIndex += 1; playNextExtensionChunk(); } }, { once: true }); voiceReaderAudio.addEventListener('error', function() { if (generation === voiceReaderGeneration) failVoiceReaderPlayback(new Error('Audio playback failed.'), voiceReaderExtensionQueue.slice(index).join(' ')); }, { once: true }); voiceReaderLoading = false; voiceReaderRequestController = null; updateToolbarState(); return voiceReaderAudio.play().then(function() { if (generation === voiceReaderGeneration) setVoiceReaderStatus('Wird vorgelesen.', 'playing'); }); }).catch(function(error) { if (generation !== voiceReaderGeneration) return;
682 if (error && error.name === 'NotAllowedError' && voiceReaderAudio) { voiceReaderNeedsResume = true; voiceReaderLoading = false; setVoiceReaderStatus('Zum Starten bitte noch einmal auf Vorlesen klicken.', 'paused'); } else { failVoiceReaderPlayback(error, voiceReaderExtensionQueue.slice(index).join(' ')); } });
683  }
684  function playWithExtension(text, playback) {
685    if (!window.fetch || typeof window.Audio === 'undefined') {
686      if (playback.allowFallback) {
687        playWithLocalSpeech(text, playback);
688      } else {
689        setVoiceReaderStatus('Vorlesen ist für den gewählten Modus in diesem Browser nicht verfügbar.', 'error');
690      }
691      return Promise.resolve();
692    }
693
694    stopVoiceReader(true);
695    voiceReaderPlayback = playback;
696    voiceReaderPlayback.generation = voiceReaderGeneration;
697    voiceReaderExtensionQueue = splitSpeechText(text);
698    voiceReaderExtensionIndex = 0;
699    voiceReaderExtensionActive = voiceReaderExtensionQueue.length > 0;
700
701    if (!voiceReaderExtensionActive) {
702      voiceReaderPlayback = null;
703      setVoiceReaderStatus('Für diesen Bereich ist kein vorlesbarer Text vorhanden.', 'error');
704      updateToolbarState();
705      return Promise.resolve();
706    }
707
708    setVoiceReaderStatus('Vorlesen wird vorbereitet.', 'loading');
709    return playNextExtensionChunk();
710  }
711
712  function restartVoiceReaderForSettings() { updateVoiceReaderModeAvailability(); if (!isVoiceReaderActive()) { return; } stopVoiceReader(true); toggleVorlesen(); }
713
714  function toggleVorlesen() {
715    if (voiceReaderNeedsResume && voiceReaderAudio) {
716      var resumeGeneration = voiceReaderGeneration;
717      voiceReaderNeedsResume = false;
718      voiceReaderAudio.play().then(function() {
719        if (resumeGeneration === voiceReaderGeneration) setVoiceReaderStatus('Wird vorgelesen.', 'playing');
720      }).catch(function(error) {
721        if (resumeGeneration === voiceReaderGeneration) failVoiceReaderPlayback(error, voiceReaderExtensionQueue.slice(voiceReaderExtensionIndex).join(' '));
722      });
723      return;
724    }
725    var voiceReaderControl = findVoiceReaderControl();
726    if (voiceReaderControl) {
727      voiceReaderControl.click();
728      return;
729    }
730
731    if (isVoiceReaderActive()) {
732      stopVoiceReader();
733      return;
734    }
735
736    var playback = getVoiceReaderPlayback();
737    if (playback.mode === 'summary') prepareOriginalSummary();
738    var text = getVoiceReaderRequestText(playback);
739    if (playback.mode === 'leichteSprache' && !text) {
740      setVoiceReaderStatus('Für diese Seite liegt noch kein hinterlegter Text in Leichter Sprache vor.', 'unavailable');
741      return;
742    }
743    if (!text) {
744      setVoiceReaderStatus('Für diesen Bereich ist kein vorlesbarer Text vorhanden.', 'error');
745      updateToolbarState();
746      return;
747    }
748
749    playWithExtension(text, playback);
750  }
751
752  // ==========================================================================
753  // TOOLBAR
754  // ==========================================================================
755
756  function createToolbar() {
757    if (document.querySelector('.styleswitcher') || document.querySelector('.min-style-switcher') || document.querySelector('.lotte-a11y-toolbar')) {
758      return;
759    }
760
761    var toolbar = document.createElement('div');
762    toolbar.className = 'lotte-a11y-toolbar';
763    toolbar.setAttribute('role', 'toolbar');
764    toolbar.setAttribute('aria-label', 'Barrierefreiheit-Optionen');
765
766    // Pruefen ob Seite Einfache-Sprache-Texte hat
767    var hasEasyText = document.querySelector('[data-text-easy], [data-simple="true"], [data-mincore-easytext-group][data-has-easy="true"]');
768
769    toolbar.innerHTML =
770      /* Dark Mode */
771      '<button class="lotte-a11y-toolbar__btn" data-action="theme" title="Dark Mode" aria-label="Dark Mode umschalten" aria-pressed="false">' +
772        '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
773          '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>' +
774        '</svg>' +
775      '</button>' +
776      /* Schrift groesser */
777      '<button class="lotte-a11y-toolbar__btn" data-action="font-increase" title="Schrift gr\u00f6\u00dfer" aria-label="Schrift vergr\u00f6\u00dfern">' +
778        '<span class="lotte-a11y-toolbar__text-symbol lotte-a11y-toolbar__text-symbol--large">A+</span>' +
779      '</button>' +
780      /* Schrift kleiner */
781      '<button class="lotte-a11y-toolbar__btn" data-action="font-decrease" title="Schrift kleiner" aria-label="Schrift verkleinern">' +
782        '<span class="lotte-a11y-toolbar__text-symbol lotte-a11y-toolbar__text-symbol--small">A\u2212</span>' +
783      '</button>' +
784      /* Hoher Kontrast */
785      '<button class="lotte-a11y-toolbar__btn" data-action="contrast" title="Hoher Kontrast" aria-label="Hohen Kontrast umschalten" aria-pressed="false">' +
786        '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
787          '<circle cx="12" cy="12" r="10"/>' +
788          '<path d="M12 2a10 10 0 0 1 0 20V2z" fill="currentColor"/>' +
789        '</svg>' +
790      '</button>' +
791      /* Layout toggle: full-width vs. boxed (max-width 1300px) */
792      '<button class="lotte-a11y-toolbar__btn" data-action="layout-toggle" title="Boxed Layout (1300px)" aria-label="Boxed/Full-Width Layout umschalten" aria-pressed="false">' +
793        '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
794          '<polyline points="15 3 21 3 21 9"/>' +
795          '<polyline points="9 21 3 21 3 15"/>' +
796          '<line x1="21" y1="3" x2="14" y2="10"/>' +
797          '<line x1="3" y1="21" x2="10" y2="14"/>' +
798        '</svg>' +
799      '</button>' +
800      /* Leichte Sprache (nur wenn Seite data-text-easy hat) */
801      (hasEasyText ?
802        '<button class="lotte-a11y-toolbar__btn lotte-a11y-toolbar__btn--divider" data-action="easy-text" title="Leichte Sprache" aria-label="Leichte Sprache umschalten" aria-pressed="false">' +
803          '<span class="lotte-a11y-toolbar__text-symbol lotte-a11y-toolbar__text-symbol--small">ES</span>' +
804        '</button>' : '') +
805      /* Vorlesen */
806      '<button class="lotte-a11y-toolbar__btn" data-action="vorlesen" title="Seite vorlesen" aria-label="Seite vorlesen">' +
807        '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
808          '<path d="M11 5L6 9H2v6h4l5 4V5z"/>' +
809          '<path d="M15.54 8.46a5 5 0 010 7.07"/>' +
810        '</svg>' +
811      '</button>';
812
813    document.body.appendChild(toolbar);
814
815    toolbar.addEventListener('click', function(e) {
816      var btn = e.target.closest('.lotte-a11y-toolbar__btn');
817      if (!btn) return;
818
819      var action = btn.dataset.action;
820
821      switch(action) {
822        case 'theme':
823          applyTheme(currentTheme === 'light' ? 'dark' : 'light');
824          break;
825        case 'font-increase':
826          increaseFontSize();
827          break;
828        case 'font-decrease':
829          decreaseFontSize();
830          break;
831        case 'contrast':
832          applyContrast(!highContrast);
833          break;
834        case 'layout-toggle':
835          applyLayout(currentLayout === 'full' ? 'boxed' : 'full');
836          break;
837        case 'easy-text':
838          applyEasyText(!easyTextActive);
839          break;
840        case 'vorlesen':
841          toggleVorlesen();
842          break;
843      }
844    });
845
846    updateToolbarState();
847  }
848
849  function updateToolbarState() {
850    var themeBtn = document.querySelector('[data-action="theme"]');
851    if (themeBtn) {
852      themeBtn.classList.toggle('active', currentTheme === 'dark');
853      themeBtn.setAttribute('aria-pressed', String(currentTheme === 'dark'));
854    }
855    var contrastBtn = document.querySelector('[data-action="contrast"]');
856    if (contrastBtn) {
857      contrastBtn.classList.toggle('active', highContrast);
858      contrastBtn.setAttribute('aria-pressed', String(highContrast));
859    }
860    var easyBtn = document.querySelector('[data-action="easy-text"]');
861    if (easyBtn) {
862      easyBtn.classList.toggle('active', easyTextActive);
863      easyBtn.setAttribute('aria-pressed', String(easyTextActive));
864    }
865    var layoutBtn = document.querySelector('[data-action="layout-toggle"]');
866    if (layoutBtn) {
867      // Button is "active" (pressed) when boxed mode is on
868      layoutBtn.classList.toggle('active', currentLayout === 'boxed');
869      layoutBtn.setAttribute('aria-pressed', String(currentLayout === 'boxed'));
870    }
871    var readBtn = document.querySelector('[data-action="vorlesen"]');
872    var prototypeReadBtn = document.getElementById('voiceReaderToggle');
873    var prototypeStatus = document.getElementById('voiceReaderStatus');
874    var prototypeStatusText = document.getElementById('voiceReaderStatusText');
875    var prototypeProgress = document.getElementById('voiceReaderProgress');
876    var isReading = isVoiceReaderActive();
877
878    if (readBtn) {
879      readBtn.classList.toggle('active', isReading);
880      readBtn.setAttribute('aria-pressed', String(isReading));
881      readBtn.setAttribute('aria-busy', String(voiceReaderLoading));
882      readBtn.setAttribute('aria-label', voiceReaderLoading ? 'Vorlesen wird vorbereitet' : (isReading ? 'Vorlesen stoppen' : 'Seite vorlesen'));
883      readBtn.setAttribute('title', voiceReaderLoading ? 'Vorlesen wird vorbereitet' : (isReading ? 'Vorlesen stoppen' : 'Seite vorlesen'));
884    }
885    if (prototypeReadBtn) {
886      prototypeReadBtn.classList.toggle('styleswitcher__toggle--active', isReading);
887      prototypeReadBtn.setAttribute('aria-pressed', String(isReading));
888      prototypeReadBtn.setAttribute('aria-busy', String(voiceReaderLoading));
889      prototypeReadBtn.textContent = voiceReaderLoading ? '...' : (isReading ? '■' : '▶');
890      prototypeReadBtn.setAttribute('aria-label', voiceReaderLoading ? 'Vorlesen wird vorbereitet' : (isReading ? 'Vorlesen stoppen' : 'Seite vorlesen lassen'));
891      prototypeReadBtn.setAttribute('title', voiceReaderLoading ? 'Vorlesen wird vorbereitet' : (isReading ? 'Vorlesen stoppen' : 'Vorlesen starten'));
892    }
893    if (prototypeStatus) {
894      prototypeStatus.hidden = !(voiceReaderLoading || isReading || voiceReaderStatusMessage);
895      prototypeStatus.setAttribute('data-voicereader-status', voiceReaderStatusKind);
896      prototypeStatus.setAttribute('role', 'status');
897      prototypeStatus.setAttribute('aria-live', 'polite');
898    }
899    if (prototypeStatusText) {
900      prototypeStatusText.setAttribute('role', 'status');
901      prototypeStatusText.setAttribute('aria-live', 'polite');
902      prototypeStatusText.textContent = voiceReaderStatusMessage || (voiceReaderLoading ? 'Vorlesen wird vorbereitet.' : (isReading ? 'Wird vorgelesen.' : ''));
903    }
904    if (prototypeProgress) {
905      prototypeProgress.classList.toggle('styleswitcher__progress-fill--active', isReading);
906    }
907  }
908
909  function increaseFontSize() {
910    var idx = FONT_SIZES.indexOf(currentFontSize);
911    if (idx < FONT_SIZES.length - 1) {
912      applyFontSize(FONT_SIZES[idx + 1]);
913    }
914  }
915
916  function decreaseFontSize() {
917    var idx = FONT_SIZES.indexOf(currentFontSize);
918    if (idx > 0) {
919      applyFontSize(FONT_SIZES[idx - 1]);
920    }
921  }
922
923  // ==========================================================================
924  // KEYBOARD NAVIGATION
925  // ==========================================================================
926
927  function initKeyboardNav() {
928    document.addEventListener('keydown', function(e) {
929      if (e.key === 'Tab') {
930        document.body.classList.add('keyboard-nav');
931      }
932    });
933
934    document.addEventListener('mousedown', function() {
935      document.body.classList.remove('keyboard-nav');
936    });
937  }
938
939  function bindPrototypeVoiceReaderControls() {
940    updateVoiceReaderModeAvailability();
941    var readButton = document.getElementById('voiceReaderToggle');
942    var speedInput = document.getElementById('voiceReaderSpeed');
943    var speedLabel = document.getElementById('voiceReaderSpeedLabel');
944    var areaButton = document.getElementById('voiceReaderArea');
945
946    if (readButton && readButton.dataset.lotteVoiceBound !== 'true') {
947      readButton.dataset.lotteVoiceBound = 'true';
948      readButton.addEventListener('click', function() {
949        toggleVorlesen();
950      });
951    }
952
953    if (speedInput && speedLabel) {
954      function updateSpeedLabel() {
955        speedLabel.textContent = parseFloat(speedInput.value || '1').toString().replace('.', ',') + 'x';
956      }
957      if (speedInput.dataset.lotteVoiceBound !== 'true') {
958        speedInput.dataset.lotteVoiceBound = 'true';
959        speedInput.addEventListener('input', updateSpeedLabel);
960        speedInput.addEventListener('change', function() {
961          restartVoiceReaderForSettings();
962        });
963      }
964      updateSpeedLabel();
965    }
966
967    var voiceSelect = document.getElementById('voiceReaderVoice');
968    if (voiceSelect && voiceSelect.dataset.lotteVoiceWarmBound !== 'true') {
969      voiceSelect.dataset.lotteVoiceWarmBound = 'true';
970      voiceSelect.addEventListener('change', function() {
971        restartVoiceReaderForSettings();
972      });
973    }
974
975    var modeSelect = document.getElementById('voiceReaderMode');
976    if (modeSelect && isLegalVoiceReaderPage()) {
977      modeSelect.value = 'standard';
978      Array.prototype.forEach.call(modeSelect.options, function(option) {
979        option.disabled = option.value !== 'standard';
980      });
981      modeSelect.setAttribute('aria-describedby', 'voiceReaderStatusText');
982    }
983    if (modeSelect && modeSelect.dataset.lotteVoiceWarmBound !== 'true') {
984      modeSelect.dataset.lotteVoiceWarmBound = 'true';
985      modeSelect.addEventListener('change', function() {
986        if (isLegalVoiceReaderPage()) {
987          modeSelect.value = 'standard';
988        }
989        restartVoiceReaderForSettings();
990      });
991    }
992
993    if (areaButton && areaButton.dataset.lotteVoiceBound !== 'true') {
994      areaButton.dataset.lotteVoiceBound = 'true';
995      areaButton.addEventListener('click', function() {
996        var mainOnly = areaButton.getAttribute('aria-pressed') !== 'true';
997        areaButton.setAttribute('aria-pressed', String(mainOnly));
998        areaButton.classList.toggle('styleswitcher__toggle--active', mainOnly);
999        areaButton.textContent = mainOnly ? 'Hauptinhalt' : 'Ganze Seite';
1000        restartVoiceReaderForSettings();
1001      });
1002    }
1003
1004    updateToolbarState();
1005  }
1006
1007  // ==========================================================================
1008  // BACK TO TOP (scroll listener fuer den Button aus footer.php)
1009  // ==========================================================================
1010
1011  function initBackToTop() {
1012    var btn = document.querySelector('.lotte-back-to-top, .back-to-top');
1013    if (!btn) return;
1014
1015    if (btn.dataset.lotteA11yBound === 'true') return;
1016    btn.dataset.lotteA11yBound = 'true';
1017
1018    btn.addEventListener('click', function() {
1019      window.scrollTo({ top: 0, behavior: 'smooth' });
1020    });
1021
1022    function updateBackToTop() {
1023      if (window.scrollY > 400) {
1024        btn.classList.add('is-visible');
1025        btn.hidden = false;
1026      } else {
1027        btn.classList.remove('is-visible');
1028        if (btn.classList.contains('lotte-back-to-top')) {
1029          btn.hidden = true;
1030        }
1031      }
1032    }
1033
1034    updateBackToTop();
1035    window.addEventListener('scroll', updateBackToTop, { passive: true });
1036  }
1037
1038  // ==========================================================================
1039  // REDUCED MOTION
1040  // ==========================================================================
1041
1042  function checkReducedMotion() {
1043    var mq = window.matchMedia('(prefers-reduced-motion: reduce)');
1044    function handle(e) {
1045      if (e.matches) {
1046        document.documentElement.setAttribute('data-reduced-motion', 'true');
1047      } else {
1048        document.documentElement.removeAttribute('data-reduced-motion');
1049      }
1050    }
1051    handle(mq);
1052    mq.addEventListener('change', handle);
1053  }
1054
1055  // ==========================================================================
1056  // SYSTEM THEME DETECTION
1057  // ==========================================================================
1058
1059  function checkSystemTheme() {
1060    if (localStorage.getItem(STORAGE_KEYS.theme)) return;
1061    var prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
1062    function handle(e) {
1063      if (!localStorage.getItem(STORAGE_KEYS.theme)) {
1064        applyTheme(e.matches ? 'dark' : 'light');
1065      }
1066    }
1067    handle(prefersDark);
1068    prefersDark.addEventListener('change', handle);
1069  }
1070
1071  // ==========================================================================
1072  // INIT
1073  // ==========================================================================
1074
1075  function init() {
1076    applyTheme(currentTheme);
1077    applyFontSize(currentFontSize);
1078    applyContrast(highContrast);
1079    // Restore layout preference on page load (default is 'full' — no attribute set)
1080    applyLayout(currentLayout);
1081    bindPrototypeVoiceReaderControls();
1082    if (!document.querySelector('.min-style-switcher') && !document.querySelector('.styleswitcher')) {
1083      createToolbar();
1084    }
1085    initBackToTop();
1086    initKeyboardNav();
1087    checkReducedMotion();
1088    checkSystemTheme();
1089
1090    // Leichte Sprache wiederherstellen falls gespeichert
1091    if (easyTextActive) {
1092      applyEasyText(true);
1093    }
1094
1095    // Do not prewarm on every page load: the same-origin warm endpoint is rate
1096    // limited and should not create console noise before a user uses VoiceReader.
1097  }
1098
1099  if (document.readyState === 'loading') {
1100    document.addEventListener('DOMContentLoaded', init);
1101  } else {
1102    init();
1103  }
1104
1105  window.lotteA11y = {
1106    getState: function() {
1107      return {
1108        theme: currentTheme,
1109        fontSize: currentFontSize,
1110        contrast: highContrast,
1111        easyText: easyTextActive,
1112        layout: currentLayout,
1113        reading: isVoiceReaderActive(),
1114        paused: false,
1115        voiceReader: getVoiceReaderState()
1116      };
1117    },
1118    applyTheme: applyTheme,
1119    applyFontSize: applyFontSize,
1120    applyContrast: applyContrast,
1121    applyLayout: applyLayout,
1122    applyEasyText: applyEasyText,
1123    toggleVorlesen: toggleVorlesen,
1124    getVoiceReaderState: getVoiceReaderState
1125  };
1126
1127})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.