PageSourceSearch

https://hotel-kindl.at/(cms)/module/static/default/content/libs/js/browser-check/static/browser-check.js

js hotel-kindl.at collected 2026-09-26 18:55:01 UTC 81,268 bytes, 2,008 lines download raw bytes

1/**
2 * Browser Check - Diagnose-Tool für Support
3 * Aufgabe: Browser- und OS-Einstellungen erkennen, die Darstellung beeinflussen
4 */
5
6const BROWSER_CHECK_CSS_HREF = '/(cms)/module/static/default/content/libs/js/browser-check/static/browser-check.css';
7const BROWSER_CHECK_FONT_HREF = 'https://fonts-api.viomassl.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap';
8
9function ensureLinkLoaded(href, markerAttr, extraAttrs) {
10  if (document.querySelector(`link[${markerAttr}="1"]`)) return;
11  const existingByHref = document.querySelector(`link[rel="stylesheet"][href="${href}"]`);
12  if (existingByHref) { existingByHref.setAttribute(markerAttr, '1'); return; }
13  const link = document.createElement('link');
14  link.rel = 'stylesheet';
15  link.href = href;
16  link.setAttribute(markerAttr, '1');
17  if (extraAttrs) Object.entries(extraAttrs).forEach(([k, v]) => link.setAttribute(k, v));
18  document.head.appendChild(link);
19}
20
21function ensureBrowserCheckCssLoaded() {
22  ensureLinkLoaded(BROWSER_CHECK_FONT_HREF, 'data-browser-check-font', { crossorigin: 'anonymous' });
23  ensureLinkLoaded(BROWSER_CHECK_CSS_HREF, 'data-browser-check-css');
24}
25
26class BrowserCheck {
27  constructor() {
28    this.checks = [];
29    this.results = [];
30    this.reportId = this.generateReportId();
31    this.timestamp = new Date().toISOString();
32  }
33
34  /**
35   * Eindeutige ID für diesen Report
36   */
37  generateReportId() {
38    const date = new Date();
39    const dateStr = date.toISOString().split('T')[0].replace(/-/g, '');
40    const random = Math.random().toString(36).substring(2, 6);
41    return `bc_${dateStr}_${random}`;
42  }
43
44  /**
45   * Alle Checks registrieren
46   */
47  registerChecks() {
48    this.registerCheck({
49      id: 'prefers-reduced-motion',
50      title: 'Bewegung reduzieren',
51      category: 'preferences',
52      test: () => this.testMediaQuery('(prefers-reduced-motion: reduce)'),
53      severity: 'warning',
54      confidence: 'high',
55      userMessage: () => 'In Ihrem Browser ist die Option „Bewegungen reduzieren" aktiviert.',
56      userImpact: 'Animationen, Übergänge und automatisch startende Inhalte wie Bildgalerien oder Slider können dadurch deaktiviert oder stark verlangsamt sein. Das ist bewusst für Nutzer zur Barrierefreiheit gemacht, die keine Bewegungen wünschen.',
57      userHelp: [
58        { os: 'Windows', steps: ['Einstellungen öffnen (Win + I)', 'Erleichterte Bedienung → Anzeige', '„Animationen in Windows anzeigen" einschalten'] },
59        { os: 'macOS', steps: ['Systemeinstellungen öffnen', 'Bedienungshilfen → Anzeige', '„Bewegung reduzieren" deaktivieren'] },
60        { os: 'iOS', steps: ['Einstellungen öffnen', 'Bedienungshilfen → Bewegung', '„Bewegung reduzieren" deaktivieren'] },
61        { os: 'Android', steps: ['Einstellungen öffnen', 'Bedienungshilfen', '„Animationen entfernen" deaktivieren'] }
62      ],
63      devAction: 'Animations-, Transition- und Autoplay-Logik auf prefers-reduced-motion prüfen.'
64    });
65
66    this.registerCheck({
67      id: 'forced-colors',
68      title: 'Erzwungene Systemfarben',
69      category: 'preferences',
70      test: () => this.testMediaQuery('(forced-colors: active)'),
71      severity: 'warning',
72      confidence: 'high',
73      userMessage: () => 'In Ihrem Browser ist der Hochkontrastmodus oder ein Farbfilter aktiv.',
74      userImpact: 'Das Betriebssystem überschreibt Farben auf der gesamten Seite durch eigene Systemfarben. Bilder, Symbole, Hintergründe und Texte können stark von der normalen Darstellung abweichen.',
75      userHelp: [
76        { os: 'Windows 10', steps: ['Einstellungen öffnen (Win + I)', 'Erleichterte Bedienung → Hoher Kontrast', 'Hohen Kontrast deaktivieren'] },
77        { os: 'Windows 11', steps: ['Einstellungen öffnen (Win + I)', 'Barrierefreiheit → Kontrast-Designs', 'Kein Design auswählen und „Anwenden" klicken'] }
78      ],
79      devAction: 'Icons, Kontraste und kritische UI-Elemente gegen Forced-Colors testen.'
80    });
81
82    this.registerCheck({
83      id: 'prefers-contrast',
84      title: 'Kontrastpräferenz',
85      category: 'preferences',
86      test: () => this.testContrast(),
87      severity: 'warning',
88      confidence: 'high',
89      userMessage: (value) => value === 'more'
90        ? 'In Ihrem Browser ist erhöhter Kontrast aktiviert.'
91        : 'In Ihrem Browser ist reduzierter Kontrast aktiviert.',
92      userImpact: 'Farben und Kontrastverhältnisse auf dieser Seite können für Sie anders erscheinen als für die meisten anderen Besucher. Insbesondere höherer Kontrast kann zu sehr kräftigen Farben und stärkeren Unterschieden zwischen Elementen führen. Ihr normales CI wird also leichte Abweichungen vorweisen. Automatische Anpassungen entsprechen den Vorgaben der Barrierefreiheitsgesetze verschiedener Länder der Europäischen Union.',
93      userHelp: [
94        { os: 'macOS', steps: ['Systemeinstellungen öffnen', 'Bedienungshilfen → Anzeige', '„Kontrast erhöhen" deaktivieren'] },
95        { os: 'Windows', steps: ['Einstellungen öffnen (Win + I)', 'Erleichterte Bedienung → Anzeige', 'Kontrast-Option an
95passen oder deaktivieren'] }
96      ],
97      devAction: 'Kontrast-sensitive Komponenten auf beide Extremwerte testen.'
98    });
99
100    this.registerCheck({
101      id: 'prefers-reduced-transparency',
102      title: 'Transparenzen reduzieren',
103      category: 'preferences',
104      test: () => this.testMediaQuery('(prefers-reduced-transparency: reduce)'),
105      severity: 'warning',
106      confidence: 'high',
107      userMessage: () => 'In Ihrem Browser ist die Option „Transparenzen reduzieren" aktiviert.',
108      userImpact: 'Glasmorphism-Effekte, semi-transparente Overlays und durchscheinende Hintergründe werden blockiert oder durch undurchsichtige Varianten ersetzt. Das Aussehen der Website kann dadurch deutlich unterschiedlich sein.',
109      userHelp: [
110        { os: 'Windows', steps: ['Einstellungen öffnen (Win + I)', 'Erleichterte Bedienung → Anzeige', '„Transparenzeffekte in Windows deaktivieren" ausschalten'] },
111        { os: 'macOS', steps: ['Systemeinstellungen öffnen', 'Bedienungshilfen → Anzeige', '„Transparenzeffekte reduzieren" deaktivieren'] }
112      ],
113      devAction: 'Transparenzabhängige CSS-Effekte (backdrop-filter, opacity, rgba) gegen diese Einstellung testen.'
114    });
115
116    this.registerCheck({
117      id: 'prefers-reduced-data',
118      title: 'Datenverbrauch reduzieren',
119      category: 'preferences',
120      test: () => this.testMediaQuery('(prefers-reduced-data: reduce)'),
121      severity: 'warning',
122      confidence: 'high',
123      userMessage: () => 'In Ihrem Browser ist die Option „Datenverbrauch reduzieren" aktiviert.',
124      userImpact: 'Diese Einstellung teilt der Website mit, dass Sie eine langsame oder begrenzte Internet-Verbindung haben. Videos, hochauflösende Bilder, Auto-Play und andere Inhalte mit hohem Datenverbrauch können daher deaktiviert oder in geringerer Qualität geladen werden. Das ist absichtlich und spart Ihre Bandbreite.',
125      userHelp: [
126        { os: 'Chrome/Edge', steps: ['Browser-Menü öffnen', 'Einstellungen → Datenschutz und Sicherheit', 'Datenschutz-Optionen zur Datenersparnis überprüfen'] },
127        { os: 'Safari', steps: ['Safari → Einstellungen', 'Datenschutz-Tab überprüfen oder System-Datensparprogramm aktivieren'] },
128        { os: 'iOS/Android', steps: ['Einstellungen öffnen', 'Datenschutz oder Datenoptionen', 'Datensparprogramm oder Niedrig-Bandbreite-Modus deaktivieren'] }
129      ],
130      devAction: 'Video-Autoplay, Bild-Auflösung, Asset-Loading und Lazy-Loading gegen diese Einstellung prüfen.'
131    });
132
133    this.registerCheck({
134      id: 'inverted-colors',
135      title: 'Invertierte Farben',
136      category: 'preferences',
137      test: () => this.testMediaQuery('(inverted-colors: inverted)'),
138      severity: 'warning',
139      confidence: 'high',
140      userMessage: () => 'In Ihrem Browser ist der Modus „Farben invertieren" aktiviert.',
141      userImpact: 'Der Browser invertiert die Farben aller Inhalte (Schwarz wird Weiß, Blau wird Orange, etc.). Dies ist üblicherweise für Nachtsicht-Modi gedacht, kann aber die Website unbrauchbar machen, wenn sie mit invertierten Farben nicht lesbar ist.',
142      userHelp: [
143        { os: 'Windows', steps: ['Einstellungen öffnen (Win + I)', 'Erleichterte Bedienung, Bildschirm-Filter überprüfen', 'Browser-Erweiterung für Farbfilter deaktivieren'] },
144        { os: 'macOS', steps: ['Systemeinstellungen öffnen', 'Bedienungshilfen → Anzeige', '„Farben invertieren" deaktivieren'] },
145        { os: 'Chrome/Edge', steps: ['Browser-Erweiterung (z.B. Dark Reader) überprüfen und deaktivieren', 'DevTools (F12) → Rendering → CSS media feature Einstellungen'] }
146      ],
147      devAction: 'SVG-Icons, Bilder und kritische Design-Elemente im inverted-colors Modus testen.'
148    });
149  }
150
151  /**
152   * Media Query testen
153   */
154  testMediaQuery(query) {
155    try {
156      const match = window.matchMedia(query).matches;
157      return {
158        matched: match,
159        value: match ? 'active' : 'inactive',
160        method: 'matchMedia'
161      };
162    } catch (e) {
163      return {
164        matched: false,
165        value: 'error',
166        method: 'matchMedia',
167        error: e.message
168      };
169    }
170  }
171
172  /**
173   * Kontrast-Präferenz testen
174   */
175  testContrast() {
176    try {
177      const moreContrast = window.matchMedia('(prefers-contrast: more)').matches;
178      const lessContrast = window.matchMedia('(prefers-contrast: less)').matches;
179      
180      let value = 'no-preference';
181      if (moreContrast) value = 'more';
182      if (lessContrast) value = 'less';
183      
184      return {
185        matched: moreContrast || lessContrast,
186        value: value,
187        method: 'matchMedia'
188      };
189    } catch (e) {
190      return {
191        matched: false,
192        value: 'not-supported',
193        method: 'matchMedia',
194        error: e.message
195      };
196    }
197  }
198
199  /**
200   * Check registrieren
201   */
202  registerCheck(checkDef) {
203    this.checks.push(checkDef);
204  }
205
206  /**
207   * Alle Checks ausführen
208   */
209  async runAllChecks() {
210    for (const check of this.checks) {
211      const testResult = check.test();
212      const resolvedUserMessage = testResult.matched && check.userMessage
213        ? check.userMessage(testResult.value)
214        : null;
215
216      this.results.push({
217        id: check.id,
218        title: check.title,
219        category: check.category,
220        severity: check.severity,
221        confidence: check.confidence,
222        matched: testResult.matched,
223        value: testResult.value,
224        userMessage: resolvedUserMessage,
225        userImpact: check.userImpact || null,
226        userHelp: check.userHelp || [],
227        devAction: check.devAction,
228        evidence: {
229          method: testResult.method,
230          error: testResult.error || null
231        }
232      });
233    }
234  }
235
236  /**
237   * Ergebnis-Report generieren
238   */
239  getReport() {
240    const systemInfo = this.getSystemInfo();
241    const warnings = this.results.filter(r => r.severity === 'warning' && r.matched).length;
242    const criticals = this.results.filter(r => r.severity === 'critical' && r.matched).length;
243    const topFindings = this.results
244      .filter(r => r.matched)
245      .sort((a, b) => {
246        const severityOrder = { critical: 0, warning: 1, info: 2 };
247        return severityOrder[a.severity] - severityOrder[b.severity];
248      })
249      .slice(0, 3)
250      .map(r => r.title);
251
252    return {
253      reportId: this.reportId,
254      timestamp: this.timestamp,
255      url: window.location.href,
256      summary: {
257        totalChecks: this.results.length,
258        matched: this.results.filter(r => r.matched).length,
259        warningCount: warnings,
260        criticalCount: criticals,
261        topFindings: topFindings
262      },
263      checks: this.results,
264      systemProfile: systemInfo,
265      quickChecks: this.getQuickChecks(systemInfo)
266    };
267  }
268
269  /**
270   * Einfacher Text-Report für Copy
271   */
272  getTextReport() {
273    const report = this.getReport();
274    let text = `Browser Check Report\n`;
275    text += `===================\n\n`;
276    text += `Diagnose-ID: ${report.reportId}\n`;
277    text += `Zeit: ${report.timestamp}\n`;
278    text += `URL: ${report.url}\n\n`;
279    
280    text += `Zusammenfassung:\n`;
281    text += `- Überprüfte Einstellungen: ${report.summary.totalChecks}\n`;
282    text += `- Aktive Einstellungen: ${report.summary.matched}\n`;
283    text += `- Warnungen: ${report.summary.warningCount}\n`;
284    text += `- Kritisch: ${report.summary.criticalCount}\n\n`;
285
286    if (report.summary.topFindings.length > 0) {
287      text += `Wichtigste Hinweise:\n`;
288      report.summary.topFindings.forEach((finding, i) => {
289        text += `${i + 1}. ${finding}\n`;
290      });
291      text += `\n`;
292    }
293
294    if (report.quickChecks && report.quickChecks.length > 0) {
295      text += `Sofort-Checks:\n`;
296      report.quickChecks.forEach(item => {
297        text += `- ${item.label}: ${item.value}`;
298        if (item.note) text += ` (${item.note})`;
299        text += `\n`;
300      });
301      text += `\n`;
302    }
303
304    if (report.systemProfile) {
305      const sys = report.systemProfile;
306      text += `Systemprofil:\n`;
307      text += `- Browser: ${sys.browserName} ${sys.browserVersion}\n`;
308      text += `- OS: ${this.formatOsLabel(sys)}\n`;
309      text += `- Viewport: ${sys.width} x ${sys.height}px\n`;
310      text += `- Zoom: ${sys.zoom}% (${this.getZoomMethodLabel(sys.zoomMethod)})\n`;
311      text += `- DPR: ${sys.dpr}\n`;
312      text += `- Default Font: ${sys.rootFontSize}\n`;
313      text += `- Touch: ${sys.touchCapable ? 'Ja' : 'Nein'} (${sys.maxTouchPoints})\n`;
314      text += `- Sprache/Land: ${sys.language} / ${sys.country}\n`;
315      text += `- Zeit: ${sys.timeLabel} (${sys.timezone})\n`;
316      text += `- Body-Klassen: ${sys.bodyBrowserClasses.length > 0 ? sys.bodyBrowserClasses.join(', ') : 'Keine browser-* Klasse erkannt'}\n`;
317      text += `- User Agent: ${sys.userAgent}\n\n`;
318    }
319
320    text += `Details:\n`;
321    text += `--------\n`;
322    report.checks.forEach(check => {
323      if (check.matched) {
324        text += `\n[${check.severity.toUpperCase()}] ${check.title}\n`;
325        text += `Wert: ${check.value}\n`;
326        if (check.userMessage) text += `Hinweis: ${check.userMessage}\n`;
327        if (check.userImpact) text += `Auswirkung: ${check.userImpact}\n`;
328      }
329    });
330
331    return text;
332  }
333
334  /**
335   * JSON-Report
336   */
337  getJsonReport() {
338    return JSON.stringify(this.getReport(), null, 2);
339  }
340
341  /**
342   * Systeminformationen sammeln
343   */
344  getSystemInfo() {
345    // Standard Font Size aus Element style ermitteln
346    const rootFontSize = window.getComputedStyle(document.documentElement).fontSize;
347
348    // Zoom-Faktor konservativ bestimmen.
349    // Hinweis: Browser expose Desktop-Seitenzoom nur sehr eingeschraenkt.
350    // outerWidth / innerWidth wird durch angedockte DevTools oder Sidebars verfälscht.
351    let zoomFactor = 1;
352    let zoomMethod = 'default';
353
354    if (window.visualViewport && Number.isFinite(window.visualViewport.scale) && window.visualViewport.scale > 0) {
355      const viewportScale = window.visualViewport.scale;
356      if (Math.abs(viewportScale - 1) > 0.01) {
357        zoomFactor = viewportScale;
358        zoomMethod = 'visualViewport-scale';
359      } else {
360        zoomMethod = 'visualViewport';
361      }
362    }
363
364    if (zoomMethod === 'default' && window.outerWidth && window.innerWidth) {
365      const outerInnerFactor = window.outerWidth / window.innerWidth;
366      const widthDifference = window.outerWidth - window.innerWidth;
367      const likelyDockedSidebar = widthDifference > 420;
368
369      if (!likelyDockedSidebar && Number.isFinite(outerInnerFactor) && outerInnerFactor > 0.5 && outerInnerFactor < 1.4) {
370        zoomFactor = outerInnerFactor;
371        zoomMethod = 'outer-inner';
372      } else if (likelyDockedSidebar) {
373        zoomMethod = 'devtools-unreliable';
374      }
375    }
376
377    const zoomPercent = Math.round(zoomFactor * 100);
378
379    const language = navigator.language || 'Nicht erkannt';
380    const country = language.includes('-') ? language.split('-')[1].toUpperCase() : 'Nicht gesetzt';
381    const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
382    const timeLabel = new Intl.DateTimeFormat(undefined, {
383      hour: '2-digit',
384      minute: '2-digit'
385    }).format(new Date());
386
387    const browserInfo = this.getBrowserInfo();
388    const osInfo = this.getOsInfo();
389
390    const bodyClasses = Array.from(document.body?.classList || []);
391    const bodyBrowserClasses = bodyClasses.filter(cls => cls.startsWith('browser-') || cls.startsWith('browswer-'));
392
393    const maxTouchPoints = Number(navigator.maxTouchPoints || navigator.msMaxTouchPoints || 0);
394    const touchCapable = ('ontouchstart' in window) || maxTouchPoints > 0;
395
396    return {
397      width: window.innerWidth,
398      height: window.innerHeight,
399      dpr: window.devicePixelRatio,
400      rootFontSize: rootFontSize,
401      zoom: zoomPercent,
402      zoomMethod: zoomMethod,
403      browserName: browserInfo.name,
404      browserVersion: browserInfo.version,
405      osName: osInfo.name,
406      osVersion: osInfo.version,
407      osVersionSource: osInfo.versionSource || 'ua',
408      language: language,
409      country: country,
410      timeLabel: timeLabel,
411      timezone: timezone,
412      touchCapable: touchCapable,
413      maxTouchPoints: maxTouchPoints,
414      bodyBrowserClasses: bodyBrowserClasses,
415      userAgent: navigator.userAgent
416    };
417  }
418
419  /**
420   * OS aus User Agent herauslesen
421   */
422  getOsInfo() {
423    const ua = navigator.userAgent;
424    let m;
425
426    if ((m = ua.match(/iPad.*CPU OS ([\d_]+)/))) return { name: 'iPadOS', version: m[1].replace(/_/g, '.') };
427    if ((m = ua.match(/iPhone.*CPU.*OS ([\d_]+)/))) return { name: 'iOS', version: m[1].replace(/_/g, '.') };
428    if ((m = ua.match(/Android ([\d.]+)/))) return { name: 'Android', version: m[1] };
429    if ((m = ua.match(/Mac OS X ([\d_]+)/))) {
430      const macUaVersion = m[1].replace(/_/g, '.');
431      const isFrozenUaVersion = macUaVersion === '10.15.7';
432      return {
433        name: 'macOS',
434        version: isFrozenUaVersion ? '' : macUaVersion,
435        versionSource: isFrozenUaVersion ? 'ua-frozen' : 'ua',
436      };
437    }
438
439    const winNtMap = { '10.0': '10/11', '6.3': '8.1', '6.2': '8', '6.1': '7', '6.0': 'Vista', '5.2': 'XP x64', '5.1': 'XP', '5.0': '2000' };
440    if ((m = ua.match(/Windows NT ([\d.]+)/))) return { name: 'Windows', version: winNtMap[m[1]] || m[1], versionSource: 'ua' };
441    if (ua.includes('Win 9x 4.90')) return { name: 'Windows', version: 'ME', versionSource: 'ua' };
442    if (ua.includes('Windows 98')) return { name: 'Windows', version: '98', versionSource: 'ua' };
443    if (ua.includes('Windows 95')) return { name: 'Windows', version: '95', versionSource: 'ua' };
444    if (ua.includes('Linux')) return { name: 'Linux', version: '', versionSource: 'ua' };
445
446    return { name: 'Unbekannt', version: '', versionSource: 'ua' };
447  }
448
449  /**
450   * Browser aus User Agent herauslesen
451   */
452  getBrowserInfo() {
453    const ua = navigator.userAgent;
454    const patterns = [
455      { name: 'Edge', regex: /Edg\/(\d+(?:\.\d+)?)/ },
456      { name: 'Opera', regex: /OPR\/(\d+(?:\.\d+)?)/ },
457      { name: 'Chrome', regex: /Chrome\/(\d+(?:\.\d+)?)/ },
458      { name: 'Firefox', regex: /Firefox\/(\d+(?:\.\d+)?)/ },
459      { name: 'Safari', regex: /Version\/(\d+(?:\.\d+)?).*Safari\// }
460    ];
461
462    for (const entry of patterns) {
463      const match = ua.match(entry.regex);
464      if (match) {
465        return { name: entry.name, version: match[1] };
466      }
467    }
468
469    return { name: 'Unbekannt', version: '-' };
470  }
471
472  /**
473   * Zoom-Status prüfen
474   */
475  detectZoomWarning() {
476    const sysInfo = this.getSystemInfo();
477    // Ab ca. 105% oder 95% wird es fuer Layout-/Supportthemen relevant
478    return Math.abs(sysInfo.zoom - 100) > 5;
479  }
480
481  getZoomStatus(zoomPercent) {
482    if (zoomPercent >= 125 || zoomPercent <= 85) {
483      return { severity: 'critical', note: 'stark abweichend' };
484    }
485    if (zoomPercent >= 106 || zoomPercent <= 94) {
486      return { severity: 'warning', note: 'abweichend' };
487    }
488    return { severity: 'ok', note: 'normal' };
489  }
490
491  getZoomMethodLabel(method) {
492    if (method === 'visualViewport-scale') return 'Viewport-Scale';
493    if (method === 'visualViewport') return 'Viewport';
494    if (method === 'outer-inner') return 'Fenster-Proportion';
495    if (method === 'devtools-unreliable') return 'ungenaue Messung (DevTools?)';
496    return 'Standard';
497  }
498
499  formatOsLabel(systemInfo = this.getSystemInfo()) {
500    if (!systemInfo) return 'Unbekannt';
501
502    if (systemInfo.osName === 'macOS' && systemInfo.osVersionSource === 'ua-frozen') {
503      return 'macOS';
504    }
505
506    return systemInfo.osName + (systemInfo.osVersion ? ` ${systemInfo.osVersion}` : '');
507  }
508
509  getQuickChecks(systemInfo = this.getSystemInfo()) {
510    const zoomStatus = this.getZoomStatus(systemInfo.zoom);
511    const quickChecks = [
512      {
513        id: 'browser',
514        label: 'Browser + OS',
515        value: `${systemInfo.browserName} ${systemInfo.browserVersion}`,
516        note: this.formatOsLabel(systemInfo),
517        severity: 'info'
518      },
519      {
520        id: 'viewport',
521        label: 'Viewport',
522        value: `${systemInfo.width} x ${systemInfo.height}`,
523        severity: 'info'
524      }
525    ];
526
527    if (zoomStatus.severity !== 'ok') {
528      quickChecks.push({
529        id: 'zoom',
530        label: 'Zoom',
531        value: `${systemInfo.zoom}%`,
532        note: zoomStatus.note,
533        severity: zoomStatus.severity
534      });
535    } else {
536      quickChecks.push({
537        id: 'touch',
538        label: 'Touch',
539        value: systemInfo.touchCapable ? 'Ja' : 'Nein',
540        severity: 'info'
541      });
542    }
543
544    return quickChecks;
545  }
546
547  getGregorianEasterSunday(year) {
548    // Meeus/Jones/Butcher-Algorithmus (gregorianischer Kalender)
549    const a = year % 19;
550    const b = Math.floor(year / 100);
551    const c = year % 100;
552    const d = Math.floor(b / 4);
553    const e = b % 4;
554    const f = Math.floor((b + 8) / 25);
555    const g = Math.floor((b - f + 1) / 3);
556    const h = (19 * a + b - d - g + 15) % 30;
557    const i = Math.floor(c / 4);
558    const k = c % 4;
559    const l = (32 + 2 * e + 2 * i - h - k) % 7;
560    const m = Math.floor((a + 11 * h + 22 * l) / 451);
561    const month = Math.floor((h + l - 7 * m + 114) / 31); // 3=Maerz, 4=April
562    const day = ((h + l - 7 * m + 114) % 31) + 1;
563    return { year, month, day };
564  }
565
566  shiftDateUtc(year, month, day, offsetDays) {
567    const date = new Date(Date.UTC(year, month - 1, day + offsetDays));
568    return {
569      year: date.getUTCFullYear(),
570      month: date.getUTCMonth() + 1,
571      day: date.getUTCDate(),
572    };
573  }
574
575  toDateKey(year, month, day) {
576    const m = String(month).padStart(2, '0');
577    const d = String(day).padStart(2, '0');
578    return `${year}-${m}-${d}`;
579  }
580
581  getBadenWuerttembergHolidayName(year, month, day) {
582    const fixedHolidays = {
583      '01-01': 'Neujahr',
584      '01-06': 'Heilige Drei Koenige',
585      '05-01': 'Tag der Arbeit',
586      '10-03': 'Tag der Deutschen Einheit',
587      '11-01': 'Allerheiligen',
588      '12-25': '1. Weihnachtstag',
589      '12-26': '2. Weihnachtstag',
590    };
591
592    const fixedKey = `${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
593    if (fixedHolidays[fixedKey]) return fixedHolidays[fixedKey];
594
595    const easter = this.getGregorianEasterSunday(year);
596    const movableHolidays = [
597      { offset: -2, name: 'Karfreitag' },
598      { offset: 1, name: 'Ostermontag' },
599      { offset: 39, name: 'Christi Himmelfahrt' },
600      { offset: 50, name: 'Pfingstmontag' },
601      { offset: 60, name: 'Fronleichnam' },
602    ];
603
604    const targetKey = this.toDateKey(year, month, day);
605    for (const holiday of movableHolidays) {
606      const moved = this.shiftDateUtc(easter.year, easter.month, easter.day, holiday.offset);
607      if (this.toDateKey(moved.year, moved.month, moved.day) === targetKey) {
608        return holiday.name;
609      }
610    }
611
612    return '';
613  }
614
615  getSupportContext() {
616    const supportPortalUrl = 'https://support.vioma.de/portal';
617    const supportPhone = '+49 781 31055-500';
618    const supportHoursLabel = 'werktags 9-17 Uhr';
619    const supportTimeZone = 'Europe/Berlin';
620
621    const now = new Date();
622    const supportTimeParts = new Intl.DateTimeFormat('de-DE', {
623      weekday: 'short',
624      year: 'numeric',
625      month: '2-digit',
626      day: '2-digit',
627      hour: '2-digit',
628      minute: '2-digit',
629      hour12: false,
630      timeZone: supportTimeZone
631    }).formatToParts(now);
632    const getPart = (type) => {
633      const part = supportTimeParts.find(entry => entry.type === type);
634      return part ? part.value : '';
635    };
636
637    const weekdayShort = getPart('weekday').replace('.', '');
638    const year = Number(getPart('year'));
639    const month = Number(getPart('month'));
640    const day = Number(getPart('day'));
641    const hour = Number(getPart('hour'));
642    const minute = Number(getPart('minute'));
643    const timeLabel = new Intl.DateTimeFormat('de-DE', {
644      hour: '2-digit',
645      minute: '2-digit',
646      hour12: false,
647      timeZone: supportTimeZone
648    }).format(now);
649
650    const isWeekday = ['Mo', 'Di', 'Mi', 'Do', 'Fr'].includes(weekdayShort);
651    const isWithinHours = hour >= 9 && (hour < 17 || (hour === 17 && minute === 0));
652    const holidayName = this.getBadenWuerttembergHolidayName(year, month, day);
653    const isHoliday = Boolean(holidayName);
654    const isPhoneAvailable = isWeekday && isWithinHours && !isHoliday;
655
656    const hash = window.location.hash || '';
657    const cidMatch = hash.match(/^#cid-(\d+)$/i);
658    const cid = cidMatch ? cidMatch[1] : '';
659    const pageUrl = `${window.location.origin}${window.location.pathname}${window.location.search}`;
660    const cidUrl = cid ? `${pageUrl}#cid-${cid}` : '';
661
662    return {
663      supportPortalUrl,
664      supportPhone,
665      supportHoursLabel,
666      supportTimeZone,
667      isPhoneAvailable,
668      isHoliday,
669      holidayName,
670      supportTimeLabel: `${weekdayShort}, ${timeLabel}`,
671      pageUrl,
672      cid,
673      cidUrl,
674      locationLabel: cid ? `Element ${cid}` : 'Kein konkretes Seitenelement automatisch erkannt'
675    };
676  }
677
678  getSupportTicketText(cidOverride = null, details = null) {
679    const report = this.getReport();
680    const support = this.getSupportContext();
681    const sys = report.systemProfile;
682    const activeChecks = report.checks.filter(check => check.matched);
683
684    const resolvedCid = cidOverride ? cidOverride.cid : support.cid;
685    const resolvedCidUrl = resolvedCid ? `${support.pageUrl}#cid-${resolvedCid}` : '';
686    const resolvedLabel = cidOverride ? cidOverride.label : null;
687    const observation = String(details?.observation || '').trim() || '[Hier einfügen]';
688    const expected = String(details?.expected || '').trim() || '[Hier einfügen]';
689
690    let text = 'Support-Zusammenfassung (aus der Website Hilfe)\n';
691    text += '====================================\n\n';
692    text += 'Bitte hier kurz beschreiben, was nicht wie erwartet funktioniert:\n\n';
693    text += 'Beobachtung / was genau ist problematisch?\n';
694    text += `${observation}\n\n`;
695    text += 'Stattdessen war erwartet worden:\n';
696    text += `${expected}\n\n`;
697
698    text += '\n\nTicket-Kontext:\n';
699    text += `- Diagnose-ID: ${report.reportId}\n`;
700    text += `- Seite: ${support.pageUrl}\n`;
701    if (resolvedCidUrl) {
702      text += `- Problemstelle (Direktlink): ${resolvedCidUrl}\n`;
703      if (resolvedLabel) text += `- Problemstelle (Bezeichnung): ${resolvedLabel}\n`;
704      text += `- Element in Backend: ${window.location.origin}/condeon/frontend.php?type=in&page_content=${resolvedCid}\n`;
705    } else {
706      text += '- Problemstelle: Kein konkretes Seitenelement ausgewählt\n';
707    }
708    text += `- Zeitpunkt: ${support.supportTimeLabel} (${support.supportTimeZone})\n\n`;
709
710    text += 'Schnellprofil:\n';
711    text += `- Browser: ${sys.browserName} ${sys.browserVersion}\n`;
712    text += `- OS: ${this.formatOsLabel(sys)}\n`;
713    text += `- Viewport: ${sys.width} x ${sys.height}px\n`;
714    text += `- Zoom: ${sys.zoom}% (${this.getZoomMethodLabel(sys.zoomMethod)})\n`;
715    text += `- DPR: ${sys.dpr}\n`;
716    text += `- Touch: ${sys.touchCapable ? 'Ja' : 'Nein'} (${sys.maxTouchPoints})\n\n`;
717
718    text += `Auffällige Einstellungen (${activeChecks.length}):\n`;
719    if (activeChecks.length === 0) {
720      text += '- Keine darstellungsrelevanten Browser-Einstellungen aktiv\n';
721    } else {
722      activeChecks.forEach(check => {
723        text += `- [${check.severity.toUpperCase()}] ${check.title}: ${check.value}\n`;
724      });
725    }
726
727    return text;
728  }
729
730  getSupportSelfHelpItems(report = this.getReport()) {
731    const baseItems = [
732      {
733        key: 'incognito',
734        title: 'Seite im Inkognito- oder Privatmodus getestet?',
735        text: 'So lässt sich schnell prüfen, ob Browser-Erweiterungen oder zwischengespeicherte Daten die Ursache sind.'
736      },
737      {
738        key: 'reload',
739        title: 'Seite vollständig neu geladen?',
740        text: 'Laden Sie die Seite einmal neu und prüfen Sie, ob das Problem weiter besteht. Unter Windows: Strg + F5, unter Mac: Cmd + Shift + R.'
741      }
742    ];
743    const items = [];
744
745    const dynamicByCheckId = {
746      'forced-colors': {
747        key: 'forced-colors',
748        title: 'Haben Sie die Seite ohne Hochkontrastmodus geöffnet?',
749        text: 'Wenn Hochkontrast oder erzwungene Systemfarben aktiv sind, können Farben, Icons und Hintergründe stark abweichen.'
750      },
751      'prefers-contrast': {
752        key: 'prefers-contrast',
753        title: 'Haben Sie die Seite ohne Kontrastpräferenz geöffnet?',
754        text: 'Erhöhter oder reduzierter Kontrast kann Farben und Hervorhebungen sichtbar verändern.'
755      },
756      'prefers-reduced-motion': {
757        key: 'prefers-reduced-motion',
758        title: 'Haben Sie die Seite ohne reduzierte Bewegungen geöffnet?',
759        text: 'Animationen, Slider oder automatische Übergänge können sonst bewusst abgeschwächt oder deaktiviert sein.'
760      },
761      'prefers-reduced-transparency': {
762        key: 'prefers-reduced-transparency',
763        title: 'Haben Sie die Seite ohne reduzierte Transparenzen geöffnet?',
764        text: 'Transparente Flächen, Overlays oder Glaseffekte können dadurch anders aussehen als vorgesehen.'
765      },
766      'prefers-reduced-data': {
767        key: 'prefers-reduced-data',
768        title: 'Haben Sie die Seite ohne Datensparmodus geöffnet?',
769        text: 'Bilder, Videos oder weitere Inhalte können in reduzierter Form geladen oder bewusst weggelassen werden.'
770      },
771      'inverted-colors': {
772        key: 'inverted-colors',
773        title: 'Haben Sie die Seite ohne invertierte Farben geöffnet?',
774        text: 'Bei invertierten Farben können Symbole, Markenfarben und Kontraste stark verfälscht erscheinen.'
775      }
776    };
777
778    report.checks
779      .filter(check => check.matched)
780      .forEach(check => {
781        const item = dynamicByCheckId[check.id];
782        if (item && !items.some(entry => entry.key === item.key)) {
783          items.push(item);
784        }
785      });
786
787    const zoomStatus = this.getZoomStatus(report.systemProfile.zoom);
788    if (zoomStatus.severity !== 'ok' && !items.some(entry => entry.key === 'zoom')) {
789      items.push({
790        key: 'zoom',
791        title: 'Haben Sie die Seite bei 100 % Zoom geöffnet?',
792        text: `Aktuell wurde ein Zoom von ${report.systemProfile.zoom}% erkannt. Das kann Abstände, Umbrüche und Größen sichtbar verändern.`
793      });
794    }
795
796    baseItems.forEach(item => {
797      if (!items.some(entry => entry.key === item.key)) {
798        items.push(item);
799      }
800    });
801
802    return items;
803  }
804}
805
806/**
807 * Browser Check UI Manager
808 */
809class BrowserCheckUI {
810  constructor(browserCheck) {
811    this.browserCheck = browserCheck;
812    this.container = null;
813    this.shadowHost = null;
814    this.onClose = null;
815    this.closeTimeoutId = null;
816    this.supportSelfHelpState = {};
817    this.supportFlowStarted = false;
818    this.supportDraft = { observation: '', expected: '' };
819    this.cidPickerState = { state: 'idle', cid: null, label: null, lastSupportText: null };
820    this._pickerHandlers = null;
821    this.portalLaunchAt = 0;
822    this.portalStepTimer = null;
823    this.portalStepStorageKey = this.getPortalStepStorageKey();
824
825    try {
826      const storedPortalLaunchAt = Number(sessionStorage.getItem(this.portalStepStorageKey));
827      if (Number.isFinite(storedPortalLaunchAt) && storedPortalLaunchAt > 0) {
828        this.portalLaunchAt = storedPortalLaunchAt;
829      }
830      // One-shot restore: beim naechsten Aufruf einmal lesen und sofort leeren.
831      sessionStorage.removeItem(this.portalStepStorageKey);
832      // Altlast aus frueherer globaler Speicherung bereinigen.
833      sessionStorage.removeItem('bc_support_portal_clicked_at');
834    } catch (e) { /* ignore */ }
835  }
836
837  getPortalStepStorageKey() {
838    const pageKey = `${window.location.pathname}${window.location.search}`;
839    return `bc_support_portal_clicked_at::${pageKey}`;
840  }
841
842  getSeverityLabel(severity) {
843    if (severity === 'warning') return 'Warnung';
844    if (severity === 'critical') return 'Kritisch';
845    return 'Info';
846  }
847
848  formatNumber(value) {
849    const numericValue = Number(value);
850    if (!Number.isFinite(numericValue)) return String(value);
851    return String(Number(numericValue.toFixed(2)));
852  }
853
854  escapeHtml(value) {
855    return String(value || '')
856      .replace(/&/g, '&amp;')
857      .replace(/</g, '&lt;')
858      .replace(/>/g, '&gt;')
859      .replace(/"/g, '&quot;')
860      .replace(/'/g, '&#39;');
861  }
862
863  getDraftValue(key) {
864    return String(this.supportDraft?.[key] || '').trim();
865  }
866
867  isSupportDraftValid() {
868    return this.getDraftValue('observation').length >= 30 && this.getDraftValue('expected').length >= 30;
869  }
870
871  getZoomMethodLabel(method) {
872    return this.browserCheck.getZoomMethodLabel(method);
873  }
874
875  getIcon(name, className = '') {
876    const classAttr = className ? ` class="${className}"` : '';
877
878    if (name === 'search') {
879      return `<svg${classAttr} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"></circle><path d="m21 21-4.3-4.3"></path></svg>`;
880    }
881
882    if (name === 'close') {
883      return `<svg${classAttr} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>`;
884    }
885
886    if (name === 'chevron-right') {
887      return `<svg${classAttr} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
887<path d="m9 18 6-6-6-6"></path></svg>`;
888    }
889
890    if (name === 'circle-check') {
891      return `<svg${classAttr} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>`;
892    }
893
894    if (name === 'info') {
895      return `<svg${classAttr} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>`;
896    }
897
898    if (name === 'pencil') {
899      return `<svg${classAttr} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m17 3 4 4M3 17.25V21h3.75L17.81 9.94M21 21H3v-3.75L17.81 2.19l4 4" /></svg>`;
900    }
901
902    return '';
903  }
904
905  syncSupportSelfHelpState(items) {
906    items.forEach(item => {
907      if (!(item.key in this.supportSelfHelpState)) {
908        this.supportSelfHelpState[item.key] = false;
909      }
910    });
911
912    Object.keys(this.supportSelfHelpState).forEach(key => {
913      if (!items.some(item => item.key === key)) {
914        delete this.supportSelfHelpState[key];
915      }
916    });
917  }
918
919  isSupportSelfHelpComplete(items) {
920    if (!items || items.length === 0) return true;
921    return items.every(item => this.supportSelfHelpState[item.key]);
922  }
923
924  updateSupportUnlockState() {
925    if (!this.container) return;
926
927    const checklistButtons = Array.from(this.container.querySelectorAll('[data-selfhelp-key]'));
928    const remaining = checklistButtons.filter(button => button.getAttribute('aria-checked') !== 'true').length;
929    const allChecked = remaining === 0;
930    const stage = this.container.querySelector('.bc-support-stage');
931    const selfHelpCard = this.container.querySelector('.bc-support-selfhelp-card');
932    const nextStepSection = this.container.querySelector('.bc-support-nextstep-card');
933    const note = this.container.querySelector('.bc-support-selfhelp-note');
934
935    if (note) {
936      if (allChecked) {
937        note.textContent = 'Alles geprüft. Die nächsten Support-Schritte sind jetzt freigeschaltet.';
938        note.classList.add('is-complete');
939      } else {
940        note.textContent = `Bitte prüfen Sie zuerst alle offenen Punkte und haken Sie diese nach Ihrem Test ab. Noch ${remaining} offen.`;
941        note.classList.remove('is-complete');
942      }
943    }
944
945    if (!stage || !selfHelpCard || !nextStepSection) return;
946
947    if (allChecked) {
948      stage.classList.add('is-unlocked');
949      selfHelpCard.classList.add('is-leaving');
950      nextStepSection.classList.add('is-visible');
951      nextStepSection.setAttribute('aria-hidden', 'false');
952      return;
953    }
954
955    stage.classList.remove('is-unlocked');
956    selfHelpCard.classList.remove('is-leaving');
957    nextStepSection.classList.remove('is-visible');
958    nextStepSection.setAttribute('aria-hidden', 'true');
959  }
960
961
962  startSupportFlow() {
963    if (!this.container || this.supportFlowStarted) return;
964
965    this.supportFlowStarted = true;
966    const guide = this.container.querySelector('.bc-support-guide');
967    const startCard = this.container.querySelector('.bc-support-start');
968    const flow = this.container.querySelector('.bc-support-flow');
969
970    if (guide) guide.classList.add('is-started');
971    if (startCard) startCard.classList.add('is-hidden');
972    if (flow) {
973      flow.classList.add('is-visible');
974      flow.setAttribute('aria-hidden', 'false');
975    }
976
977    this.updateSupportUnlockState();
978    this.updatePickerButtonState();
979  }
980
981  updateSupportDraftField(field, value) {
982    if (!['observation', 'expected'].includes(field)) return;
983    this.supportDraft[field] = value;
984    if (this.cidPickerState.state === 'copied') {
985      this.cidPickerState.state = 'selected';
986    }
987    const counter = this.container?.querySelector(`[data-counter-for="${field}"]`);
988    if (counter) {
989      const len = value.trim().length;
990      const valid = len >= 30;
991      counter.textContent = valid ? `${len}\u2009\u2713` : `${len}\u00a0/\u00a030`;
992      counter.classList.toggle('is-valid', valid);
993    }
994    this.updatePickerButtonState();
995  }
996
997  focusFirstIncompleteSupportField() {
998    if (!this.container) return;
999
1000    const nextField = ['observation', 'expected'].find((field) => this.getDraftValue(field).length < 30);
1001    if (!nextField) return;
1002
1003    const target = this.container.querySelector(`textarea[data-support-field="${nextField}"]`);
1004    if (!target) return;
1005
1006    target.focus();
1007    const valueLength = target.value.length;
1008    if (typeof target.setSelectionRange === 'function') {
1009      target.setSelectionRange(valueLength, valueLength);
1010    }
1011  }
1012
1013  isPortalStepDone() {
1014    return this.portalLaunchAt > 0 && (Date.now() - this.portalLaunchAt) >= 60000;
1015  }
1016
1017  schedulePortalStepUpdate() {
1018    if (this.portalStepTimer) {
1019      window.clearTimeout(this.portalStepTimer);
1020      this.portalStepTimer = null;
1021    }
1022
1023    if (!this.portalLaunchAt || this.isPortalStepDone()) return;
1024
1025    const waitMs = Math.max(0, 60000 - (Date.now() - this.portalLaunchAt));
1026    this.portalStepTimer = window.setTimeout(() => {
1027      this.portalStepTimer = null;
1028      this.updatePickerButtonState();
1029    }, waitMs + 20);
1030  }
1031
1032  markPortalStepStarted() {
1033    this.portalLaunchAt = Date.now();
1034    try {
1035      sessionStorage.setItem(this.portalStepStorageKey, String(this.portalLaunchAt));
1036    } catch (e) { /* ignore */ }
1037
1038    this.schedulePortalStepUpdate();
1039    this.updatePickerButtonState();
1040  }
1041
1042  composeSupportSummary() {
1043    if (!['selected', 'copied'].includes(this.cidPickerState.state)) return;
1044    if (!this.isSupportDraftValid()) return;
1045
1046    const cidOverride = this.cidPickerState.cid
1047      ? { cid: this.cidPickerState.cid, label: this.cidPickerState.label }
1048      : null;
1049    const text = this.browserCheck.getSupportTicketText(cidOverride, {
1050      observation: this.getDraftValue('observation'),
1051      expected: this.getDraftValue('expected'),
1052    });
1053
1054    this.cidPickerState.lastSupportText = text;
1055    this.cidPickerState.state = 'copied';
1056
1057    navigator.clipboard.writeText(text).catch(err => console.error('Fehler beim Kopieren:', err));
1058    try {
1059      sessionStorage.setItem('bc_support_text', text);
1060      if (this.cidPickerState.cid) {
1061        sessionStorage.setItem('bc_support_cid', this.cidPickerState.cid);
1062      } else {
1063        sessionStorage.removeItem('bc_support_cid');
1064      }
1065    } catch (e) { /* ignore */ }
1066
1067    this.updatePickerButtonState();
1068  }
1069
1070  getFrontendEditBridge() {
1071    return window.__condeonAssist?.frontendEdit || null;
1072  }
1073
1074  getPickableCidElements() {
1075    const bridge = this.getFrontendEditBridge();
1076    const bridgedElements = bridge?.getSupportCidElements?.();
1077    if (Array.isArray(bridgedElements) && bridgedElements.length > 0) {
1078      return bridgedElements;
1079    }
1080    return Array.from(document.querySelectorAll('[data-cid]'));
1081  }
1082  toggleSupportSelfHelpItem(button) {
1083    const key = button.dataset.selfhelpKey;
1084    const nextState = !(button.getAttribute('aria-checked') === 'true');
1085
1086    this.supportSelfHelpState[key] = nextState;
1087    button.setAttribute('aria-checked', nextState ? 'true' : 'false');
1088    button.classList.toggle('is-checked', nextState);
1089
1090    this.updateSupportUnlockState();
1091  }
1092
1093  startElementPickMode() {
1094    const bridge = this.getFrontendEditBridge();
1095    const panelContainer = document.getElementById('bc-panel-container');
1096
1097    // Ensure the same effect as clicking "Elemente bearbeiten"
1098    // so selectable elements are visibly marked in support pick mode.
1099    if (sessionStorage.getItem('fe_edit_enabled') !== 'true') {
1100      sessionStorage.setItem('fe_edit_enabled', 'true');
1101      bridge?.enable?.();
1102      this.updateFrontendEditButtonState();
1103    }
1104
1105    const cidElements = this.getPickableCidElements();
1106    if (cidElements.length === 0) {
1107      this.copySupportWithoutCid();
1108      return;
1109    }
1110
1111    const previousPickerState = {
1112      state: this.cidPickerState.state,
1113      cid: this.cidPickerState.cid,
1114      label: this.cidPickerState.label,
1115    };
1116    this.cidPickerState.state = 'selecting';
1117    if (panelContainer) panelContainer.classList.add('bc-pick-mode-hidden');
1118
1119    if (bridge?.startSupportPickMode) {
1120      bridge.startSupportPickMode(
1121        (el) => {
1122          panelContainer?.classList.remove('bc-pick-mode-hidden');
1123          this.onCidPicked(el.dataset.cid, this.extractElementLabel(el));
1124        },
1125        () => {
1126          panelContainer?.classList.remove('bc-pick-mode-hidden');
1127          this.copySupportWithoutCid();
1128        },
1129        () => {
1130          panelContainer?.classList.remove('bc-pick-mode-hidden');
1131          this.cidPickerState.state = previousPickerState.state || 'idle';
1132          this.cidPickerState.cid = previousPickerState.cid || null;
1133          this.cidPickerState.label = previousPickerState.label || null;
1134          this.updatePickerButtonState();
1135        },
1136        cidElements
1137      );
1138      return;
1139    }
1140
1141    // Fallback: eigener DOM-Picker (wenn frontend_edit.js nicht geladen ist)
1142    bridge?.suspend?.();
1143    const eligibleCidElements = new Set(cidElements);
1144    document.body.classList.add('bc-pick-mode');
1145
1146    const highlight = document.createElement('div');
1147    highlight.id = 'bc-picker-highlight';
1148    document.body.appendChild(highlight);
1149
1150    const labelEl = document.createElement('div');
1151    labelEl.id = 'bc-picker-highlight-label';
1152    document.body.appendChild(labelEl);
1153
1154    const bar = document.createElement('div');
1155    bar.id = 'bc-picker-bar';
1156    bar.innerHTML = `
1157      <span>Klicken Sie auf die Problemstelle auf der Seite</span>
1158      <button id="bc-picker-no-cid" type="button">Ohne Element fortfahren</button>
1159      <button id="bc-picker-cancel" type="button">Abbrechen (Esc)</button>
1160    `;
1161    document.body.appendChild(bar);
1162
1163    let currentCidEl = null;
1164
1165    const positionHighlight = (el) => {
1166      const rect = el.getBoundingClientRect();
1167      highlight.style.top = `${rect.top}px`;
1168      highlight.style.left = `${rect.left}px`;
1169      highlight.style.width = `${rect.width}px`;
1170      highlight.style.height = `${rect.height}px`;
1171      highlight.style.display = 'block';
1172
1173      labelEl.textContent = `CID ${el.dataset.cid} – klicken zum Auswählen`;
1174      labelEl.style.display = 'block';
1175
1176      const labelWidth = labelEl.offsetWidth;
1177      const labelHeight = labelEl.offsetHeight;
1178      const viewportPadding = 8;
1179
1180      let top = rect.top - labelHeight - 8;
1181      if (top < viewportPadding) {
1182        top = rect.bottom + 8;
1183      }
1184      if (top + labelHeight > window.innerHeight - viewportPadding) {
1185        top = Math.max(viewportPadding, window.innerHeight - labelHeight - viewportPadding);
1186      }
1187
1188      let left = rect.left;
1189      if (left < viewportPadding) left = viewportPadding;
1190      if (left + labelWidth > window.innerWidth - viewportPadding) {
1191        left = Math.max(viewportPadding, window.innerWidth - labelWidth - viewportPadding);
1192      }
1193
1194      labelEl.style.top = `${top}px`;
1195      labelEl.style.left = `${left}px`;
1196    };
1197
1198    const onMouseMove = (e) => {
1199      if (e.target.closest('#bc-picker-bar')) return;
1200      const el = e.target.closest('[data-cid]');
1201      const eligibleEl = el && eligibleCidElements.has(el) ? el : null;
1202      if (eligibleEl !== currentCidEl) {
1203        currentCidEl = eligibleEl;
1204        if (eligibleEl) {
1205          positionHighlight(eligibleEl);
1206        } else {
1207          highlight.style.display = 'none';
1208          labelEl.style.display = 'none';
1209        }
1210      }
1211    };
1212
1213    const onScroll = () => {
1214      if (currentCidEl) positionHighlight(currentCidEl);
1215    };
1216
1217    const onClick = (e) => {
1218      if (e.target.closest('#bc-picker-bar')) return;
1219      const el = e.target.closest('[data-cid]');
1220      if (el && eligibleCidElements.has(el)) {
1221        e.preventDefault();
1222        e.stopPropagation();
1223        const cid = el.dataset.cid;
1224        const label = this.extractElementLabel(el);
1225        this.stopElementPickMode(true);
1226        this.onCidPicked(cid, label);
1227      }
1228    };
1229
1230    const onKeyDown = (e) => {
1231      if (e.key === 'Escape') this.stopElementPickMode(false, previousPickerState);
1232    };
1233
1234    bar.querySelector('#bc-picker-no-cid').addEventListener('click', () => {
1235      this.stopElementPickMode(true);
1236      this.copySupportWithoutCid();
1237    });
1238    bar.querySelector('#bc-picker-cancel').addEventListener('click', () => this.stopElementPickMode(false, previousPickerState));
1239    document.addEventListener('mousemove', onMouseMove, true);
1240    document.addEventListener('scroll', onScroll, true);
1241    document.addEventListener('click', onClick, true);
1242    document.addEventListener('keydown', onKeyDown, true);
1243
1244    this._pickerHandlers = { onMouseMove, onScroll, onClick, onKeyDown };
1245  }
1246
1247  stopElementPickMode(captured, previousState = null) {
1248    if (this._pickerHandlers) {
1249      document.removeEventListener('mousemove', this._pickerHandlers.onMouseMove, true);
1250      document.removeEventListener('scroll', this._pickerHandlers.onScroll, true);
1251      document.removeEventListener('click', this._pickerHandlers.onClick, true);
1252      document.removeEventListener('keydown', this._pickerHandlers.onKeyDown, true);
1253      this._pickerHandlers = null;
1254    }
1255    document.getElementById('bc-picker-highlight')?.remove();
1256    document.getElementById('bc-picker-highlight-label')?.remove();
1257    document.getElementById('bc-picker-bar')?.remove();
1258    document.body.classList.remove('bc-pick-mode');
1259    document.getElementById('bc-panel-container')?.classList.remove('bc-pick-mode-hidden');
1260    this.getFrontendEditBridge()?.resume?.();
1261    if (!captured) {
1262      if (previousState) {
1263        this.cidPickerState.state = previousState.state || 'idle';
1264        this.cidPickerState.cid = previousState.cid || null;
1265        this.cidPickerState.label = previousState.label || null;
1266      } else {
1267        this.cidPickerState.state = 'idle';
1268      }
1269      this.updatePickerButtonState();
1270    }
1271  }
1272
1273  extractElementLabel(el) {
1274    const tag = el.tagName.toLowerCase();
1275    const ariaLabel = el.getAttribute('aria-label');
1276    if (ariaLabel) return `${tag}: "${ariaLabel.substring(0, 60)}"`;
1277    const heading = el.querySelector('h1, h2, h3, h4, h5, h6');
1278    if (heading && heading.textContent.trim()) {
1279      return `${tag}: "${heading.textContent.trim().substring(0, 60)}"`;
1280    }
1281    const text = el.textContent.replace(/\s+/g, ' ').trim().substring(0, 60);
1282    if (text) return `${tag}: "${text}"`;
1283    return tag;
1284  }
1285
1286  onCidPicked(cid, label) {
1287    this.cidPickerState.state = 'selected';
1288    this.cidPickerState.cid = cid;
1289    this.cidPickerState.label = label;
1290    this.supportFlowStarted = true;
1291    this.updatePickerButtonState();
1292  }
1293
1294  copySupportWithoutCid() {
1295    this.cidPickerState.state = 'selected';
1296    this.cidPickerState.cid = null;
1297    this.cidPickerState.label = null;
1298    this.supportFlowStarted = true;
1299    this.updatePickerButtonState();
1300  }
1301
1302  updatePickerButtonState() {
1303    if (!this.container) return;
1304
1305    const pickBtn = this.container.querySelector('[data-action="pick-and-copy"], [data-action="reselect-element"]');
1306    const composeBtn = this.container.querySelector('[data-action="compose-support"]');
1307    const portalBtn = this.container.querySelector('[data-action="open-support"]');
1308    const actionsEl = this.container.querySelector('.bc-support-actions');
1309    const formStep = this.container.querySelector('.bc-support-describe');
1310    const composeActions = this.container.querySelector('.bc-support-compose-actions');
1311    const portalActions = this.container.querySelector('.bc-support-portal-actions');
1312    const selected = this.cidPickerState.state === 'selected' || this.cidPickerState.state === 'copied';
1313    const copied = this.cidPickerState.state === 'copied';
1314    const draftValid = this.isSupportDraftValid();
1315    const portalDone = this.isPortalStepDone();
1316    const showComposeArea = draftValid || copied;
1317
1318    if (pickBtn) {
1319      pickBtn.dataset.action = this.cidPickerState.cid ? 'reselect-element' : 'pick-and-copy';
1320      pickBtn.textContent = this.cidPickerState.cid ? 'Problemstelle ändern' : 'Problemstelle au
1320swählen';
1321      pickBtn.classList.toggle('bc-btn-secondary', selected);
1322    }
1323
1324    const stepPick = this.container.querySelector('[data-step="pick"] .bc-support-checkitem');
1325    const stepDescribe = this.container.querySelector('[data-step="describe"] .bc-support-checkitem');
1326    const stepPortal = this.container.querySelector('[data-step="portal"] .bc-support-checkitem');
1327    if (stepPick) stepPick.classList.toggle('is-checked', selected);
1328    if (stepDescribe) stepDescribe.classList.toggle('is-checked', draftValid);
1329    if (stepPortal) stepPortal.classList.toggle('is-checked', portalDone);
1330
1331    this.schedulePortalStepUpdate();
1332
1333    if (formStep) {
1334      const wasHidden = formStep.getAttribute('aria-hidden') === 'true';
1335      formStep.classList.toggle('is-active', selected);
1336      formStep.setAttribute('aria-hidden', selected ? 'false' : 'true');
1337      if (selected && wasHidden) {
1338        const firstTA = formStep.querySelector('textarea');
1339        if (firstTA) setTimeout(() => firstTA.focus(), 50);
1340      }
1341    }
1342
1343    if (composeActions) {
1344      composeActions.classList.toggle('is-active', showComposeArea);
1345      composeActions.setAttribute('aria-hidden', showComposeArea ? 'false' : 'true');
1346    }
1347
1348    if (portalActions) {
1349      portalActions.classList.toggle('is-active', copied);
1350      portalActions.setAttribute('aria-hidden', copied ? 'false' : 'true');
1351    }
1352
1353    if (composeBtn) {
1354      if (copied) {
1355        composeBtn.disabled = false;
1356        composeBtn.textContent = 'Zusammenfassung erneut kopieren';
1357        composeBtn.classList.add('bc-btn-secondary');
1358      } else {
1359        composeBtn.disabled = !draftValid;
1360        composeBtn.textContent = 'Zusammenfassung kopieren';
1361        composeBtn.classList.remove('bc-btn-secondary');
1362      }
1363    }
1364
1365    if (portalBtn) {
1366      if (copied) {
1367        portalBtn.removeAttribute('disabled');
1368        portalBtn.classList.add('bc-cta-ready');
1369      } else {
1370        portalBtn.setAttribute('disabled', 'disabled');
1371        portalBtn.classList.remove('bc-cta-ready');
1372      }
1373    }
1374
1375    if (actionsEl) {
1376      let note = actionsEl.querySelector('.bc-support-cid-note');
1377      if (!note) {
1378        note = document.createElement('div');
1379        note.className = 'bc-support-cid-note';
1380        actionsEl.appendChild(note);
1381      }
1382
1383      if (copied) {
1384        if (this.cidPickerState.cid) {
1385          note.innerHTML = `Problemstelle: CID&nbsp;${this.cidPickerState.cid}. <button type="button" class="bc-support-cid-change">Ändern</button>`;
1386          note.querySelector('.bc-support-cid-change').addEventListener('click', () => this.handleAction('reselect-element'));
1387        } else {
1388          note.textContent = 'Zusammenfassung kopiert (ohne konkrete Elementauswahl).';
1389        }
1390      } else if (selected) {
1391        note.textContent = this.cidPickerState.cid
1392          ? `Problemstelle ausgewählt: CID\u00a0${this.cidPickerState.cid}.`
1393          : '';
1394      } else {
1395        note.textContent = '';
1396      }
1397    }
1398  }
1399
1400  /**
1401   * UI rendern
1402   */
1403  render(targetElement) {
1404    if (!targetElement) {
1405      console.warn('BrowserCheck: Kein Target-Element angegeben');
1406      return;
1407    }
1408
1409    if (sessionStorage.getItem('fe_edit_enabled') === null) {
1410      sessionStorage.setItem('fe_edit_enabled', 'false');
1411    }
1412
1413    const shadowHost = document.createElement('browser-check-panel');
1414
1415    this.container = document.createElement('div');
1416    this.container.className = 'bc-panel';
1417    this.container.innerHTML = this.getHTML();
1418
1419    targetElement.innerHTML = '';
1420    shadowHost.shadowRoot.appendChild(this.container);
1421    targetElement.appendChild(shadowHost);
1422    this.shadowHost = shadowHost;
1423    this.attachEventListeners();
1424  }
1425
1426  renderLoading(targetElement, statusText = 'Browser-Umgebung wird analysiert...') {
1427    if (!targetElement) return;
1428
1429    const shadowHost = document.createElement('browser-check-panel');
1430    const loadingPanel = document.createElement('div');
1431    loadingPanel.className = 'bc-panel bc-panel-loading';
1432    loadingPanel.innerHTML = `
1433      <div class="bc-header">
1434        <h2><span class="bc-title-icon">${this.getIcon('search', 'bc-icon bc-icon-search')}</span><span>Website Hilfe</span></h2>
1435      </div>
1436      <div class="bc-loading">
1437        <div class="bc-loading-title">Analyse läuft</div>
1438        <div class="bc-loading-text">${statusText}</div>
1439        <div class="bc-loading-bar" role="progressbar" aria-label="Browser Check läuft">
1440          <span class="bc-loading-bar-fill"></span>
1441        </div>
1442      </div>
1443    `;
1444
1445    targetElement.innerHTML = '';
1446    shadowHost.shadowRoot.appendChild(loadingPanel);
1447    targetElement.appendChild(shadowHost);
1448  }
1449
1450  /**
1451   * HTML für die UI generieren
1452   */  
1453  getHTML() {
1454    const report = this.browserCheck.getReport();
1455    const support = this.browserCheck.getSupportContext();
1456    const currentYear = new Date().getFullYear();
1457    this.supportFlowStarted = this.cidPickerState.state === 'selected' || this.cidPickerState.state === 'copied';
1458    const selfHelpItems = this.browserCheck.getSupportSelfHelpItems(report);
1459    this.syncSupportSelfHelpState(selfHelpItems);
1460    const selfHelpComplete = this.isSupportSelfHelpComplete(selfHelpItems);
1461    const activeChecks = report.checks.filter(check => check.matched);
1462    const inactiveChecks = report.checks.filter(check => !check.matched);
1463    const hasWarnings = report.summary.warningCount > 0;
1464    const ampelClass = hasWarnings ? 'warning' : 'ok';
1465    let revealIndex = 0;
1466    const nextRevealDelay = () => `${90 + (revealIndex++ * 85)}ms`;
1467    const quickChecks = report.quickChecks || [];
1468
1469    let html = `
1470      <div class="bc-header">
1471        <h2><span class="bc-title-icon">${this.getIcon('search', 'bc-icon bc-icon-search')}</span><span>Website Hilfe</span></h2>
1472        <div style="display: flex; gap: 8px; align-items: center;">
1473          <button class="bc-toggle-fe-edit" data-action="toggle-fe-edit" aria-label="Frontend Edit umschalten" title="Frontend Edit aktivieren/deaktivieren" style="background: transparent; border: 1px solid var(--bc-line); border-radius: 16px; height: 34px; padding: 0 12px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 150ms ease; color: var(--bc-text-soft); font-size: 13px; font-weight: 500; gap: 6px;">${this.getIcon('pencil', 'bc-icon')} <span>Elemente bearbeiten</span></button>
1474          <button class="bc-close" data-action="close" aria-label="Website Hilfe schließen">${this.getIcon('close', 'bc-icon bc-icon-close')}</button>
1475        </div>
1476      </div>
1477
1478      <div class="bc-summary">
1479        <div class="bc-ampel bc-ampel-${ampelClass}"></div>
1480        <div class="bc-summary-text">
1481          <strong>Diagnose-ID:</strong> ${report.reportId}<br>
1482          <strong>Überprüfte Einstellungen:</strong> ${report.summary.matched} von ${report.summary.totalChecks} problematisch
1483        </div>
1484      </div>
1485    `;
1486
1487    if (quickChecks.length > 0) {
1488      html += `<div class="bc-quickchecks">`;
1489      quickChecks.forEach(item => {
1490        const severity = item.severity || 'info';
1491        const noteHtml = item.note ? `<small>${item.note}</small>` : '';
1492        html += `
1493          <div class="bc-quickcheck bc-quickcheck-${severity}" style="--bc-stagger-delay: ${nextRevealDelay()};">
1494            <span class="bc-quickcheck-label">${item.label}</span>
1495            <span class="bc-quickcheck-value">${item.value}</span>
1496            ${noteHtml}
1497          </div>
1498        `;
1499      });
1500      html += `</div>`;
1501    }
1502
1503    html += `<div class="bc-results">`;
1504    if (activeChecks.length === 0) {
1505      html += `<div class="bc-check bc-check-ok" style="--bc-stagger-delay: ${nextRevealDelay()};"><div class="bc-allok-inner">${this.getIcon('circle-check', 'bc-icon bc-allok-icon')}<div class="bc-check-message"><strong>Alles unauffällig.</strong><span class="bc-check-subtext">Der Browser und Betriebssystem greift nicht in die Darstellung ein.</span></div></div></div>`;
1506    }
1507
1508    activeChecks.forEach(check => {
1509      const matchClass = 'matched';
1510      const severityClass = `bc-check-severity-${check.severity}`;
1511      const severityBadge = `<span class="bc-badge bc-severity-${check.severity}">${this.getSeverityLabel(check.severity)}</span>`;
1512
1513      let helpHtml = '';
1514      if (check.matched && check.userHelp && check.userHelp.length > 0) {
1515        const stepsHtml = check.userHelp.map(entry => `
1516          <div class="bc-help-os">
1517            <span class="bc-help-os-label">${entry.os}</span>
1518            <ol class="bc-help-steps">${entry.steps.map(s => `<li>${s}</li>`).join('')}</ol>
1519          </div>
1520        `).join('');
1521        helpHtml = `
1522          <details class="bc-help">
1523            <summary class="bc-help-toggle">${this.getIcon('chevron-right', 'bc-icon bc-icon-chevron')}<span>Wie kann ich das deaktivieren?</span></summary>
1524            <div class="bc-help-body">${stepsHtml}</div>
1525          </details>
1526        `;
1527      }
1528
1529      html += `
1530        <div class="bc-check bc-check-${matchClass} ${severityClass}" style="--bc-stagger-delay: ${nextRevealDelay()};">
1531          <div class="bc-check-header">
1532            <strong>${check.title}</strong>
1533            ${check.matched ? severityBadge : ''}
1534          </div>
1535          ${check.matched && check.userMessage ? `<div class="bc-check-message">${check.userMessage}</div>` : ''}
1536          ${check.matched && check.userImpact ? `<div class="bc-check-impact">${check.userImpact}</div>` : ''}
1537          ${helpHtml}
1538        </div>
1539      `;
1540    });
1541
1542    if (inactiveChecks.length > 0) {
1543      html += `
1544        <details class="bc-ok-group" style="--bc-stagger-delay: ${nextRevealDelay()};">
1545          <summary class="bc-ok-group-toggle">${this.getIcon('chevron-right', 'bc-icon bc-icon-chevron')}<span>Alles ok (${inactiveChecks.length})</span></summary>
1546          <ul class="bc-ok-list">
1547            ${inactiveChecks.map(check => `<li><span class="bc-ok-title">${check.title}</span> <span class="bc-ok-meta">nicht aktiv</span></li>`).join('')}
1548          </ul>
1549        </details>
1550      `;
1551    }
1552
1553    html += `</div>`;
1554    const sysInfo = report.systemProfile || this.browserCheck.getSystemInfo();
1555    const hasZoomWarning = this.browserCheck.getZoomStatus(sysInfo.zoom).severity !== 'ok';
1556    const zoomWarningHtml = hasZoomWarning ? `<div class="bc-device-warning"><strong>Hinweis:</strong> Zoom erkannt (${sysInfo.zoom}%). Das kann die Darstellung beeinflussen.</div>` : '';
1557    const touchLabel = sysInfo.touchCapable ? 'Ja' : 'Nein';
1558    const dprLabel = this.formatNumber(sysInfo.dpr);
1559    const zoomLabel = this.formatNumber(sysInfo.zoom);
1560    const maxTouchPointsLabel = this.formatNumber(sysInfo.maxTouchPoints);
1561    const rootFontNumeric = parseFloat(sysInfo.rootFontSize);
1562    const rootFontLabel = Number.isFinite(rootFontNumeric)
1563      ? `${this.formatNumber(rootFontNumeric)}px`
1564      : sysInfo.rootFontSize;
1565    const timeLabel = `${sysInfo.timeLabel} <small>(${sysInfo.timezone})</small>`;
1566    let infoCardIndex = 0;
1567    const nextInfoDelay = () => `${620 + (infoCardIndex++ * 70)}ms`;
1568    const renderInfoCard = (key, value, extraClass = '') => `
1569      <div class="bc-info-card ${extraClass}" style="--bc-stagger-delay: ${nextInfoDelay()};">
1570        <span class="bc-info-key">${key}</span>
1571        <span class="bc-info-value">${value}</span>
1572      </div>
1573    `;
1574    const bodyClassesLabel = sysInfo.bodyBrowserClasses.length > 0
1575      ? sysInfo.bodyBrowserClasses.map(cls => `<span class="bc-chip">${cls}</span>`).join('')
1576      : '<span class="bc-chip bc-chip-muted">Keine browser-* Klasse erkannt</span>';
1577
1578    const technicalDetailsHtml = `
1579      <details class="bc-device-group" style="--bc-stagger-delay: ${nextRevealDelay()};">
1580        <summary class="bc-device-group-toggle">${this.getIcon('chevron-right', 'bc-icon bc-icon-chevron')}<span>Systemprofil</span><small>Technische Rohdaten und Browserumgebung</small></summary>
1581        <div class="bc-device-info">
1582          ${zoomWarningHtml}
1583
1584          <div class="bc-device-grid">
1585            ${renderInfoCard('Browser', `${sysInfo.browserName} ${sysInfo.browserVersion}`)}
1586            ${renderInfoCard('OS', `${this.browserCheck.formatOsLabel(sysInfo)}`)}
1587            ${renderInfoCard('Viewport', `${sysInfo.width} x ${sysInfo.height}px`)}
1588            ${renderInfoCard('Zoom', `${zoomLabel}% <small>(${this.getZoomMethodLabel(sysInfo.zoomMethod)})</small>`, hasZoomWarning ? 'is-warning' : '')}
1589            ${renderInfoCard('DPR', dprLabel)}
1590            ${renderInfoCard('Default Font', rootFontLabel)}
1591            ${renderInfoCard('Touch', `${touchLabel} (${maxTouchPointsLabel})`)}
1592            ${renderInfoCard('Sprache', sysInfo.language)}
1593            ${renderInfoCard('Land', sysInfo.country)}
1594            ${renderInfoCard('Zeit', timeLabel)}
1595          </div>
1596
1597          <div class="bc-device-classes">
1598            <span class="bc-device-label">Body-Klassen:</span>
1599            <div class="bc-chip-list">${bodyClassesLabel}</div>
1600          </div>
1601        </div>
1602      </details>
1603
1604      <div class="bc-device-useragent-block" style="--bc-stagger-delay: ${nextRevealDelay()};">
1605        <div class="bc-device-useragent">
1606          <span class="bc-device-label">User Agent:</span>
1607          <code>${sysInfo.userAgent}</code>
1608        </div>
1609      </div>
1610    `;
1611
1612    const supportStatusClass = support.isPhoneAvailable ? 'is-open' : 'is-closed';
1613    const supportStatusText = support.isPhoneAvailable
1614      ? `Notfall? Unsere Hotline ist jetzt erreichbar (${support.supportHoursLabel}).`
1615      : support.isHoliday
1616        ? `Unsere Hotline ist heute wegen Feiertag (${support.holidayName}) nicht besetzt (${support.supportHoursLabel}).`
1617        : `Unsere Hotline ist aktuell nicht besetzt (${support.supportHoursLabel}).`;
1618    const supportSubText = support.isPhoneAvailable
1619      ? `Telefon: ${support.supportPhone}`
1620      : `Telefon: ${support.supportPhone} · Tickets können Sie rund um die Uhr im Support-Portal anlegen.`;
1621    const supportFlowStarted = this.supportFlowStarted;
1622    const selfHelpHtml = selfHelpItems.map(item => `
1623            <li>
1624              <button type="button" class="bc-support-checkitem ${this.supportSelfHelpState[item.key] ? 'is-checked' : ''}" data-selfhelp-key="${item.key}" role="checkbox" aria-checked="${this.supportSelfHelpState[item.key] ? 'true' : 'false'}">
1625                <span class="bc-support-checkcontrol" aria-hidden="true"></span>
1626                <span class="bc-support-checktext"><strong>${item.title}</strong><small>${item.text}</small></span>
1627              </button>
1628            </li>
1629          `).join('');
1630
1631    html += `
1632      <section class="bc-support-guide ${supportFlowStarted ? 'is-started' : ''}" style="--bc-stagger-delay: ${nextRevealDelay()};">
1633        <h3>Benötigen Sie Hilfe?</h3>
1634        <p class="bc-support-intro">Helfen Sie uns, Ihnen zu helfen. Prüfen Sie zuerst kurz die Selbsthilfe und nutzen Sie anschließend bevorzugt unser Support-Portal.</p>
1635
1636        <div class="bc-support-start ${supportFlowStarted ? 'is-hidden' : ''}" aria-hidden="${supportFlowStarted ? 'true' : 'false'}">
1637          <strong>Supportvorgang starten</strong>
1638          <p>Öffnen Sie den geführten Ablauf mit Checkliste und Ticket-Übergabe.</p>
1639          <button class="bc-btn" data-action="start-support-flow">Supportvorgang starten</button>
1640        </div>
1641
1642        <div class="bc-support-flow ${supportFlowStarted ? 'is-visible' : ''}" aria-hidden="${supportFlowStarted ? 'false' : 'true'}">
1643
1644        <div class="bc-support-stage ${selfHelpComplete ? 'is-unlocked' : ''}">
1645          <div class="bc-support-selfhelp bc-support-selfhelp-card ${selfHelpComplete ? 'is-leaving' : ''}">
1646            <div class="bc-support-selfhelp-title">Schnelle Hilfe zur Selbsthilfe</div>
1647            <div class="bc-support-selfhelp-note ${selfHelpComplete ? 'is-complete' : ''}">${selfHelpComplete ? 'Alles geprüft. Die nächsten Support-Schritte sind jetzt freigeschaltet.' : `Bitte prüfen Sie zuerst alle offenen Punkte und haken Sie diese nach Ihrem Test ab. Noch ${selfHelpItems.length} offen.`}</div>
1648            <ul class="bc-support-selfhelp-list" aria-label="Selbsthilfe-Checkliste">
1649              ${selfHelpHtml}
1650            </ul>
1651          </div>
1652
1653          <div class="bc-support-nextstep-card ${selfHelpComplete ? 'is-visible' : ''}" aria-hidden="${selfHelpComplete ? 'false' : 'true'}">
1654            
1655
1656            <ol class="bc-support-steps" aria-label="Support in drei Schritten">
1657              <li data-step="pick">
1658                <div class="bc-support-checkitem bc-support-stepitem" role="presentation">
1659                  <span class="bc-support-checkcontrol" aria-hidden="true"></span>
1660                  <span class="bc-support-checktext"><strong>Problemstelle auswählen</strong><small>Markieren Sie das betroffene Element auf der Seite (optional auch ohne Element).</small></span>
1661                </div>
1662              </li>
1663              <li data-step="describe">
1664                <div class="bc-support-checkitem bc-support-stepitem" role="presentation">
1665                  <span class="bc-support-checkcontrol" aria-hidden="true"></span>
1666                  <span class="bc-support-checktext"><strong>Problem kurz beschreiben</strong><small>Füllen Sie Beobachtung und Erwartung aus, damit der Support direkt starten kann.</small></span>
1667                </div>
1668              </li>
1669              <li data-step="portal">
1670                <div class="bc-support-checkitem bc-support-stepitem" role="presentation">
1671                  <span class="bc-support-checkcontrol" aria-hidden="true"></span>
1672                  <span class="bc-support-checktext"><strong>Ticket im Portal anlegen</strong><small>Fügen Sie den kopierten Text ins Feld "Ausführliche Beschreibung" ein.</small></span>
1673                </div>
1674              </li>
1675            </ol>
1676
1677            <div class="bc-support-actions">
1678              <button class="bc-btn" data-action="pick-and-copy">Problemstelle auswählen</button>
1679            </div>
1680
1681            <div class="bc-support-describe" aria-hidden="true">
1682              <label class="bc-support-field">
1683                <span class="bc-support-field-header">
1684                  <span>Beobachtung / was genau ist hier problematisch?</span>
1685                  ${(() => { const n = this.getDraftValue('observation').length; return `<span class="bc-support-field-counter${n >= 30 ? ' is-valid' : ''}" data-counter-for="observation">${n >= 30 ? n + '\u2009\u2713' : n + '\u00a0/\u00a030'}</span>`; })()}
1686                </span>
1687				<textarea data-support-field="observation" rows="3" placeholder="Zum Beispiel: Das gepflegte Bild an der Stelle ist unscharf. \nOder: Der Text an der Stelle sieht sehr groß aus, das passt nicht gut ins Gesamtbild der Seite.">${this.escapeHtml(this.supportDraft.observation)}</textarea>
1688              </label>
1689              <label class="bc-support-field">
1690                <span class="bc-support-field-header">
1691                  <span>Erwartet haben Sie aber:</span>
1692                  ${(() => { const n = this.getDraftValue('expected').length; return `<span class="bc-support-field-counter${n >= 30 ? ' is-valid' : ''}" data-counter-for="expected">${n >= 30 ? n + '\u2009\u2713' : n + '\u00a0/\u00a030'}</span>`; })()}
1693                </span>
1694                <textarea data-support-field="expected" rows="3" placeholder="Zum Beispiel: Das Bild sollte scharf sein. \nOder: Der Text sollte etwas kleiner als die normale Schriftgröße sein.">${this.escapeHtml(this.supportDraft.expected)}</textarea>
1695              </label>
1696            </div>
1697
1698            <div class="bc-support-compose-actions" aria-hidden="true">
1699              <button class="bc-btn bc-btn-secondary" data-action="compose-support">Zusammenfassung kopieren</button>
1700            </div>
1701
1702            <div class="bc-support-portal-actions" aria-hidden="true">
1703              <strong>Ticket anlegen</strong>
1704              <p>Sie können jetzt zu HubSpot wechseln und dort ein neues Ticket anlegen. Fügen Sie die kopierte Zusammenfassung in das Feld „Ausführliche Beschreibung" ein.</p>
1705              <button class="bc-btn" data-action="open-support" disabled>Ticket erstellen</button>
1706            </div>
1707
1708            <div class="bc-support-status ${supportStatusClass}">
1709              ${this.getIcon('info', 'bc-icon bc-support-status-icon')}
1710              <div>
1711                <strong>${supportStatusText}</strong>
1712                <small>${supportSubText}</small>
1713              </div>
1714            </div>
1715          </div>
1716        </div>
1717        </div>
1718      </section>
1719    `;
1720
1721    html += technicalDetailsHtml;
1722
1723    html += `
1724      <div class="bc-actions">
1725        <button class="bc-btn" data-action="copy-text">Technischen Bericht kopieren</button>
1726        <button class="bc-btn bc-btn-secondary" data-action="copy-json">JSON kopieren</button>
1727      </div>
1728
1729      <footer class="bc-footer">
1730        <div class="bc-footer-copy">&copy; ${currentYear} vioma GmbH</div>
1731        <picture class="bc-footer-logo">
1732          <source media="(prefers-color-scheme: dark)" srcset="https://www.vioma.de/media/87043/vioma-logo-vec-white-rgb-3016109.svg">
1733          <img src="https://www.vioma.de/media/87043/vioma-logo-vec-black-rgb-3016107.svg" alt="vioma GmbH" width="112" height="28">
1734        </picture>
1735      </footer>
1736    `;
1737
1738    return html;
1739  }
1740
1741
1742  toggleFrontendEdit() {
1743    const isEnabled = sessionStorage.getItem('fe_edit_enabled') === 'true';
1744    sessionStorage.setItem('fe_edit_enabled', !isEnabled ? 'true' : 'false');
1745    
1746    this.updateFrontendEditButtonState();
1747    
1748    if (window.__condeonAssist?.frontendEdit) {
1749      if (isEnabled) {
1750        window.__condeonAssist.frontendEdit.disable?.();
1751      } else {
1752        window.__condeonAssist.frontendEdit.enable?.();
1753      }
1754    }
1755  }
1756
1757  updateFrontendEditButtonState() {
1758    const toggleBtn = this.container?.querySelector('[data-action="toggle-fe-edit"]');
1759    if (toggleBtn) {
1760      const isEnabled = sessionStorage.getItem('fe_edit_enabled') === 'true';
1761      toggleBtn.style.borderColor = isEnabled ? 'var(--bc-accent)' : 'var(--bc-line)';
1762      toggleBtn.style.color = isEnabled ? 'var(--bc-accent)' : 'var(--bc-text-soft)';
1763      toggleBtn.style.backgroundColor = isEnabled ? 'rgba(33, 150, 243, 0.12)' : 'transparent';
1764    }
1765  }
1766
1767  attachEventListeners() {
1768    this.updateFrontendEditButtonState();
1769
1770    const buttons = this.container.querySelectorAll('[data-action]');
1771    buttons.forEach(btn => {
1772      btn.addEventListener('click', (e) => {
1773        const action = e.currentTarget.dataset.action;
1774        this.handleAction(action);
1775      });
1776    });
1777
1778    const pickStepItem = this.container.querySelector('[data-step="pick"]');
1779    if (pickStepItem) {
1780      pickStepItem.addEventListener('click', () => {
1781        const action = this.cidPickerState.cid ? 'reselect-element' : 'pick-and-copy';
1782        this.handleAction(action);
1783      });
1784
1785      pickStepItem.addEventListener('touchend', (e) => {
1786        e.preventDefault();
1787        const action = this.cidPickerState.cid ? 'reselect-element' : 'pick-and-copy';
1788        this.handleAction(action);
1789      }, { passive: false });
1790    }
1791
1792    const describeStepItem = this.container.querySelector('[data-step="describe"]');
1793    if (describeStepItem) {
1794      describeStepItem.addEventListener('click', () => {
1795        this.focusFirstIncompleteSupportField();
1796      });
1797
1798      describeStepItem.addEventListener('touchend', (e) => {
1799        e.preventDefault();
1800        this.focusFirstIncompleteSupportField();
1801      }, { passive: false });
1802    }
1803
1804    const selfHelpButtons = this.container.querySelectorAll('[data-selfhelp-key]');
1805    selfHelpButtons.forEach(button => {
1806      button.addEventListener('click', (e) => {
1807        this.toggleSupportSelfHelpItem(e.currentTarget);
1808      });
1809    });
1810
1811    const draftFields = this.container.querySelectorAll('[data-support-field]');
1812    draftFields.forEach(field => {
1813      field.addEventListener('input', (e) => {
1814        this.updateSupportDraftField(e.currentTarget.dataset.supportField, e.currentTarget.value);
1815      });
1816    });
1817
1818    this.updateSupportUnlockState();
1819    this.updatePickerButtonState();
1820  }
1821
1822  /**
1823   * Aktionen handhaben
1824   */
1825  handleAction(action) {
1826    switch (action) {
1827      case 'close':
1828        if (!this.container || this.container.classList.contains('is-closing')) break;
1829
1830        this.container.classList.add('is-closing');
1831        if (this.closeTimeoutId) window.clearTimeout(this.closeTimeoutId);
1832
1833        // Sofort: Container raussliden + Badge wieder zeigen
1834        if (typeof this.onClose === 'function') this.onClose();
1835
1836        // Inhalt still im Hintergrund leeren, nachdem Animation abgeklungen ist
1837        this.closeTimeoutId = window.setTimeout(() => {
1838          if (this.container) this.container.innerHTML = '';
1839          this.closeTimeoutId = null;
1840        }, 500);
1841        break;
1842      case 'toggle-fe-edit':
1843        this.toggleFrontendEdit();
1844        break;
1845      case 'copy-text':
1846        this.copyToClipboard(this.browserCheck.getTextReport(), 'Text-Report kopiert!');
1847        break;
1848      case 'copy-json':
1849        this.copyToClipboard(this.browserCheck.getJsonReport(), 'JSON kopiert!');
1850        break;
1851      case 'copy-support':
1852        this.copyToClipboard(this.browserCheck.getSupportTicketText(), 'Support-Zusammenfassung kopiert!');
1853        break;
1854      case 'pick-and-copy':
1855        this.startElementPickMode();
1856        break;
1857      case 'recopy-support': {
1858        const textToRecopy = this.cidPickerState.lastSupportText || '';
1859        if (textToRecopy) {
1860          navigator.clipboard.writeText(textToRecopy).catch(err => console.error('Fehler beim Kopieren:', err));
1861        }
1862        break;
1863      }
1864      case 'reselect-element':
1865        this.startElementPickMode();
1866        break;
1867      case 'open-support':
1868        this.markPortalStepStarted();
1869        window.open('https://support.vioma.de/portal/new', '_blank', 'noopener');
1870        break;
1871      case 'compose-support':
1872        this.composeSupportSummary();
1873        break;
1874      case 'start-support-flow':
1875        this.startSupportFlow();
1876        break;
1877    }
1878  }
1879
1880  /**
1881   * In Zwischenablage kopieren
1882   */
1883  copyToClipboard(text, message) {
1884    navigator.clipboard.writeText(text).then(() => {
1885      alert(message);
1886    }).catch(err => {
1887      console.error('Fehler beim Kopieren:', err);
1888    });
1889  }
1890}
1891
1892/**
1893 * Custom Element für Shadow DOM Isolation des Panels
1894 * Verhindert, dass Template-CSS (z. B. :is(h1,h2,...) { color: ... }) in den Panel-Inhalt eindringt.
1895 */
1896class BrowserCheckPanel extends HTMLElement {
1897  constructor() {
1898    super();
1899    const shadow = this.attachShadow({ mode: 'open' });
1900
1901    // Box-sizing-Reset + no-motion-Suppressor für Shadow-Inhalte
1902    const style = document.createElement('style');
1903    style.textContent = [
1904      '* { box-sizing: border-box; }',
1905      ':host(.bc-no-motion), :host(.bc-no-motion) * {',
1906      '  transition-duration: 0s !important;',
1907      '  animation-duration: 0s !important;',
1908      '  animation-delay: 0s !important;',
1909      '}',
1910    ].join('\n');
1911    shadow.appendChild(style);
1912
1913    const fontLink = document.createElement('link');
1914    fontLink.rel = 'stylesheet';
1915    fontLink.href = BROWSER_CHECK_FONT_HREF;
1916    fontLink.setAttribute('crossorigin', 'anonymous');
1917    shadow.appendChild(fontLink);
1918
1919    const cssLink = document.createElement('link');
1920    cssLink.rel = 'stylesheet';
1921    cssLink.href = BROWSER_CHECK_CSS_HREF;
1922    shadow.appendChild(cssLink);
1923  }
1924}
1925
1926if (!customElements.get('browser-check-panel')) {
1927  customElements.define('browser-check-panel', BrowserCheckPanel);
1928}
1929
1930/**
1931 * Initialisierung
1932 */
1933function ensureFrontendEditLoaded() {
1934  if (window.__condeonAssist?.frontendEdit) return Promise.resolve();
1935  return new Promise((resolve) => {
1936    const script = document.createElement('script');
1937    script.src = '/(cms)/module/static/default/content/libs/js/nst/frontend_edit.js';
1938    script.onload = resolve;
1939    script.onerror = resolve; // auch bei Fehler nicht blockieren
1940    document.head.appendChild(script);
1941  });
1942}
1943
1944async function initBrowserCheck() {
1945  // Browser-Check defaults Frontend Edit to disabled on first run.
1946  // This must happen before frontend_edit.js is loaded.
1947  if (sessionStorage.getItem('fe_edit_enabled') === null) {
1948    sessionStorage.setItem('fe_edit_enabled', 'false');
1949  }
1950
1951  await ensureFrontendEditLoaded();
1952
1953  const panel = document.createElement('div');
1954  panel.id = 'bc-panel-container';
1955  document.body.appendChild(panel);
1956
1957  const helpBadge = document.createElement('button');
1958  helpBadge.id = 'bc-help-badge';
1959  helpBadge.type = 'button';
1960  helpBadge.setAttribute('aria-label', 'Website Hilfe öffnen');
1961  helpBadge.textContent = 'Website Hilfe';
1962  document.body.appendChild(helpBadge);
1963
1964  const check = new BrowserCheck();
1965  const ui = new BrowserCheckUI(check);
1966  check.registerChecks();
1967  await check.runAllChecks();
1968
1969  const openOverlay = async () => {
1970    // Inhalt off-screen rendern (Container liegt noch bei translateX(100%))
1971    ui.render(panel);
1972    helpBadge.classList.add('is-hidden');
1973
1974    // Warten bis Shadow-DOM-CSS geladen, damit kein FOUC beim Slide-in sichtbar ist
1975    await new Promise(resolve => {
1976      const cssLink = ui.shadowHost?.shadowRoot.querySelector('link[href*="browser-check"]');
1977      if (!cssLink || cssLink.sheet) {
1978        resolve();
1979      } else {
1980        cssLink.addEventListener('load', resolve, { once: true });
1981        setTimeout(resolve, 100); // Fallback
1982      }
1983    });
1984
1985    panel.classList.add('is-open');
1986  };
1987
1988  const closeOverlayToBadge = () => {
1989    panel.classList.remove('is-open');
1990    helpBadge.classList.remove('is-hidden');
1991  };
1992
1993  helpBadge.addEventListener('click', openOverlay);
1994  ui.onClose = closeOverlayToBadge;
1995
1996  window.__browserCheck = check;
1997  window.__browserCheckUI = ui;
1998
1999  console.log('BrowserCheck initialisiert. Report verfügbar unter window.__browserCheck');
2000}
2001
2002ensureBrowserCheckCssLoaded();
2003
2004if (document.readyState === 'loading') {
2005  document.addEventListener('DOMContentLoaded', initBrowserCheck);
2006} else {
2007  initBrowserCheck();
2008}

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.