{"id":52854,"date":"2026-04-27T12:31:37","date_gmt":"2026-04-27T10:31:37","guid":{"rendered":"https:\/\/blickpunkt-lokalsport.de\/?page_id=52854"},"modified":"2026-05-08T16:56:20","modified_gmt":"2026-05-08T14:56:20","slug":"statistikbg","status":"publish","type":"page","link":"https:\/\/blickpunkt-lokalsport.de\/english\/statistikbg\/","title":{"rendered":"Statistikbg"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"52854\" class=\"elementor elementor-52854\" data-elementor-settings=\"{&quot;element_pack_global_tooltip_width&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;size&quot;:&quot;&quot;,&quot;sizes&quot;:[]},&quot;element_pack_global_tooltip_width_tablet&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;size&quot;:&quot;&quot;,&quot;sizes&quot;:[]},&quot;element_pack_global_tooltip_width_mobile&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;size&quot;:&quot;&quot;,&quot;sizes&quot;:[]},&quot;element_pack_global_tooltip_padding&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;top&quot;:&quot;&quot;,&quot;right&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;left&quot;:&quot;&quot;,&quot;isLinked&quot;:true},&quot;element_pack_global_tooltip_padding_tablet&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;top&quot;:&quot;&quot;,&quot;right&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;left&quot;:&quot;&quot;,&quot;isLinked&quot;:true},&quot;element_pack_global_tooltip_padding_mobile&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;top&quot;:&quot;&quot;,&quot;right&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;left&quot;:&quot;&quot;,&quot;isLinked&quot;:true},&quot;element_pack_global_tooltip_border_radius&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;top&quot;:&quot;&quot;,&quot;right&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;left&quot;:&quot;&quot;,&quot;isLinked&quot;:true},&quot;element_pack_global_tooltip_border_radius_tablet&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;top&quot;:&quot;&quot;,&quot;right&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;left&quot;:&quot;&quot;,&quot;isLinked&quot;:true},&quot;element_pack_global_tooltip_border_radius_mobile&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;top&quot;:&quot;&quot;,&quot;right&quot;:&quot;&quot;,&quot;bottom&quot;:&quot;&quot;,&quot;left&quot;:&quot;&quot;,&quot;isLinked&quot;:true}}\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-e6f55eb e-flex e-con-boxed e-con e-parent\" data-id=\"e6f55eb\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-139f8e5 elementor-widget elementor-widget-html\" data-id=\"139f8e5\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!DOCTYPE html>\r\n<html lang=\"de\">\r\n<head>\r\n<meta charset=\"utf-8\">\r\n<title>Fotostatistik OWL \u2014 Blickpunkt Lokalsport<\/title>\r\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\r\n<style>\r\n:root {\r\n  --bg: #f6f4ef;\r\n  --bg-soft: #ffffff;\r\n  --card: #ffffff;\r\n  --line: #e6e2d8;\r\n  --line-soft: #f0ece2;\r\n  --text: #2a2a2a;\r\n  --text-soft: #5a5a5a;\r\n  --muted: #9a948a;\r\n  --accent: #2f7d4f;\r\n  --accent-soft: #d8eadd;\r\n  --accent-alt: #5a87bf;\r\n  --accent-alt-soft: #dde7f3;\r\n  --warn: #c97a2b;\r\n  --warn-soft: #f6e3cb;\r\n  --cold: #b85a1c;\r\n  --cold-soft: #fde4cb;\r\n  --shadow: 0 1px 2px rgba(0,0,0,.04), 0 4px 12px rgba(0,0,0,.04);\r\n}\r\n* { box-sizing: border-box; }\r\nhtml, body {\r\n  margin: 0;\r\n  padding: 0;\r\n  background: var(--bg);\r\n  color: var(--text);\r\n  font: 15px\/1.55 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;\r\n}\r\n.wrap { max-width: 1200px; margin: 0 auto; padding: 36px 22px 80px; }\r\nh1 { font-size: 30px; margin: 0 0 6px; letter-spacing: -.02em; }\r\nh2 {\r\n  font-size: 16px;\r\n  margin: 0 0 18px;\r\n  letter-spacing: .02em;\r\n  color: var(--text-soft);\r\n  text-transform: uppercase;\r\n  font-weight: 600;\r\n}\r\n.sub { color: var(--text-soft); margin-bottom: 16px; font-size: 14px; }\r\n\r\n.status {\r\n  margin: 0 0 10px;\r\n  padding: 10px 12px;\r\n  border-radius: 10px;\r\n  background: var(--bg-soft);\r\n  border: 1px solid var(--line);\r\n  color: var(--text-soft);\r\n  font-size: 13px;\r\n}\r\n.status.error {\r\n  background: var(--warn-soft);\r\n  color: var(--warn);\r\n  border-color: #edcfaa;\r\n}\r\n.status.ok {\r\n  background: var(--accent-soft);\r\n  color: var(--accent);\r\n  border-color: #bdd8c5;\r\n}\r\n.proxy-log {\r\n  margin: 0 0 28px;\r\n  color: var(--muted);\r\n  font-size: 12px;\r\n  word-break: break-word;\r\n}\r\n\r\n.kpi-grid {\r\n  display: grid;\r\n  grid-template-columns: repeat(4, 1fr);\r\n  gap: 14px;\r\n  margin-bottom: 36px;\r\n}\r\n.kpi {\r\n  background: var(--card);\r\n  border: 1px solid var(--line);\r\n  border-radius: 12px;\r\n  padding: 18px 20px;\r\n  box-shadow: var(--shadow);\r\n}\r\n.kpi .num {\r\n  font-size: 30px;\r\n  font-weight: 700;\r\n  color: var(--accent);\r\n  letter-spacing: -.02em;\r\n}\r\n.kpi .lbl {\r\n  color: var(--text-soft);\r\n  font-size: 13px;\r\n  margin-top: 4px;\r\n}\r\n\r\n.grid { display: grid; grid-template-columns: 1.5fr 1fr; gap: 22px; margin-bottom: 28px; }\r\n\r\n@media (max-width: 900px) {\r\n  .grid { grid-template-columns: 1fr; }\r\n  .kpi-grid { grid-template-columns: repeat(2, 1fr); }\r\n}\r\n@media (max-width: 560px) {\r\n  .kpi-grid { grid-template-columns: 1fr; }\r\n}\r\n\r\n.card {\r\n  background: var(--card);\r\n  border: 1px solid var(--line);\r\n  border-radius: 12px;\r\n  padding: 22px;\r\n  box-shadow: var(--shadow);\r\n}\r\n\r\n.club-card { padding: 14px 0; border-bottom: 1px solid var(--line-soft); }\r\n.club-card:last-child { border-bottom: 0; }\r\n.club-head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; flex-wrap: wrap; }\r\n.club-rank { font-size: 12px; color: var(--muted); font-weight: 600; min-width: 24px; }\r\n.club-name { font-weight: 600; flex: 1; }\r\n.club-count { font-size: 13px; color: var(--accent); font-weight: 600; }\r\n\r\n.bar-track { height: 8px; background: var(--line-soft); border-radius: 4px; overflow: hidden; }\r\n.bar-fill { height: 100%; background: var(--accent); border-radius: 4px; transition: width .4s ease; }\r\n.bar-fill.alt { background: var(--accent-alt); }\r\n\r\n.age-pills { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }\r\n.age-pill {\r\n  display: inline-flex;\r\n  align-items: center;\r\n  gap: 6px;\r\n  padding: 3px 9px;\r\n  background: var(--accent-soft);\r\n  color: var(--accent);\r\n  border-radius: 14px;\r\n  font-size: 12px;\r\n  font-weight: 500;\r\n  cursor: help;\r\n}\r\n.age-pill b { font-weight: 600; }\r\n.age-pill .pill-count {\r\n  background: rgba(255,255,255,.6);\r\n  padding: 0 6px;\r\n  border-radius: 10px;\r\n  font-size: 11px;\r\n  font-weight: 600;\r\n}\r\n.age-pill.cold {\r\n  background: var(--cold-soft);\r\n  color: var(--cold);\r\n  border: 1px dashed var(--cold);\r\n  padding: 2px 8px;\r\n}\r\n.age-pill.cold .pill-count { background: rgba(255,255,255,.7); }\r\n\r\n.cold-badge {\r\n  display: inline-block;\r\n  padding: 2px 8px;\r\n  background: var(--cold-soft);\r\n  color: var(--cold);\r\n  border-radius: 10px;\r\n  font-size: 11px;\r\n  font-weight: 600;\r\n  margin-right: 4px;\r\n}\r\n\r\n.legend {\r\n  display: flex;\r\n  gap: 16px;\r\n  flex-wrap: wrap;\r\n  margin-bottom: 14px;\r\n  font-size: 12px;\r\n  color: var(--text-soft);\r\n  align-items: center;\r\n}\r\n.legend-item { display: inline-flex; align-items: center; gap: 6px; }\r\n.legend-swatch { width: 14px; height: 14px; border-radius: 50%; display: inline-block; }\r\n.legend-swatch.warm { background: var(--accent-soft); border: 1px solid var(--accent); }\r\n.legend-swatch.cold { background: var(--cold-soft); border: 1px dashed var(--cold); }\r\n\r\n.bar-row { margin-bottom: 14px; }\r\n.bar-label {\r\n  font-size: 14px;\r\n  margin-bottom: 4px;\r\n  font-weight: 500;\r\n  display: flex;\r\n  justify-content: space-between;\r\n  align-items: baseline;\r\n  gap: 10px;\r\n}\r\n.age-num { font-size: 13px; color: var(--accent-alt); font-weight: 600; }\r\n\r\n.month-row {\r\n  display: flex;\r\n  align-items: flex-end;\r\n  gap: 8px;\r\n  height: 180px;\r\n  padding: 8px 0;\r\n  overflow-x: auto;\r\n}\r\n.month-bar {\r\n  flex: 1;\r\n  min-width: 38px;\r\n  display: flex;\r\n  flex-direction: column;\r\n  align-items: center;\r\n  justify-content: flex-end;\r\n  height: 100%;\r\n}\r\n.month-fill {\r\n  width: 70%;\r\n  max-width: 32px;\r\n  background: linear-gradient(to top, var(--accent), #6cc28d);\r\n  border-radius: 4px 4px 0 0;\r\n  min-height: 4px;\r\n  transition: height .4s ease;\r\n}\r\n.month-count { font-size: 12px; color: var(--text); font-weight: 600; margin-bottom: 4px; }\r\n.month-label { font-size: 11px; color: var(--muted); margin-top: 6px; }\r\n\r\n.search-box { display: flex; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; align-items: center; }\r\n.search-box input {\r\n  flex: 1;\r\n  min-width: 220px;\r\n  background: #fff;\r\n  border: 1px solid var(--line);\r\n  color: var(--text);\r\n  padding: 10px 14px;\r\n  border-radius: 8px;\r\n  font: inherit;\r\n}\r\n.search-box input:focus {\r\n  outline: 2px solid var(--accent);\r\n  outline-offset: -1px;\r\n  border-color: transparent;\r\n}\r\n.search-box button {\r\n  background: var(--bg-soft);\r\n  border: 1px solid var(--line);\r\n  color: var(--text-soft);\r\n  padding: 9px 16px;\r\n  border-radius: 8px;\r\n  cursor: pointer;\r\n  font: inherit;\r\n}\r\n.search-box button:hover { background: var(--line-soft); }\r\n.search-box .info { color: var(--muted); font-size: 13px; margin-left: auto; }\r\n\r\ntable { width: 100%; border-collapse: collapse; font-size: 13px; }\r\nth, td {\r\n  padding: 10px 12px;\r\n  text-align: left;\r\n  border-bottom: 1px solid var(--line-soft);\r\n  vertical-align: top;\r\n}\r\nth {\r\n  position: sticky;\r\n  top: 0;\r\n  background: #faf8f3;\r\n  color: var(--text-soft);\r\n  font-size: 11px;\r\n  text-transform: uppercase;\r\n  letter-spacing: .05em;\r\n  font-weight: 600;\r\n}\r\n.date { white-space: nowrap; font-variant-numeric: tabular-nums; color: var(--accent); width: 110px; font-weight: 600; }\r\n.clubs { font-weight: 500; }\r\n.age { width: 170px; }\r\n.summary { color: var(--muted); font-size: 12px; }\r\n.comp-pill {\r\n  display: inline-block;\r\n  padding: 2px 8px;\r\n  background: var(--warn-soft);\r\n  color: var(--warn);\r\n  border-radius: 12px;\r\n  font-size: 11px;\r\n  font-weight: 500;\r\n}\r\n.muted { color: var(--muted); }\r\n.table-wrap {\r\n  max-height: 600px;\r\n  overflow: auto;\r\n  border: 1px solid var(--line);\r\n  border-radius: 10px;\r\n  background: #fff;\r\n}\r\ntr.hidden { display: none; }\r\ntr:hover td { background: #faf8f3; }\r\n.empty-note { color: var(--muted); font-size: 13px; }\r\n<\/style>\r\n<\/head>\r\n<body>\r\n<div class=\"wrap\">\r\n  <h1>Fotostatistik OWL<\/h1>\r\n  <div class=\"sub\" id=\"subline\">Kalenderdaten werden geladen\u2026<\/div>\r\n  <div class=\"status\" id=\"status\">ICS-Kalender wird \u00fcber Proxy-Fallback geladen\u2026<\/div>\r\n  <div class=\"proxy-log\" id=\"proxyLog\">Noch kein Proxy getestet.<\/div>\r\n\r\n  <div class=\"kpi-grid\" id=\"kpis\">\r\n    <div class=\"kpi\"><div class=\"num\">\u2013<\/div><div class=\"lbl\">Foto-Termine gesamt<\/div><\/div>\r\n    <div class=\"kpi\"><div class=\"num\">\u2013<\/div><div class=\"lbl\">Vereine fotografiert<\/div><\/div>\r\n    <div class=\"kpi\"><div class=\"num\">\u2013<\/div><div class=\"lbl\">Altersklassen abgedeckt<\/div><\/div>\r\n    <div class=\"kpi\"><div class=\"num\">\u2013<\/div><div class=\"lbl\">Top-Verein<\/div><\/div>\r\n  <\/div>\r\n\r\n  <div class=\"grid\">\r\n    <div class=\"card\">\r\n      <h2>Vereine \u2014 sortiert nach Anzahl Termine, mit Altersklassen<\/h2>\r\n      <div class=\"legend\">\r\n        <span class=\"legend-item\"><span class=\"legend-swatch warm\"><\/span> aktiv (letzter Termin innerhalb 8 Wochen)<\/span>\r\n        <span class=\"legend-item\"><span class=\"legend-swatch cold\"><\/span> kalt (letzter Termin l\u00e4nger als 8 Wochen her)<\/span>\r\n        <span class=\"legend-item\" style=\"margin-left:auto\"><b id=\"coldTotal\">0<\/b>&nbsp;kalte Altersklassen insgesamt<\/span>\r\n      <\/div>\r\n      <div id=\"clubsList\"><div class=\"empty-note\">Wird geladen\u2026<\/div><\/div>\r\n    <\/div>\r\n\r\n    <div class=\"card\">\r\n      <h2>Top Altersklassen<\/h2>\r\n      <div id=\"agesList\"><div class=\"empty-note\">Wird geladen\u2026<\/div><\/div>\r\n    <\/div>\r\n  <\/div>\r\n\r\n  <div class=\"card\" style=\"margin-bottom: 28px;\">\r\n    <h2>Termine pro Monat<\/h2>\r\n    <div class=\"month-row\" id=\"monthsList\"><div class=\"empty-note\">Wird geladen\u2026<\/div><\/div>\r\n  <\/div>\r\n\r\n  <div class=\"card\">\r\n    <h2 id=\"tableHeadline\">Alle Termine \u2014 neueste zuerst<\/h2>\r\n    <div class=\"search-box\">\r\n      <input id=\"search\" type=\"text\" placeholder=\"Filtern: Verein, Altersklasse oder Datum (z.B. 2026-04, Theesen, U15)\u2026\">\r\n      <button id=\"resetBtn\" type=\"button\">Reset<\/button>\r\n      <span class=\"info\" id=\"info\">0 Termine<\/span>\r\n    <\/div>\r\n    <div class=\"table-wrap\">\r\n      <table>\r\n        <thead>\r\n          <tr><th>Datum<\/th><th>Verein(e)<\/th><th>Altersklasse<\/th><th>Originaltitel<\/th><\/tr>\r\n        <\/thead>\r\n        <tbody id=\"tbody\"><\/tbody>\r\n      <\/table>\r\n    <\/div>\r\n  <\/div>\r\n<\/div>\r\n\r\n<script>\r\nconst ICS_URL = \"https:\/\/export.kalender.digital\/ics\/4615281\/b40e8d17b4be6185fe64\/fototerminbesttigt.ics?past_months=3&future_months=36\";\r\n\r\nconst PROXIES = [\r\n  { name: \"AllOrigins Raw\", build: url => `https:\/\/api.allorigins.win\/raw?url=${encodeURIComponent(url)}` },\r\n  { name: \"Corsproxy.io\", build: url => `https:\/\/corsproxy.io\/?${encodeURIComponent(url)}` },\r\n  { name: \"Isomorphic Git\", build: url => `https:\/\/cors.isomorphic-git.org\/${url}` },\r\n  { name: \"CodeTabs\", build: url => `https:\/\/api.codetabs.com\/v1\/proxy?quest=${encodeURIComponent(url)}` },\r\n  { name: \"ThingProxy\", build: url => `https:\/\/thingproxy.freeboard.io\/fetch\/${url}` },\r\n  { name: \"Cors.sh\", build: url => `https:\/\/proxy.cors.sh\/${url}` },\r\n  { name: \"AllOrigins Get\", build: url => `https:\/\/api.allorigins.win\/get?url=${encodeURIComponent(url)}` },\r\n  { name: \"YaCDN\", build: url => `https:\/\/yacdn.org\/proxy\/${url}` }\r\n];\r\n\r\nconst COLD_WEEKS = 8;\r\nconst MONTHS_DE = [\"Jan\", \"Feb\", \"M\u00e4r\", \"Apr\", \"Mai\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Okt\", \"Nov\", \"Dez\"];\r\n\r\nconst CLUB_CANON = [\r\n  { canon: \"VfR Wellensiek\", aliases: [\"vfr wellensiek\", \"wellensiek\"] },\r\n  { canon: \"VfB Fichte Bielefeld\", aliases: [\"vfb fichte bielefeld\", \"vfb fichte\", \"fichte\"] },\r\n  { canon: \"SV Heepen\", aliases: [\"sv heepen\", \"spvg heepen\"] },\r\n  { canon: \"VfL Theesen\", aliases: [\"vfl theesen\"] },\r\n  { canon: \"TuS J\u00f6llenbeck\", aliases: [\"tus j\u00f6llenbeck\", \"jollenbeck\", \"j\u00f6llenbeck\"] },\r\n  { canon: \"TuS Quelle\", aliases: [\"tus quelle\", \"quelle\"] },\r\n  { canon: \"TuS Dornberg\", aliases: [\"tus dornberg\", \"dornberg\"] },\r\n  { canon: \"TuS Eintracht Bielefeld\", aliases: [\"tus eintracht bielefeld\", \"tus eintracht\"] },\r\n  { canon: \"SV Ubbedissen\", aliases: [\"sv ubbedissen 09\", \"sv ubbedissen\"] },\r\n  { canon: \"TuS Ost\", aliases: [\"tus ost\"] },\r\n  { canon: \"SC Halle\", aliases: [\"sc halle\"] },\r\n  { canon: \"VfL Schildesche\", aliases: [\"vfl schildesche\"] },\r\n  { canon: \"FC G\u00fctersloh\", aliases: [\"fc g\u00fctersloh\", \"fc guetersloh\", \"fc gutersloh\"] },\r\n  { canon: \"SC 04\/26 Bielefeld\", aliases: [\"sc bielefeld 04\/26\", \"sc 04\/26 bielefeld\", \"sc bielefeld\", \"scb\"] },\r\n  { canon: \"SC Peckeloh\", aliases: [\"sc peckeloh\", \"jsg peckeloh\"] },\r\n  { canon: \"Spvg. Steinhagen\", aliases: [\"spvg. steinhagen\", \"spvg steinhagen\", \"steinhagen\", \"turksportsteinhagen\", \"t\u00fcrksportsteinhagen\"] },\r\n  { canon: \"SC Verl\", aliases: [\"sc verl\"] },\r\n  { canon: \"TuS Hoberge-Uerentrup\", aliases: [\"tus hoberge-uerentrup\", \"hoberge uerentrup\"] },\r\n  { canon: \"TuS Einigkeit Hillegossen\", aliases: [\"tus hillegossen\", \"tus einigkeit hillegossen\", \"hillegossen\"] },\r\n  { canon: \"SV Spexard\", aliases: [\"sv spexard\"] },\r\n  { canon: \"VfL Oldentrup\", aliases: [\"vfl oldentrup\"] },\r\n  { canon: \"TV Friesen Milse\", aliases: [\"tv friesen milse\"] },\r\n  { canon: \"BV Werther\", aliases: [\"bv werther\", \"werther\"] },\r\n  { canon: \"TuS Langenheide\", aliases: [\"tus langenheide\"] },\r\n  { canon: \"SV Avenwedde\", aliases: [\"sv avenwedde\"] },\r\n  { canon: \"TG H\u00f6rste\", aliases: [\"tg h\u00f6rste\", \"tg hoerste\"] },\r\n  { canon: \"Arminia Bielefeld\", aliases: [\"arminia bielefeld\", \"traditionself arminia\", \"dsc arminia bielefeld\"] },\r\n  { canon: \"Borussia Dortmund\", aliases: [\"borussia dortmund\"] },\r\n  { canon: \"FC Schalke 04\", aliases: [\"fc schalke 04\"] },\r\n  { canon: \"SC Wiedenbr\u00fcck\", aliases: [\"sc wiedenbr\u00fcck\", \"sc wiedenbruck\"] },\r\n  { canon: \"TuS Senne\", aliases: [\"tus senne\", \"tus 08 senne i\", \"tus 08 senn i\"] },\r\n  { canon: \"SuK Canlar Bielefeld\", aliases: [\"suk canlar bielefeld\"] },\r\n  { canon: \"Brakel\", aliases: [\"brakel\"] },\r\n  { canon: \"Ummeln\", aliases: [\"ummeln\", \"vfl ummeln\"] },\r\n  { canon: \"H\u00f6velhofer SV\", aliases: [\"h\u00f6velhofer sv\", \"hoevelhofer sv\"] },\r\n  { canon: \"SV Heide Paderborn\", aliases: [\"sv heide paderborn\"] },\r\n  { canon: \"JSG FC L\u00fcbbecke \/ L\u00fcbbecker Land\", aliases: [\"jsg fc l\u00fcbbecke\", \"jsg fc luebbecke\", \"l\u00fcbbecker land\", \"lubbecker land\"] },\r\n  { canon: \"Nordkirchen\", aliases: [\"nordkirchen\"] },\r\n  { canon: \"Victoria Clarholz\", aliases: [\"victoria clarholz\"] },\r\n  { canon: \"Blau-Wei\u00df 98 G\u00fctersloh\", aliases: [\"blau wei\u00df 98 g\u00fctersloh\", \"blau weiss 98 g\u00fctersloh\", \"bw 98 g\u00fctersloh\", \"bw 98 gutersloh\"] },\r\n  { canon: \"B\u00fcnder SV\", aliases: [\"b\u00fcnder sv\", \"buender sv\"] },\r\n  { canon: \"SSV Bauer 07\/28\", aliases: [\"ssv bauer 07\/28\"] },\r\n  { canon: \"RW Kirchlengern\", aliases: [\"rw kirchlengern\"] },\r\n  { canon: \"Westfalia Soest\", aliases: [\"westfalia soest\"] },\r\n  { canon: \"JSG Quelle \/ Gadderbaum\", aliases: [\"jsg quelle \/ gadderbaum\", \"jsg quelle\"] },\r\n  { canon: \"VfL Sportfreunde Lotte\", aliases: [\"vfl sportfreunde lotte\"] },\r\n  { canon: \"Schlo\u00df Holte\", aliases: [\"schloss holte\", \"schlo\u00df holte\"] },\r\n  { canon: \"Lippstadt\", aliases: [\"lippstadt\"] },\r\n  { canon: \"SV Oelde\", aliases: [\"sv oelde\"] },\r\n  { canon: \"SG Wattenscheid 09\", aliases: [\"sg wattenscheid 09\"] }\r\n];\r\n\r\nconst IGNORE_SUMMARY_PATTERNS = [\r\n  \/^test\\b\/i,\r\n  \/\\btestverein\\b\/i,\r\n  \/\\btesthausen\\b\/i,\r\n  \/\\bjhv\\b\/i,\r\n  \/\\btagespraktika\\b\/i,\r\n  \/\\bvorstand\\b\/i,\r\n  \/^pokalfinale$\/i,\r\n  \/^kick for ref!?$\/i,\r\n  \/^auslosung\\b\/i\r\n];\r\n\r\nconst AGE_ORDER = {\r\n  \"U7\": 7, \"U8\": 8, \"U9\": 9, \"U10\": 10, \"U11\": 11, \"U12\": 12, \"U13\": 13, \"U14\": 14,\r\n  \"U15\": 15, \"U16\": 16, \"U17\": 17, \"U19\": 19,\r\n  \"Herren\": 100, \"Frauen\": 101, \"Turnier\": 110, \"U15 Frauen\": 111\r\n};\r\n\r\nconst state = { rows: [], total: 0 };\r\n\r\nfunction escapeHtml(v) {\r\n  return String(v ?? \"\")\r\n    .replace(\/&\/g, \"&amp;\")\r\n    .replace(\/<\/g, \"&lt;\")\r\n    .replace(\/>\/g, \"&gt;\")\r\n    .replace(\/\"\/g, \"&quot;\");\r\n}\r\n\r\nfunction normalizeText(v) {\r\n  return String(v || \"\")\r\n    .toLowerCase()\r\n    .normalize(\"NFD\").replace(\/[\\u0300-\\u036f]\/g, \"\")\r\n    .replace(\/\u00df\/g, \"ss\")\r\n    .replace(\/[()'\"]\/g, \" \")\r\n    .replace(\/[\u2013\u2014]\/g, \"-\")\r\n    .replace(\/\\s+\/g, \" \")\r\n    .trim();\r\n}\r\n\r\nfunction decodeICSValue(value) {\r\n  return String(value || \"\")\r\n    .replace(\/\\\\,\/g, \",\")\r\n    .replace(\/\\\\;\/g, \";\")\r\n    .replace(\/\\\\n\/gi, \"\\n\")\r\n    .replace(\/\\\\\\\\\/g, \"\\\\\");\r\n}\r\n\r\nfunction parseICSText(icsText) {\r\n  const unfolded = icsText.replace(\/\\r\\n[ \\t]\/g, \"\").replace(\/\\n[ \\t]\/g, \"\");\r\n  const lines = unfolded.split(\/\\r?\\n\/);\r\n  const events = [];\r\n  let event = null;\r\n\r\n  for (const line of lines) {\r\n    if (line === \"BEGIN:VEVENT\") { event = {}; continue; }\r\n    if (line === \"END:VEVENT\") { if (event) events.push(event); event = null; continue; }\r\n    if (!event) continue;\r\n\r\n    const idx = line.indexOf(\":\");\r\n    if (idx === -1) continue;\r\n\r\n    const key = line.slice(0, idx).split(\";\")[0].toUpperCase();\r\n    const value = line.slice(idx + 1);\r\n\r\n    if (!event[key]) event[key] = value;\r\n  }\r\n  return events;\r\n}\r\n\r\nfunction parseICSDate(value) {\r\n  if (!value) return null;\r\n  const pure = value.replace(\/Z$\/, \"\");\r\n\r\n  if (\/^\\d{8}$\/.test(pure)) {\r\n    return new Date(+pure.slice(0,4), +pure.slice(4,6)-1, +pure.slice(6,8));\r\n  }\r\n\r\n  if (\/^\\d{8}T\\d{6}$\/.test(pure)) {\r\n    return new Date(\r\n      +pure.slice(0,4),\r\n      +pure.slice(4,6)-1,\r\n      +pure.slice(6,8),\r\n      +pure.slice(9,11),\r\n      +pure.slice(11,13),\r\n      +pure.slice(13,15)\r\n    );\r\n  }\r\n\r\n  return null;\r\n}\r\n\r\nfunction formatDateDE(d) {\r\n  return `${String(d.getDate()).padStart(2,\"0\")}.${String(d.getMonth()+1).padStart(2,\"0\")}.${d.getFullYear()}`;\r\n}\r\n\r\nfunction formatISODate(d) {\r\n  return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,\"0\")}-${String(d.getDate()).padStart(2,\"0\")}`;\r\n}\r\n\r\nfunction canonicalClubName(raw) {\r\n  const n = normalizeText(raw)\r\n    .replace(\/\\bii+\\b\/g, \"\")\r\n    .replace(\/\\biii+\\b\/g, \"\")\r\n    .replace(\/\\b1\\b\/g, \"\")\r\n    .replace(\/\\b2\\b\/g, \"\")\r\n    .replace(\/\\s+\/g, \" \")\r\n    .trim();\r\n\r\n  for (const entry of CLUB_CANON) {\r\n    if (entry.aliases.some(a => n === normalizeText(a) || n.includes(normalizeText(a)))) {\r\n      return entry.canon;\r\n    }\r\n  }\r\n\r\n  return raw.replace(\/\\s+\/g, \" \").trim();\r\n}\r\n\r\nfunction extractAge(summary, description = \"\") {\r\n  const text = `${summary} ${description}`;\r\n\r\n  if (\/\\bU15\\s*Frauen\\b\/i.test(text) || \/\\bU15w\\b\/i.test(text)) return \"U15 Frauen\";\r\n\r\n  const ageMatch = text.match(\/\\bU\\s?(\\d{1,2})\\b\/i);\r\n  if (ageMatch) return `U${ageMatch[1]}`;\r\n\r\n  if (\/\\bHerren\\b\/i.test(text) || \/\\bDamen\\b\/i.test(text)) return \/\\bDamen\\b\/i.test(text) ? \"Frauen\" : \"Herren\";\r\n  if (\/\\bFrauen\\b\/i.test(text)) return \"Frauen\";\r\n  if (\/\\bTurnier\\b\/i.test(text) || \/\\bHallenturnier\\b\/i.test(text)) return \"Turnier\";\r\n  return \"\";\r\n}\r\n\r\nfunction extractCompetition(summary, description = \"\") {\r\n  const text = `${summary} ${description}`;\r\n  if (\/\\bPokal\\b\/i.test(text) || \/\\bWestfalenpokal\\b\/i.test(text)) return \"Pokal\";\r\n  if (\/\\bTurnier\\b\/i.test(text) || \/\\bHallenturnier\\b\/i.test(text) || \/\\bCup\\b\/i.test(text)) return \"Turnier\";\r\n  return \"\";\r\n}\r\n\r\nfunction shouldIgnoreEvent(summary) {\r\n  return IGNORE_SUMMARY_PATTERNS.some(rx => rx.test(summary.trim()));\r\n}\r\n\r\nfunction cleanSummaryForClubParsing(summary) {\r\n  return summary\r\n    .replace(\/^Vorschlag:\\s*\/i, \"\")\r\n    .replace(\/^Vorschalg:\\s*\/i, \"\")\r\n    .replace(\/^Buchung\\s*\/i, \"\")\r\n    .replace(\/^Fototermin\\s*\/i, \"\")\r\n    .replace(\/^Pokal\\s+\/i, \"\")\r\n    .replace(\/^Flutlichtspiel\\s+\/i, \"\")\r\n    .replace(\/\\b(U15 Frauen|U\\d{1,2}|Herren|Frauen|Damen|Turnier|Hallenturnier)\\b\/gi, \"\")\r\n    .replace(\/\\((?:w)\\)\/gi, \"\")\r\n    .replace(\/\\s+\/g, \" \")\r\n    .trim();\r\n}\r\n\r\nfunction extractClubs(summary) {\r\n  const cleaned = cleanSummaryForClubParsing(summary);\r\n  const norm = normalizeText(cleaned);\r\n  const hits = [];\r\n\r\n  for (const entry of CLUB_CANON) {\r\n    for (const alias of entry.aliases) {\r\n      const a = normalizeText(alias);\r\n      if (norm.includes(a)) {\r\n        if (!hits.includes(entry.canon)) hits.push(entry.canon);\r\n        break;\r\n      }\r\n    }\r\n  }\r\n\r\n  if (hits.length >= 2) return hits.slice(0, 2);\r\n\r\n  const split = cleaned.split(\/\\s*-\\s*\/).map(v => v.trim()).filter(Boolean);\r\n  if (split.length >= 2) {\r\n    const clubs = split.slice(0, 2).map(canonicalClubName).filter(Boolean);\r\n    return [...new Set(clubs)].slice(0, 2);\r\n  }\r\n\r\n  if (hits.length === 1) return hits;\r\n  return [];\r\n}\r\n\r\nfunction weeksAgo(dateObj, reference = new Date()) {\r\n  const diffMs = reference.getTime() - dateObj.getTime();\r\n  return Math.floor(diffMs \/ (1000 * 60 * 60 * 24 * 7));\r\n}\r\n\r\nfunction ageSortValue(age) {\r\n  return AGE_ORDER[age] ?? 999;\r\n}\r\n\r\nfunction monthDiff(from, to) {\r\n  return (to.getFullYear() - from.getFullYear()) * 12 + (to.getMonth() - from.getMonth());\r\n}\r\n\r\nfunction buildMonthRange(dates) {\r\n  if (!dates.length) return [];\r\n  const sorted = [...dates].sort((a, b) => a - b);\r\n  const start = new Date(sorted[0].getFullYear(), sorted[0].getMonth(), 1);\r\n  const end = new Date(sorted[sorted.length - 1].getFullYear(), sorted[sorted.length - 1].getMonth(), 1);\r\n  const total = monthDiff(start, end);\r\n  const result = [];\r\n\r\n  for (let i = 0; i <= total; i++) {\r\n    const current = new Date(start.getFullYear(), start.getMonth() + i, 1);\r\n    result.push({\r\n      key: `${current.getFullYear()}-${String(current.getMonth()+1).padStart(2,\"0\")}`,\r\n      label: `${MONTHS_DE[current.getMonth()]} ${String(current.getFullYear()).slice(2)}`,\r\n      count: 0\r\n    });\r\n  }\r\n\r\n  return result;\r\n}\r\n\r\nasync function fetchICSWithFallback(url) {\r\n  const proxyLog = document.getElementById(\"proxyLog\");\r\n  const attempts = [];\r\n\r\n  for (const proxy of PROXIES) {\r\n    const proxyUrl = proxy.build(url);\r\n    attempts.push(`Teste ${proxy.name}`);\r\n    proxyLog.textContent = attempts.join(\" \u00b7 \");\r\n\r\n    try {\r\n      const res = await fetch(proxyUrl, {\r\n        cache: \"no-store\",\r\n        headers: { \"x-requested-with\": \"fotostatistik-owl\" }\r\n      });\r\n\r\n      if (!res.ok) throw new Error(`HTTP ${res.status}`);\r\n\r\n      let text = await res.text();\r\n\r\n      if (!text || text.length < 50) throw new Error(\"Leere Antwort\");\r\n\r\n      if (text.trim().startsWith(\"{\")) {\r\n        try {\r\n          const json = JSON.parse(text);\r\n          if (json.contents && typeof json.contents === \"string\") {\r\n            text = json.contents;\r\n          }\r\n        } catch (e) {}\r\n      }\r\n\r\n      if (!text.includes(\"BEGIN:VCALENDAR\")) {\r\n        throw new Error(\"Keine g\u00fcltige ICS-Antwort\");\r\n      }\r\n\r\n      attempts.push(`OK \u00fcber ${proxy.name}`);\r\n      proxyLog.textContent = attempts.join(\" \u00b7 \");\r\n      return { text, proxyName: proxy.name, proxyUrl };\r\n    } catch (err) {\r\n      attempts.push(`${proxy.name} fehlgeschlagen`);\r\n      proxyLog.textContent = attempts.join(\" \u00b7 \");\r\n    }\r\n  }\r\n\r\n  throw new Error(\"Alle Proxys fehlgeschlagen\");\r\n}\r\n\r\nfunction buildEvents(rawEvents) {\r\n  return rawEvents.map(item => {\r\n    const summary = decodeICSValue(item.SUMMARY || \"\").trim();\r\n    const description = decodeICSValue(item.DESCRIPTION || \"\").trim();\r\n    const dateObj = parseICSDate(item.DTSTART);\r\n\r\n    if (!summary || !dateObj) return null;\r\n    if (shouldIgnoreEvent(summary)) return null;\r\n\r\n    const age = extractAge(summary, description);\r\n    const competition = extractCompetition(summary, description);\r\n    const clubs = extractClubs(summary);\r\n\r\n    return {\r\n      summary,\r\n      description,\r\n      dateObj,\r\n      dateDe: formatDateDE(dateObj),\r\n      isoDate: formatISODate(dateObj),\r\n      age,\r\n      competition,\r\n      clubs,\r\n      clubsSearch: clubs.join(\" | \").toLowerCase()\r\n    };\r\n  })\r\n  .filter(Boolean)\r\n  .sort((a, b) => b.dateObj - a.dateObj);\r\n}\r\n\r\nfunction buildStats(events) {\r\n  const now = new Date();\r\n  const clubMap = new Map();\r\n  const ageMap = new Map();\r\n  const monthMap = new Map();\r\n\r\n  const months = buildMonthRange(events.map(e => e.dateObj));\r\n  months.forEach(m => monthMap.set(m.key, m));\r\n\r\n  for (const event of events) {\r\n    if (event.age) ageMap.set(event.age, (ageMap.get(event.age) || 0) + 1);\r\n\r\n    const monthKey = `${event.dateObj.getFullYear()}-${String(event.dateObj.getMonth()+1).padStart(2,\"0\")}`;\r\n    if (monthMap.has(monthKey)) monthMap.get(monthKey).count += 1;\r\n\r\n    for (const club of event.clubs) {\r\n      if (!clubMap.has(club)) clubMap.set(club, { count: 0, ages: new Map() });\r\n      const entry = clubMap.get(club);\r\n      entry.count += 1;\r\n\r\n      if (event.age) {\r\n        if (!entry.ages.has(event.age)) entry.ages.set(event.age, { count: 0, lastDate: null });\r\n        const ageEntry = entry.ages.get(event.age);\r\n        ageEntry.count += 1;\r\n        if (!ageEntry.lastDate || event.dateObj > ageEntry.lastDate) ageEntry.lastDate = event.dateObj;\r\n      }\r\n    }\r\n  }\r\n\r\n  let totalColdAges = 0;\r\n\r\n  const clubRanking = [...clubMap.entries()].map(([name, data]) => {\r\n    const ages = [...data.ages.entries()]\r\n      .map(([age, meta]) => {\r\n        const diffWeeks = weeksAgo(meta.lastDate, now);\r\n        const isCold = diffWeeks > COLD_WEEKS;\r\n        if (isCold) totalColdAges += 1;\r\n        return {\r\n          age,\r\n          count: meta.count,\r\n          lastDate: meta.lastDate,\r\n          lastDateDe: formatDateDE(meta.lastDate),\r\n          lastDiffText: diffWeeks < 5 ? `${diffWeeks} W.` : `${Math.floor(diffWeeks \/ 4)} Mon.`,\r\n          isCold\r\n        };\r\n      })\r\n      .sort((a, b) => b.count - a.count || ageSortValue(a.age) - ageSortValue(b.age));\r\n\r\n    return {\r\n      name,\r\n      count: data.count,\r\n      ages,\r\n      coldCount: ages.filter(a => a.isCold).length\r\n    };\r\n  }).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name, \"de\"));\r\n\r\n  const ageRanking = [...ageMap.entries()]\r\n    .map(([age, count]) => ({ age, count }))\r\n    .sort((a, b) => b.count - a.count || ageSortValue(a.age) - ageSortValue(b.age));\r\n\r\n  const topClub = clubRanking[0] || null;\r\n\r\n  return {\r\n    totalAppointments: events.length,\r\n    totalClubs: clubRanking.length,\r\n    totalAges: ageRanking.length,\r\n    topClubName: topClub ? topClub.name : \"\u2013\",\r\n    topClubCount: topClub ? topClub.count : 0,\r\n    totalColdAges,\r\n    clubRanking,\r\n    ageRanking,\r\n    months: [...monthMap.values()]\r\n  };\r\n}\r\n\r\nfunction renderKpis(stats) {\r\n  document.getElementById(\"kpis\").innerHTML = `\r\n    <div class=\"kpi\"><div class=\"num\">${stats.totalAppointments}<\/div><div class=\"lbl\">Foto-Termine gesamt<\/div><\/div>\r\n    <div class=\"kpi\"><div class=\"num\">${stats.totalClubs}<\/div><div class=\"lbl\">Vereine fotografiert<\/div><\/div>\r\n    <div class=\"kpi\"><div class=\"num\">${stats.totalAges}<\/div><div class=\"lbl\">Altersklassen abgedeckt<\/div><\/div>\r\n    <div class=\"kpi\"><div class=\"num\">${stats.topClubCount}<\/div><div class=\"lbl\">Top-Verein: ${escapeHtml(stats.topClubName)}<\/div><\/div>\r\n  `;\r\n}\r\n\r\nfunction renderClubs(stats) {\r\n  const list = document.getElementById(\"clubsList\");\r\n  document.getElementById(\"coldTotal\").textContent = stats.totalColdAges;\r\n\r\n  if (!stats.clubRanking.length) {\r\n    list.innerHTML = `<div class=\"empty-note\">Keine Vereinsdaten gefunden.<\/div>`;\r\n    return;\r\n  }\r\n\r\n  const max = stats.clubRanking[0].count || 1;\r\n\r\n  list.innerHTML = stats.clubRanking.map((club, index) => {\r\n    const pills = club.ages.map(age => {\r\n      const cold = age.isCold ? \" cold\" : \"\";\r\n      const title = `Letzter Termin ${age.lastDateDe} \u00b7 vor ${age.lastDiffText}`;\r\n      return `<span class=\"age-pill${cold}\" title=\"${escapeHtml(title)}\"><b>${escapeHtml(age.age)}<\/b><span class=\"pill-count\">${age.count}<\/span><\/span>`;\r\n    }).join(\"\");\r\n\r\n    const coldBadge = club.coldCount > 0\r\n      ? `<span class=\"cold-badge\" title=\"Altersklassen, die seit \u00fcber 8 Wochen nicht fotografiert wurden\">${club.coldCount} kalt<\/span>`\r\n      : \"\";\r\n\r\n    return `\r\n      <div class=\"club-card\">\r\n        <div class=\"club-head\">\r\n          <span class=\"club-rank\">#${index + 1}<\/span>\r\n          <span class=\"club-name\">${escapeHtml(club.name)}<\/span>\r\n          ${coldBadge}\r\n          <span class=\"club-count\">${club.count} Termine<\/span>\r\n        <\/div>\r\n        <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.max(4, Math.round(club.count \/ max * 100))}%\"><\/div><\/div>\r\n        <div class=\"age-pills\">${pills}<\/div>\r\n      <\/div>\r\n    `;\r\n  }).join(\"\");\r\n}\r\n\r\nfunction renderAges(stats) {\r\n  const list = document.getElementById(\"agesList\");\r\n  if (!stats.ageRanking.length) {\r\n    list.innerHTML = `<div class=\"empty-note\">Keine Altersklassen erkannt.<\/div>`;\r\n    return;\r\n  }\r\n\r\n  const max = stats.ageRanking[0].count || 1;\r\n  list.innerHTML = stats.ageRanking.map(item => `\r\n    <div class=\"bar-row\">\r\n      <div class=\"bar-label\">${escapeHtml(item.age)} <span class=\"age-num\">${item.count}<\/span><\/div>\r\n      <div class=\"bar-track\"><div class=\"bar-fill alt\" style=\"width:${Math.max(2, Math.round(item.count \/ max * 100))}%\"><\/div><\/div>\r\n    <\/div>\r\n  `).join(\"\");\r\n}\r\n\r\nfunction renderMonths(stats) {\r\n  const list = document.getElementById(\"monthsList\");\r\n  if (!stats.months.length) {\r\n    list.innerHTML = `<div class=\"empty-note\">Keine Monatsdaten vorhanden.<\/div>`;\r\n    return;\r\n  }\r\n\r\n  const max = Math.max(...stats.months.map(m => m.count), 1);\r\n  list.innerHTML = stats.months.map(item => `\r\n    <div class=\"month-bar\">\r\n      <div class=\"month-count\">${item.count}<\/div>\r\n      <div class=\"month-fill\" style=\"height:${item.count ? Math.max(6, Math.round(item.count \/ max * 100)) : 4}%\"><\/div>\r\n      <div class=\"month-label\">${escapeHtml(item.label)}<\/div>\r\n    <\/div>\r\n  `).join(\"\");\r\n}\r\n\r\nfunction renderTable(events) {\r\n  const tbody = document.getElementById(\"tbody\");\r\n  document.getElementById(\"tableHeadline\").textContent = `Alle Termine (${events.length}) \u2014 neueste zuerst`;\r\n\r\n  tbody.innerHTML = events.map(event => {\r\n    const clubsText = event.clubs.length ? event.clubs.join(\" \u00b7 \") : `<span class=\"muted\">\u2014<\/span>`;\r\n    const agePill = event.age ? `<span class=\"age-pill\">${escapeHtml(event.age)}<\/span>` : \"\";\r\n    const compPill = event.competition ? ` <span class=\"comp-pill\">${escapeHtml(event.competition)}<\/span>` : \"\";\r\n\r\n    return `\r\n      <tr data-date=\"${escapeHtml(event.isoDate)}\" data-clubs=\"${escapeHtml(event.clubsSearch)}\" data-age=\"${escapeHtml(event.age.toLowerCase())}\">\r\n        <td class=\"date\">${escapeHtml(event.dateDe)}<\/td>\r\n        <td class=\"clubs\">${clubsText}<\/td>\r\n        <td class=\"age\">${agePill}${compPill}<\/td>\r\n        <td class=\"summary\">${escapeHtml(event.summary)}<\/td>\r\n      <\/tr>\r\n    `;\r\n  }).join(\"\");\r\n\r\n  state.rows = Array.from(document.querySelectorAll(\"#tbody tr\"));\r\n  state.total = state.rows.length;\r\n  filterRows();\r\n}\r\n\r\nfunction filterRows() {\r\n  const q = document.getElementById(\"search\").value.trim().toLowerCase();\r\n  let visible = 0;\r\n\r\n  state.rows.forEach(row => {\r\n    const haystack = (row.dataset.date + \" \" + row.dataset.clubs + \" \" + row.dataset.age + \" \" + row.textContent).toLowerCase();\r\n    const match = !q || haystack.includes(q);\r\n    row.classList.toggle(\"hidden\", !match);\r\n    if (match) visible++;\r\n  });\r\n\r\n  document.getElementById(\"info\").textContent = q ? `${visible} von ${state.total} Terminen` : `${state.total} Termine`;\r\n}\r\n\r\nasync function loadCalendar() {\r\n  const status = document.getElementById(\"status\");\r\n  const subline = document.getElementById(\"subline\");\r\n\r\n  try {\r\n    status.textContent = \"Kalender wird geladen\u2026\";\r\n\r\n    const result = await fetchICSWithFallback(ICS_URL);\r\n    const rawEvents = parseICSText(result.text);\r\n    const events = buildEvents(rawEvents);\r\n    const stats = buildStats(events);\r\n\r\n    renderKpis(stats);\r\n    renderClubs(stats);\r\n    renderAges(stats);\r\n    renderMonths(stats);\r\n    renderTable(events);\r\n\r\n    const oldest = events.length ? events[events.length - 1].dateDe : \"\u2013\";\r\n    const newest = events.length ? events[0].dateDe : \"\u2013\";\r\n    subline.textContent = `Best\u00e4tigte Foto-Termine seit ${oldest} \u00b7 Stand ${newest} \u00b7 Blickpunkt Lokalsport`;\r\n    status.textContent = `Kalender erfolgreich geladen \u00fcber ${result.proxyName} \u00b7 ${events.length} Termine verarbeitet`;\r\n    status.classList.remove(\"error\");\r\n    status.classList.add(\"ok\");\r\n  } catch (err) {\r\n    console.error(err);\r\n    status.textContent = \"Kalender konnte nicht geladen werden. Alle Proxy-Varianten sind fehlgeschlagen.\";\r\n    status.classList.remove(\"ok\");\r\n    status.classList.add(\"error\");\r\n    document.getElementById(\"clubsList\").innerHTML = `<div class=\"empty-note\">Keine Daten geladen.<\/div>`;\r\n    document.getElementById(\"agesList\").innerHTML = `<div class=\"empty-note\">Keine Daten geladen.<\/div>`;\r\n    document.getElementById(\"monthsList\").innerHTML = `<div class=\"empty-note\">Keine Daten geladen.<\/div>`;\r\n    document.getElementById(\"tbody\").innerHTML = \"\";\r\n    document.getElementById(\"info\").textContent = \"0 Termine\";\r\n  }\r\n}\r\n\r\ndocument.getElementById(\"search\").addEventListener(\"input\", filterRows);\r\ndocument.getElementById(\"resetBtn\").addEventListener(\"click\", () => {\r\n  document.getElementById(\"search\").value = \"\";\r\n  filterRows();\r\n});\r\n\r\nloadCalendar();\r\n<\/script>\r\n<\/body>\r\n<\/html>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Fotostatistik OWL \u2014 Blickpunkt Lokalsport Fotostatistik OWL Kalenderdaten werden geladen\u2026 ICS-Kalender wird \u00fcber Proxy-Fallback geladen\u2026 Noch kein Proxy getestet. \u2013Foto-Termine gesamt \u2013Vereine fotografiert \u2013Altersklassen abgedeckt \u2013Top-Verein Vereine \u2014 sortiert nach Anzahl Termine, mit Altersklassen aktiv (letzter Termin innerhalb 8 Wochen) kalt (letzter Termin l\u00e4nger als 8 Wochen her) 0&nbsp;kalte Altersklassen insgesamt Wird geladen\u2026 Top Altersklassen &#8230; <a title=\"Statistikbg\" class=\"read-more\" href=\"https:\/\/blickpunkt-lokalsport.de\/english\/statistikbg\/\" aria-label=\"Read more about Statistikbg\">Read more<\/a><\/p>","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"footnotes":""},"class_list":["post-52854","page","type-page","status-publish"],"acf":[],"_links":{"self":[{"href":"https:\/\/blickpunkt-lokalsport.de\/english\/wp-json\/wp\/v2\/pages\/52854","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blickpunkt-lokalsport.de\/english\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/blickpunkt-lokalsport.de\/english\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/blickpunkt-lokalsport.de\/english\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blickpunkt-lokalsport.de\/english\/wp-json\/wp\/v2\/comments?post=52854"}],"version-history":[{"count":10,"href":"https:\/\/blickpunkt-lokalsport.de\/english\/wp-json\/wp\/v2\/pages\/52854\/revisions"}],"predecessor-version":[{"id":52978,"href":"https:\/\/blickpunkt-lokalsport.de\/english\/wp-json\/wp\/v2\/pages\/52854\/revisions\/52978"}],"wp:attachment":[{"href":"https:\/\/blickpunkt-lokalsport.de\/english\/wp-json\/wp\/v2\/media?parent=52854"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}