Files
geograsim/App/pages/glossar.php
T
Adminator 0febe51685 Glossar-Qualität Welle 1: Angebot&Nachfrage neu (Kino-Beispiel) + Markt-Widget, DIN-Striche, Fixes
- angebot-nachfrage: kindgerechter Text (20-€-Taschengeld/Kino), kein Lehrplan-Jargon,
  neues interaktives Markt-Widget (10 Äpfel, Preis-Regler → Gleichgewicht)
- beduerfnisse, raumnutzungskonflikt: Lehrplan-Sätze raus, kindgerechter
- DIN 5008: Zahlenbereiche programmweit als "5 – 9" (71 Einträge)
- Leichte Sprache: falsche Bindestriche gefixt (Infrastruktur, Grundausstattung, …)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 02:17:54 +02:00

1160 lines
49 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Glossar — Übersichtsseite + Detailansicht.
*
* /App/pages/glossar.php → Alle Begriffe als Grid
* /App/pages/glossar.php?term=co2 → Eintrag direkt geöffnet (für Tooltip-Links aus Sims)
* /App/pages/glossar.php?module=klima → vorgefilterter Grid
*
* Datenquelle: /php/api/glossar.php
* Modul-Metadaten: DB-Tabelle module_info (Admin-pflegbar)
*/
require_once __DIR__ . '/../php/config/app.php';
require_once __DIR__ . '/../php/config/db.php';
$term = isset($_GET['term']) ? preg_replace('/[^a-z0-9_-]/', '', strtolower($_GET['term'])) : '';
$module = isset($_GET['module']) ? preg_replace('/[^a-z0-9_-]/', '', strtolower($_GET['module'])) : '';
$bp = BASE_PATH;
$api = BASE_URL . '/php/api/glossar.php';
// Modul-Metadaten aus DB (Icons + Titel; Farben bleiben Design-Code).
// Filter-Chips zeigen NUR Module mit echter Sim — Status 'aktiv' allein
// reicht nicht (z.B. 'stadt' war als aktiv markiert ohne Sim-Code, 'klima-3d'
// teilt sich Sim-Ordner mit 'klima'). Indikator: existiert App/sims/<slug>/
// als Verzeichnis. Damit bleibt der Filter automatisch in Sync mit den
// tatsächlich aufrufbaren Simulationen.
$simsRoot = realpath(__DIR__ . '/../sims');
$modDb = getDB()->query("
SELECT module_id, title, icon, sort_order
FROM module_info
WHERE status IN ('aktiv', 'beta')
ORDER BY sort_order, title
")->fetchAll(PDO::FETCH_ASSOC);
$moduleMeta = [];
$moduleOrder = [];
foreach ($modDb as $m) {
if (!is_dir($simsRoot . '/' . $m['module_id'])) continue; // kein Sim-Code → kein Filter-Chip
$moduleMeta[$m['module_id']] = [
'icon' => $m['icon'] ?: '📘',
'label' => $m['title'],
];
$moduleOrder[] = $m['module_id'];
}
// Schulbuch-Kapitel je Modul (für den 📖-Link bei aktivem Modul-Filter)
$buchLink = [];
$buchAlias = ['klima-3d' => 'klima'];
foreach (array_keys($moduleMeta) as $mid) {
$ziel = $buchAlias[$mid] ?? $mid;
if (is_file(__DIR__ . '/buch-' . $ziel . '.php')) $buchLink[$mid] = 'buch-' . $ziel;
}
?><!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Glossar geograsim.at<?= $term ? ' — ' . htmlspecialchars($term) : '' ?></title>
<meta name="description" content="Das GeoGraSim-Glossar erklärt Fachbegriffe aus Klima, Energie, Raumplanung und Geografie kinderverständlich mit konkreten Beispielen und Vergleichsgrafiken.">
<link rel="icon" type="image/svg+xml" href="<?= $bp ?>/favicon.svg">
<link rel="icon" type="image/png" href="<?= $bp ?>/favicon-96x96.png" sizes="96x96">
<link rel="stylesheet" href="<?= $bp ?>/assets/fonts/inter.css">
<link rel="stylesheet" href="<?= $bp ?>/assets/css/design-system.css">
<style>
/* Override body overflow — Glossar is scrollable */
body { overflow: auto; height: auto; min-height: 100dvh; background: var(--ggs-bg); }
/* ===== Quick-Strip (Mindmap-Link, früher in der Topbar) ===== */
.gl-quick-strip {
max-width: 1100px; margin: .8rem auto 0; padding: 0 1.2rem;
text-align: right;
}
.gl-quick-strip .gl-btn-sm { display: inline-flex; }
/* ===== Legacy-Topbar (nicht mehr benutzt — Klassen behalten falls noch referenziert) ===== */
.gl-topbar {
background: var(--ggs-white);
padding: .55rem 1.2rem;
display: flex;
align-items: center;
gap: .8rem;
border-bottom: 1px solid var(--ggs-border);
position: sticky; top: 0; z-index: 50;
}
.gl-topbar a { text-decoration: none; }
.gl-topbar-brand { display: flex; align-items: center; gap: .5rem; }
.gl-topbar-brand img.gl-text { height: 40px; display: block; }
.gl-topbar .spacer { flex: 1; }
.gl-btn-sm {
display: inline-flex; align-items: center; gap: .3rem;
min-height: 40px; padding: .4rem .8rem;
border: 1px solid var(--ggs-border);
border-radius: var(--ggs-radius-sm); background: var(--ggs-white);
font-size: .78rem; font-weight: 600; cursor: pointer;
color: var(--ggs-text); font-family: inherit;
text-decoration: none;
}
.gl-btn-sm.gl-btn-primary { background: var(--ggs-fjord); color: var(--ggs-white); border-color: var(--ggs-fjord); }
@media (hover: hover) {
.gl-btn-sm:hover { background: var(--ggs-fjord-light); color: var(--ggs-fjord-dark); border-color: var(--ggs-fjord); }
.gl-btn-sm.gl-btn-primary:hover { background: var(--ggs-fjord-dark); }
}
/* ===== Hero ===== */
.gl-hero {
background: linear-gradient(135deg, var(--ggs-fjord-dark) 0%, var(--ggs-fjord) 100%);
color: var(--ggs-white);
padding: 3rem 1.5rem 3.5rem;
text-align: center;
}
.gl-hero h1 { font-size: clamp(1.8rem, 4vw, 2.6rem); font-weight: 900; letter-spacing: -.03em; margin-bottom: .5rem; }
.gl-hero p { font-size: 1.02rem; color: rgba(255,255,255,.88); max-width: 640px; margin: 0 auto 1.8rem; }
.gl-hero-stats { display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap; margin-top: 1.2rem; }
.gl-hero-stat { text-align: center; }
.gl-hero-stat-num { font-size: 1.8rem; font-weight: 900; display: block; }
.gl-hero-stat-lbl { font-size: .72rem; opacity: .8; text-transform: uppercase; letter-spacing: .08em; }
/* ===== Controls (Search + Filter) ===== */
.gl-controls { max-width: 1100px; margin: -1.8rem auto 1.5rem; padding: 0 1.2rem; position: relative; z-index: 5; }
.gl-search-wrap {
background: var(--ggs-white); border-radius: var(--ggs-radius-lg);
box-shadow: var(--ggs-shadow-lg); padding: 1rem 1.2rem;
display: flex; align-items: center; gap: .8rem; border: 1px solid var(--ggs-border);
}
.gl-search-icon { font-size: 1.2rem; opacity: .55; }
.gl-search-input {
flex: 1; border: 0; outline: 0; font-size: 1rem; font-family: inherit;
color: var(--ggs-text); background: transparent; padding: .4rem 0;
}
.gl-search-input::placeholder { color: var(--ggs-text-light); }
.gl-search-clear {
background: transparent; border: 0; cursor: pointer; color: var(--ggs-text-muted);
font-size: 1.3rem; padding: 0 .4rem; display: none;
}
.gl-search-clear.show { display: inline-block; }
.gl-filters { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem; justify-content: center; }
.gl-chip {
display: inline-flex; align-items: center; gap: .3rem;
min-height: 38px; padding: .45rem 1rem;
border-radius: var(--ggs-radius-pill);
background: var(--ggs-white); border: 1.5px solid var(--ggs-border);
font-size: .82rem; font-weight: 600; color: var(--ggs-text-muted);
cursor: pointer; transition: all .2s var(--ggs-ease); font-family: inherit;
white-space: nowrap;
}
.gl-chip.active { background: var(--ggs-fjord); color: var(--ggs-white); border-color: var(--ggs-fjord); }
a.gl-chip.gl-buch { background: var(--ggs-forest, #1f4b37); color: var(--ggs-white); border-color: var(--ggs-forest, #1f4b37); text-decoration: none; }
@media (hover: hover) {
.gl-chip:hover { border-color: var(--ggs-fjord); color: var(--ggs-fjord); }
}
.gl-chip-count { font-size: .65rem; opacity: .65; font-weight: 500; }
/* ===== Grid ===== */
.gl-grid-wrap { max-width: 1100px; margin: 0 auto 3rem; padding: 0 1.2rem; scroll-behavior: smooth; }
html { scroll-behavior: smooth; }
.gl-grid-info { font-size: .8rem; color: var(--ggs-text-muted); margin-bottom: .8rem; padding: 0 .3rem; }
/* Buchstaben-Sprungnavigation (A-Z-Leiste) */
.gl-letter-nav {
display: flex; flex-wrap: wrap; gap: .2rem;
background: var(--ggs-white);
border: 1px solid var(--ggs-border);
border-radius: var(--ggs-radius-md);
padding: .5rem .6rem;
margin: .3rem 0 1rem;
position: sticky; top: 52px; z-index: 40;
box-shadow: var(--ggs-shadow-sm);
}
.gl-letter-nav a, .gl-letter-nav span {
display: inline-flex; align-items: center; justify-content: center;
min-width: 28px; height: 28px; padding: 0 .35rem;
font-size: .82rem; font-weight: 700;
border-radius: var(--ggs-radius-sm);
text-decoration: none; user-select: none;
}
.gl-letter-nav a {
color: var(--ggs-fjord-dark); background: var(--ggs-fjord-light);
transition: all .15s var(--ggs-ease);
}
.gl-letter-nav a, .gl-letter-nav span {
min-width: 36px; height: 36px;
}
@media (hover: hover) {
.gl-letter-nav a:hover { background: var(--ggs-fjord); color: var(--ggs-white); }
}
.gl-letter-nav span { color: var(--ggs-text-light); opacity: .45; cursor: default; }
/* Buchstabenbanner (Warning-Style: schmal, volle Breite, Buchstabe links) */
.gl-letter-banner {
grid-column: 1 / -1;
background: var(--ggs-sand);
color: var(--ggs-fjord-dark);
border-left: 5px solid var(--ggs-sand-dark);
border-radius: var(--ggs-radius-sm);
padding: .5rem 1rem;
font-size: 1.15rem; font-weight: 900;
letter-spacing: -.01em;
display: flex; align-items: baseline; gap: .6rem;
scroll-margin-top: 100px;
margin-top: .4rem;
}
.gl-letter-banner:first-child { margin-top: 0; }
.gl-letter-banner-count {
font-size: .72rem; font-weight: 600; color: var(--ggs-text-muted);
letter-spacing: 0;
}
.gl-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1rem;
}
.gl-card {
background: var(--ggs-white); border: 1px solid var(--ggs-border);
border-radius: var(--ggs-radius-md); overflow: hidden; cursor: pointer;
transition: all .25s var(--ggs-ease); text-align: left;
font-family: inherit; padding: 0; display: flex; flex-direction: column;
}
@media (hover: hover) {
.gl-card:hover { transform: translateY(-3px); border-color: var(--ggs-fjord); box-shadow: var(--ggs-shadow-md); }
}
.gl-card:active { transform: scale(.98); border-color: var(--ggs-fjord); }
.gl-card-img {
height: 110px; background: var(--ggs-fjord-light);
display: flex; align-items: center; justify-content: center;
overflow: hidden; position: relative;
}
.gl-card-img img, .gl-card-img svg { max-width: 100%; max-height: 100%; object-fit: contain; }
.gl-card-img.gl-no-img {
background: linear-gradient(135deg, var(--ggs-fjord-light), var(--ggs-moss-light));
}
.gl-card-img-emoji { font-size: 3rem; opacity: .45; }
.gl-card-body { padding: .9rem 1rem 1rem; flex: 1; display: flex; flex-direction: column; }
.gl-card-category {
font-size: .62rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
color: var(--ggs-fjord); margin-bottom: .2rem;
}
.gl-card-title { font-size: 1rem; font-weight: 800; color: var(--ggs-fjord-dark); margin-bottom: .3rem; line-height: 1.25; }
.gl-card-short { font-size: .82rem; color: var(--ggs-text); line-height: 1.45; flex: 1; }
.gl-card-modules { display: flex; gap: .25rem; margin-top: .6rem; }
.gl-card-mod { font-size: .85rem; }
/* Empty / loading */
.gl-empty { text-align: center; padding: 3rem 1rem; color: var(--ggs-text-muted); }
.gl-empty strong { display: block; font-size: 1.1rem; color: var(--ggs-text); margin-bottom: .3rem; }
.gl-loading {
text-align: center; padding: 3rem 1rem; color: var(--ggs-text-muted);
font-size: .9rem;
}
/* ===== Detail Modal ===== */
.gl-modal {
position: fixed; inset: 0; background: rgba(31, 78, 90, .78); backdrop-filter: blur(6px);
z-index: 100; display: none; align-items: flex-start; justify-content: center;
overflow-y: auto; padding: 2rem 1rem;
}
.gl-modal.open { display: flex; }
.gl-modal-card {
background: var(--ggs-white); border-radius: var(--ggs-radius-lg);
max-width: 780px; width: 100%; padding: 1.8rem 2rem 2rem;
position: relative; box-shadow: 0 20px 60px rgba(0,0,0,.3);
margin: auto;
}
.gl-modal-close {
position: absolute; top: 14px; right: 14px;
width: 34px; height: 34px; border-radius: 50%;
background: var(--ggs-bg); border: 0; cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 1.3rem; color: var(--ggs-text-muted);
transition: all .2s var(--ggs-ease);
}
.gl-modal-close:hover { background: var(--ggs-coral-light); color: var(--ggs-coral); }
.gl-detail-category {
display: inline-block; font-size: .68rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .08em; color: var(--ggs-fjord);
background: var(--ggs-fjord-light); padding: 3px 10px;
border-radius: var(--ggs-radius-pill); margin-bottom: .4rem;
}
.gl-detail-title { font-size: 1.7rem; font-weight: 900; color: var(--ggs-fjord-dark); margin-bottom: .3rem; letter-spacing: -.02em; }
.gl-detail-short { font-size: 1.02rem; color: var(--ggs-text); font-style: italic; margin-bottom: 1rem; line-height: 1.5; }
.gl-detail-img {
background: var(--ggs-bg);
border-radius: var(--ggs-radius-md);
margin: 1rem 0 1.2rem; padding: 1rem;
text-align: center; border: 1px solid var(--ggs-border);
cursor: zoom-in; position: relative;
transition: border-color .2s var(--ggs-ease);
}
.gl-detail-img:hover { border-color: var(--ggs-fjord); }
.gl-detail-img img, .gl-detail-img svg { max-width: 100%; height: auto; }
.gl-detail-img::after {
content: '⤢ Vollbild';
position: absolute; bottom: 6px; right: 10px;
font-size: .7rem; font-weight: 700; color: var(--ggs-text-muted);
background: var(--ggs-white); padding: 2px 8px;
border-radius: var(--ggs-radius-pill);
opacity: .75; pointer-events: none;
}
/* Toggle Leichte Sprache */
.gl-easy-toggle {
display: inline-flex; background: var(--ggs-bg);
border-radius: var(--ggs-radius-pill); padding: 3px;
gap: 2px; margin-bottom: 1rem;
border: 1px solid var(--ggs-border);
}
.gl-easy-toggle button {
display: inline-flex; align-items: center; gap: .3rem;
min-height: 32px; padding: .25rem .75rem;
border: 0; background: transparent; font-family: inherit;
font-size: .78rem; font-weight: 600; color: var(--ggs-text-muted);
cursor: pointer; border-radius: var(--ggs-radius-pill);
transition: all .15s var(--ggs-ease);
}
.gl-easy-toggle button.active {
background: var(--ggs-fjord); color: var(--ggs-white);
box-shadow: 0 1px 3px rgba(0,0,0,.12);
}
.gl-easy-toggle button:not(.active):hover { color: var(--ggs-fjord-dark); }
/* Lightbox für Infografiken */
.gl-lightbox {
position: fixed; inset: 0; z-index: 200;
background: rgba(15, 34, 48, .94);
display: none; align-items: center; justify-content: center;
padding: 40px 2vw 60px; cursor: zoom-out;
animation: gl-fade-in .2s var(--ggs-ease);
overflow: auto;
}
.gl-lightbox.open { display: flex; }
.gl-lightbox-frame {
/* KONKRETE Breite — sonst kollabiert der Flex-Item auf Padding, */
/* wenn die SVG-intrinsische Größe 0 ist (Safari / iPad-Bug). */
width: min(1200px, 96vw);
background: var(--ggs-bg);
border-radius: var(--ggs-radius-md);
box-shadow: 0 20px 80px rgba(0,0,0,.5);
padding: 20px;
box-sizing: border-box;
display: block;
}
.gl-lightbox-frame img {
display: block;
width: 100%;
height: auto;
max-height: calc(100dvh - 140px);
}
.gl-lightbox-hint {
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
color: var(--ggs-white); font-size: .8rem; opacity: .75;
background: rgba(0,0,0,.4); padding: 6px 14px; border-radius: var(--ggs-radius-pill);
pointer-events: none;
}
@keyframes gl-fade-in { from { opacity: 0 } to { opacity: 1 } }
.gl-detail-text { font-size: .95rem; line-height: 1.7; color: var(--ggs-text); margin-bottom: 1rem; }
.gl-detail-text p { margin-bottom: .9rem; }
.gl-detail-text p:last-child { margin-bottom: 0; }
.gl-inline-link {
color: var(--ggs-fjord); text-decoration: none; font-weight: 600;
border-bottom: 1px dashed var(--ggs-fjord);
}
@media (hover: hover) {
.gl-inline-link:hover { color: var(--ggs-fjord-dark); border-bottom-style: solid; background: var(--ggs-fjord-light); }
}
.gl-inline-ext { color: var(--ggs-fjord); text-decoration: underline; }
.gl-ref a {
font-size: .72em; font-weight: 700; color: var(--ggs-coral);
text-decoration: none; padding: 0 2px;
}
.gl-ref a:hover { color: var(--ggs-fjord-dark); }
.gl-sources {
padding-left: 1.5em; margin-bottom: 1rem;
font-size: .82rem; line-height: 1.6; color: var(--ggs-text);
}
.gl-sources li { margin-bottom: .3rem; }
.gl-sources li::marker { color: var(--ggs-coral); font-weight: 700; }
.gl-sources a { color: var(--ggs-fjord-dark); text-decoration: none; border-bottom: 1px dotted var(--ggs-fjord); }
.gl-sources a:hover { color: var(--ggs-fjord); border-bottom-style: solid; }
.gl-source-date { font-size: .85em; color: var(--ggs-text-muted); }
.gl-source-hi { background: var(--ggs-sand); transition: background .4s; border-radius: var(--ggs-radius-sm); padding: 2px 4px; }
/* ===== Interaktive Experimente ===== */
.gl-experiment {
margin: 1.4rem 0;
padding: 1rem 1.2rem;
background: linear-gradient(180deg, var(--ggs-fjord-light), var(--ggs-white));
border: 1px solid var(--ggs-border);
border-radius: var(--ggs-radius-md);
}
.ge-wrap { display: flex; flex-direction: column; gap: .8rem; }
.ge-head h3 { font-size: 1rem; font-weight: 800; color: var(--ggs-fjord-dark); margin-bottom: .2rem; }
.ge-head p { font-size: .85rem; color: var(--ggs-text); line-height: 1.5; }
.ge-track, .ge-albedo-svg { width: 100%; height: auto; background: var(--ggs-white); border-radius: var(--ggs-radius-sm); padding: .4rem; }
.ge-controls { display: flex; gap: .5rem; flex-wrap: wrap; }
.ge-btn {
display: inline-flex; align-items: center; gap: .3rem;
min-height: 40px; padding: .4rem 1rem;
border: 1.5px solid var(--ggs-border);
border-radius: var(--ggs-radius-sm);
background: var(--ggs-white); color: var(--ggs-text);
font-family: inherit; font-size: .82rem; font-weight: 600;
cursor: pointer; text-align: left;
}
.ge-btn-primary { background: var(--ggs-fjord); color: var(--ggs-white); border-color: var(--ggs-fjord); }
@media (hover: hover) {
.ge-btn:not(.ge-btn-primary):hover { border-color: var(--ggs-fjord); color: var(--ggs-fjord); }
.ge-btn-primary:hover { background: var(--ggs-fjord-dark); }
}
.ge-btn:active { transform: scale(.97); }
.ge-legend { list-style: none; padding: 0; font-size: .82rem; line-height: 1.6; }
.ge-legend li { display: flex; align-items: center; gap: .5rem; margin-bottom: .2rem; }
.ge-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
.ge-note { font-size: .75rem; color: var(--ggs-text-muted); line-height: 1.5; font-style: italic; }
/* ppm-Raster */
.ge-ppm-grid {
display: grid;
gap: 0;
background: var(--ggs-bg);
padding: 4px;
border-radius: var(--ggs-radius-sm);
border: 1px solid var(--ggs-border);
cursor: crosshair;
}
.ge-ppm-grid > span {
aspect-ratio: 1;
background: var(--ggs-fjord-light);
}
.ge-ppm-grid > span.ge-ppm-special {
background: var(--ggs-coral);
border-radius: 50%;
position: relative; z-index: 1;
}
.ge-ppm-grid > span.ge-ppm-flash { animation: ge-flash 1.8s ease-out; }
@keyframes ge-flash {
0% { box-shadow: 0 0 0 0 rgba(200,92,74,.95); }
60% { box-shadow: 0 0 0 24px rgba(200,92,74,0); transform: scale(3); }
100% { transform: scale(1); }
}
.ge-ppm-grid > span.ge-ppm-hit {
position: relative; z-index: 10;
animation: ge-ppm-bounce 1.1s cubic-bezier(.22, 1, .36, 1) both;
}
@keyframes ge-ppm-bounce {
0% { transform: scale(1) translateY(0); }
25% { transform: scale(5) translateY(-10px); }
45% { transform: scale(4) translateY(0); }
60% { transform: scale(4.5) translateY(-6px); }
80% { transform: scale(4) translateY(0); }
100% { transform: scale(4) translateY(-2px); }
}
.ge-ppm-msg {
font-size: .9rem; font-weight: 700; color: var(--ggs-moss-dark);
padding: .4rem .6rem; min-height: 22px;
opacity: 0; transition: opacity .25s var(--ggs-ease);
}
.ge-ppm-msg.show { opacity: 1; }
.ge-ppm-msg.miss { color: var(--ggs-text-muted); font-weight: 600; }
/* Albedo */
.ge-albedo-choice { display: flex; gap: .4rem; flex-wrap: wrap; }
.ge-albedo-stats {
display: flex; gap: 1.2rem; flex-wrap: wrap;
font-size: .88rem;
background: var(--ggs-bg); padding: .6rem .9rem;
border-radius: var(--ggs-radius-sm);
}
.ge-albedo-stats strong { color: var(--ggs-fjord-dark); }
/* Fußabdruck-Rechner */
.ge-calc { display: flex; flex-direction: column; gap: .7rem; }
.ge-calc-row { display: flex; flex-direction: column; gap: .3rem; }
.ge-calc-q { font-size: .85rem; font-weight: 700; color: var(--ggs-fjord-dark); }
.ge-calc-opts { display: flex; gap: .35rem; flex-wrap: wrap; }
.ge-calc-opts .ge-btn { flex: 1 1 30%; font-size: .78rem; min-height: 44px; padding: .4rem .6rem; justify-content: center; text-align: center; }
.ge-calc-result {
background: var(--ggs-white);
border: 1.5px solid var(--ggs-fjord);
border-radius: var(--ggs-radius-sm);
padding: .8rem 1rem;
text-align: center;
}
.ge-calc-result > div:first-child { font-size: .82rem; color: var(--ggs-text-muted); }
.ge-calc-total { font-size: 1.4rem; font-weight: 900; color: var(--ggs-fjord-dark); display: block; margin: .2rem 0 .5rem; }
.ge-calc-bar {
height: 10px; background: var(--ggs-bg); border-radius: var(--ggs-radius-pill);
overflow: hidden; margin-bottom: .3rem;
}
.ge-calc-fill {
height: 100%; background: linear-gradient(90deg, var(--ggs-moss), var(--ggs-orange), var(--ggs-coral));
transition: width .4s var(--ggs-ease);
}
.ge-calc-scale { display: flex; justify-content: space-between; font-size: .65rem; color: var(--ggs-text-muted); margin-bottom: .3rem; }
.ge-calc-label { font-size: .75rem; color: var(--ggs-text); }
/* Slider-basierte Experimente (Treibhauseffekt, Windrad, Methan-Waage) */
.ge-gh-svg, .ge-wind-svg, .ge-bal-svg {
width: 100%; height: auto;
background: linear-gradient(180deg, #dae8ec 0%, #eff5f6 60%, var(--ggs-white) 100%);
border-radius: var(--ggs-radius-sm);
}
.ge-bal-svg { background: var(--ggs-white); }
.ge-gh-slider {
display: flex; flex-direction: column; gap: .3rem;
background: var(--ggs-white);
padding: .6rem .9rem;
border-radius: var(--ggs-radius-sm);
border: 1px solid var(--ggs-border);
}
.ge-gh-slider label {
font-size: .85rem; color: var(--ggs-text);
}
.ge-gh-slider label strong { color: var(--ggs-fjord-dark); font-size: 1rem; }
.ge-gh-slider input[type="range"] {
width: 100%; accent-color: var(--ggs-fjord); height: 28px;
}
.ge-gh-markers {
display: flex; justify-content: space-between;
font-size: .65rem; color: var(--ggs-text-muted); text-align: center;
line-height: 1.1;
}
.ge-gh-result { display: flex; gap: .5rem; flex-wrap: wrap; }
.ge-gh-pill {
background: var(--ggs-white);
border: 1.5px solid var(--ggs-border);
border-radius: var(--ggs-radius-pill);
padding: .4rem .9rem;
font-size: .82rem;
font-weight: 600;
color: var(--ggs-text);
}
.ge-gh-pill strong { color: var(--ggs-fjord-dark); }
.ge-gh-pill[data-state="good"] { border-color: var(--ggs-moss); color: var(--ggs-moss-dark); background: var(--ggs-moss-light); }
.ge-gh-pill[data-state="now"] { border-color: var(--ggs-fjord); color: var(--ggs-fjord-dark); background: var(--ggs-fjord-light); }
.ge-gh-pill[data-state="warn"] { border-color: var(--ggs-orange); color: var(--ggs-orange); background: #fbe8d3; }
.ge-gh-pill[data-state="bad"] { border-color: var(--ggs-coral); color: var(--ggs-coral); background: var(--ggs-coral-light); }
.ge-wind-obj { opacity: .45; transition: opacity .25s var(--ggs-ease), filter .25s var(--ggs-ease); }
.ge-wind-obj.ge-wind-active { opacity: 1; filter: drop-shadow(0 2px 6px rgba(74,124,138,.35)); }
.gl-detail-unit {
display: inline-block; font-size: .78rem; font-weight: 700; font-family: var(--ggs-font-mono);
color: var(--ggs-fjord-dark); background: var(--ggs-fjord-light);
padding: 4px 10px; border-radius: var(--ggs-radius-sm); margin-bottom: 1rem;
}
.gl-section-title {
font-size: .72rem; font-weight: 800; text-transform: uppercase; letter-spacing: .08em;
color: var(--ggs-text-muted); margin: 1.3rem 0 .6rem;
}
.gl-examples { list-style: none; padding: 0; }
.gl-examples li {
background: var(--ggs-bg); border-left: 3px solid var(--ggs-moss);
padding: .55rem .9rem; border-radius: 0 var(--ggs-radius-sm) var(--ggs-radius-sm) 0;
margin-bottom: .35rem; font-size: .88rem;
}
.gl-links, .gl-related { display: flex; flex-wrap: wrap; gap: .5rem; }
.gl-link, .gl-related a {
display: inline-flex; align-items: center; gap: .3rem;
background: var(--ggs-white); border: 1.5px solid var(--ggs-border);
border-radius: var(--ggs-radius-sm); padding: .4rem .8rem;
font-size: .8rem; color: var(--ggs-fjord-dark); text-decoration: none; font-weight: 600;
}
.gl-link:hover, .gl-related a:hover { border-color: var(--ggs-fjord); background: var(--ggs-fjord-light); color: var(--ggs-fjord); }
.gl-modules-row { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .2rem; margin-bottom: .6rem; }
.gl-modules-row .gl-chip { cursor: default; font-size: .7rem; padding: .25rem .6rem; }
/* ===== Footer ===== */
.gl-footer {
text-align: center; padding: 2rem 1rem 2.5rem;
font-size: .72rem; color: var(--ggs-text-muted);
border-top: 1px solid var(--ggs-border);
background: var(--ggs-white);
}
.gl-footer img { height: 36px; opacity: .5; margin-bottom: .5rem; }
.gl-footer a { color: var(--ggs-fjord); text-decoration: none; }
/* Responsive */
@media (max-width: 640px) {
.gl-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: .6rem; }
.gl-hero { padding: 2rem 1rem 2.5rem; }
.gl-hero-stats { gap: 1.2rem; }
.gl-modal-card { padding: 1.3rem 1.2rem 1.5rem; }
.gl-detail-title { font-size: 1.3rem; }
}
</style>
</head>
<body>
<?php
/* Standard-Hauptnav + Kontext-Sub-Nav (N.5.1 — Glossar zeigt keinen Country-Picker,
weil das Glossar selbst country-agnostisch ist; nur Lehrplan braucht den Picker) */
$activeMain = null; /* Glossar ist nicht in der Hauptnav als Tab */
$activeSub = 'glossar';
$showCountryPicker = false;
include __DIR__ . '/_partials/public_topnav.php';
include __DIR__ . '/_partials/erkundungs_subnav.php';
?>
<!-- Mindmap-Quick-Link bleibt erhalten, jetzt als kleiner Strip rechts oben -->
<div class="gl-quick-strip">
<a href="<?= $bp ?>/pages/mindmap.php" class="gl-btn-sm">🧠 Mindmap der Begriffe</a>
</div>
<section class="gl-hero">
<h1>Das GeoGraSim-Glossar</h1>
<p>Fachbegriffe aus Klima, Energie, Raumplanung und Geografie — kinderverständlich erklärt, mit konkreten Zahlen und Vergleichen, die Größenordnungen fassbar machen.</p>
<div class="gl-hero-stats">
<div class="gl-hero-stat"><span class="gl-hero-stat-num" id="gl-stat-total"></span><span class="gl-hero-stat-lbl">Begriffe</span></div>
<div class="gl-hero-stat"><span class="gl-hero-stat-num" id="gl-stat-images"></span><span class="gl-hero-stat-lbl">Grafiken</span></div>
<div class="gl-hero-stat"><span class="gl-hero-stat-num">4</span><span class="gl-hero-stat-lbl">Simulationen</span></div>
</div>
</section>
<div class="gl-controls">
<div class="gl-search-wrap">
<span class="gl-search-icon">🔍</span>
<input id="gl-search" class="gl-search-input" type="search" autocomplete="off"
placeholder="Begriff suchen — z.B. Treibhaus, ppm, Photovoltaik …">
<button id="gl-search-clear" class="gl-search-clear" aria-label="Suche löschen">×</button>
</div>
<div id="gl-filters" class="gl-filters"></div>
</div>
<div class="gl-grid-wrap">
<div id="gl-grid-info" class="gl-grid-info"></div>
<nav id="gl-letter-nav" class="gl-letter-nav" aria-label="Sprung zu Buchstabe"></nav>
<div id="gl-grid" class="gl-grid"></div>
<div id="gl-loading" class="gl-loading">Glossar wird geladen …</div>
</div>
<div id="gl-modal" class="gl-modal" role="dialog" aria-modal="true" aria-labelledby="gl-detail-title">
<div class="gl-modal-card">
<button id="gl-modal-close" class="gl-modal-close" aria-label="Schließen">×</button>
<div id="gl-modal-content">
<div class="gl-loading">Lädt …</div>
</div>
</div>
</div>
<div id="gl-lightbox" class="gl-lightbox" role="dialog" aria-modal="true" aria-label="Grafik Vollbildansicht">
<div class="gl-lightbox-frame">
<img id="gl-lightbox-img" src="" alt="">
</div>
<div class="gl-lightbox-hint">Tippen oder Esc drücken, um zu schließen</div>
</div>
<script src="<?= $bp ?>/assets/js/glossar-experiments.js?v=<?= @filemtime(__DIR__ . '/../assets/js/glossar-experiments.js') ?: time() ?>"></script>
<script src="<?= $bp ?>/assets/js/glossar-experiments-natur.js?v=<?= @filemtime(__DIR__ . '/../assets/js/glossar-experiments-natur.js') ?: time() ?>"></script>
<script src="<?= $bp ?>/assets/js/glossar-experiments-wirtschaft.js?v=<?= @filemtime(__DIR__ . '/../assets/js/glossar-experiments-wirtschaft.js') ?: time() ?>"></script>
<script src="<?= $bp ?>/assets/js/glossar-experiments-markt.js?v=<?= @filemtime(__DIR__ . "/../assets/js/glossar-experiments-markt.js") ?: time() ?>"></script>
<footer class="gl-footer">
<img src="<?= $bp ?>/assets/img/bildLogo.png" alt="">
<div>GeoGraSim · <a href="<?= $bp ?>/ueber.html">Über</a> · <a href="<?= $bp ?>/handbuch.html">Handbuch</a> · <a href="<?= $bp ?>/impressum.html">Impressum</a> · <a href="<?= $bp ?>/datenschutz.html">Datenschutz</a></div>
</footer>
<script>
const API = <?= json_encode($api) ?>;
const BP = <?= json_encode($bp) ?>;
const INIT_TERM = <?= json_encode($term) ?>;
const INIT_MODULE = <?= json_encode($module) ?>;
// Aus DB-Tabelle module_info (Admin-pflegbar, Source-of-Truth)
const MODULE_META = <?= json_encode($moduleMeta, JSON_UNESCAPED_UNICODE) ?>;
const MODULE_ORDER = <?= json_encode($moduleOrder, JSON_UNESCAPED_UNICODE) ?>;
const BUCH_LINK = <?= json_encode($buchLink, JSON_UNESCAPED_UNICODE) ?>; // Modul → Schulbuch-Kapitel
const CATEGORY_LABEL = {
klima: 'Klima',
einheit: 'Einheit',
energie: 'Energie',
stadt: 'Stadt',
wasser: 'Wasser',
geologie: 'Geologie',
wirtschaft: 'Wirtschaft',
biologie: 'Biologie',
astronomie: 'Astronomie',
gesellschaft: 'Gesellschaft'
};
let STATE = {
all: [],
filter: { module: '', category: '', search: '' },
easy: localStorage.getItem('ggs-glossar-easy') === '1',
currentKey: null
};
function esc(s) {
return String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function fmtImg(path) {
if (!path) return null;
if (/^https?:/.test(path)) return path;
return BP + '/' + path.replace(/^\//, '');
}
// Thumbnail-Pfad ableiten: assets/img/glossar/<slug>.png
// → assets/img/glossar/thumbs/<slug>.webp
// SVGs bleiben unverändert (sind sowieso klein). Wenn der Thumb fehlt,
// fällt das <img> per onerror auf das Original-Bild zurück.
function fmtThumb(path) {
if (!path) return null;
if (/^https?:/.test(path)) return path;
if (/\.svg$/i.test(path)) return BP + '/' + path.replace(/^\//, '');
const m = path.match(/^(.*\/)([^\/]+)\.(png|jpe?g|webp)$/i);
if (!m) return BP + '/' + path.replace(/^\//, '');
return BP + '/' + m[1].replace(/^\//, '') + 'thumbs/' + m[2] + '.webp';
}
/**
* Fachartikel-Rendering: nimmt Plain-Text mit Markdown-artigen Markups und
* baut daraus sicheres HTML.
* [text](glossar:slug) → klickbarer Glossar-Link (öffnet Detail-Modal)
* [^n] → Hochgestellte Quellen-Referenz
* **fett** → <strong>
* *kursiv* → <em>
* Absätze durch Leerzeilen.
*/
function renderArticle(text) {
if (!text) return '';
// Zuerst HTML-escapen, dann Markups einsetzen (deren Syntax benutzt ASCII-Zeichen).
let h = esc(text);
// Glossar-Links
h = h.replace(/\[([^\]]+)\]\(glossar:([a-z0-9_-]+)\)/g,
(_, label, slug) => `<a class="gl-inline-link" href="?term=${encodeURIComponent(slug)}" data-key="${esc(slug)}">${label}</a>`);
// Externe Links (normale Markdown)
h = h.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g,
(_, label, url) => `<a class="gl-inline-ext" href="${esc(url)}" target="_blank" rel="noopener">${label}</a>`);
// Quellen-Refs [^n] → hochgestellt
h = h.replace(/\[\^(\d+)\]/g,
(_, n) => `<sup class="gl-ref"><a href="#gl-source-${n}" data-ref="${n}">${n}</a></sup>`);
// Einfache Textauszeichnung
h = h.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
h = h.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, '<em>$1</em>');
// Absätze
const paras = h.split(/\n\s*\n/).map(p => p.trim()).filter(Boolean);
return paras.map(p => '<p>' + p.replace(/\n/g, '<br>') + '</p>').join('');
}
/** Datum yyyy-mm-dd → Abgerufen am D.M.YYYY */
function fmtDate(d) {
if (!d) return '';
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(d);
if (!m) return d;
return `abgerufen ${+m[3]}.${+m[2]}.${m[1]}`;
}
/** Inline-Varianten-Rendering für Beispiele (ein Satz pro Item, keine Absätze). */
function renderInline(text) {
if (!text) return '';
let h = esc(text);
h = h.replace(/\[([^\]]+)\]\(glossar:([a-z0-9_-]+)\)/g,
(_, label, slug) => `<a class="gl-inline-link" href="?term=${encodeURIComponent(slug)}" data-key="${esc(slug)}">${label}</a>`);
h = h.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g,
(_, label, url) => `<a class="gl-inline-ext" href="${esc(url)}" target="_blank" rel="noopener">${label}</a>`);
h = h.replace(/\[\^(\d+)\]/g,
(_, n) => `<sup class="gl-ref"><a href="#gl-source-${n}" data-ref="${n}">${n}</a></sup>`);
h = h.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
return h;
}
function categoryLabel(key) {
if (!key) return '';
return CATEGORY_LABEL[key] || (key.charAt(0).toUpperCase() + key.slice(1));
}
// ============ LOAD ============
async function loadAll() {
const loading = document.getElementById('gl-loading');
try {
const res = await fetch(API);
if (!res.ok) throw new Error('HTTP ' + res.status);
const list = await res.json();
STATE.all = Array.isArray(list) ? list : [];
// Statistik
document.getElementById('gl-stat-total').textContent = STATE.all.length;
document.getElementById('gl-stat-images').textContent = STATE.all.filter(e => e.image_path).length;
loading.style.display = 'none';
renderFilters();
render();
// Tiefen-Link ausführen
if (INIT_TERM) openDetail(INIT_TERM, false);
} catch (err) {
loading.innerHTML = '<strong style="color:var(--ggs-coral)">Glossar konnte nicht geladen werden.</strong><br>' + esc(err.message);
}
}
// ============ FILTERS ============
function renderFilters() {
const wrap = document.getElementById('gl-filters');
const modules = {};
const cats = {};
STATE.all.forEach(e => {
(e.modules || []).forEach(m => { modules[m] = (modules[m] || 0) + 1; });
if (e.category) cats[e.category] = (cats[e.category] || 0) + 1;
});
let html = `<button class="gl-chip${!STATE.filter.module && !STATE.filter.category ? ' active' : ''}" data-type="all">
Alle <span class="gl-chip-count">${STATE.all.length}</span>
</button>`;
// Module-Filter (Reihenfolge aus DB sort_order)
MODULE_ORDER.forEach(m => {
if (!modules[m]) return;
const meta = MODULE_META[m] || { icon: '•', label: m };
html += `<button class="gl-chip${STATE.filter.module === m ? ' active' : ''}" data-type="module" data-value="${m}">
${meta.icon} ${meta.label} <span class="gl-chip-count">${modules[m]}</span>
</button>`;
});
// Kategorie-Filter (nur wenn vorhanden)
Object.keys(cats).sort().forEach(c => {
html += `<button class="gl-chip${STATE.filter.category === c ? ' active' : ''}" data-type="category" data-value="${c}">
${esc(categoryLabel(c))} <span class="gl-chip-count">${cats[c]}</span>
</button>`;
});
// Schulbuch-Link, wenn ein Modul mit Kapitel gefiltert ist
if (STATE.filter.module && BUCH_LINK[STATE.filter.module]) {
html += `<a class="gl-chip gl-buch" href="<?= $bp ?>/${BUCH_LINK[STATE.filter.module]}">📖 Zum Schulbuch-Kapitel</a>`;
}
wrap.innerHTML = html;
wrap.querySelectorAll('.gl-chip').forEach(btn => {
btn.addEventListener('click', () => {
const type = btn.dataset.type, val = btn.dataset.value || '';
if (type === 'all') {
STATE.filter.module = '';
STATE.filter.category = '';
} else if (type === 'module') {
STATE.filter.module = STATE.filter.module === val ? '' : val;
STATE.filter.category = '';
} else if (type === 'category') {
STATE.filter.category = STATE.filter.category === val ? '' : val;
STATE.filter.module = '';
}
renderFilters(); render();
});
});
}
// ============ RENDER GRID ============
const ALPHABET = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','#'];
function normFirstLetter(title) {
const c = String(title || '').trim().charAt(0).toUpperCase();
if (c === 'Ä') return 'A';
if (c === 'Ö') return 'O';
if (c === 'Ü') return 'U';
if (c === 'ß') return 'S';
if (!/[A-Z]/.test(c)) return '#';
return c;
}
function renderCard(e) {
const thumb = fmtThumb(e.image_path);
const full = fmtImg(e.image_path);
// Card lädt das kleine Thumbnail (webp ~1030 KB). Fehlt der Thumb (z.B.
// weil das Generierungs-Skript für diesen Eintrag noch nicht gelaufen ist),
// fällt das <img> per onerror auf das Original zurück — kein Layout-Sprung.
const imgBlock = thumb
? `<div class="gl-card-img"><img src="${esc(thumb)}" alt="" loading="lazy" onerror="this.onerror=null;this.src='${esc(full)}'"></div>`
: `<div class="gl-card-img gl-no-img"><span class="gl-card-img-emoji">${moduleEmoji(e.modules)}</span></div>`;
const modEmoji = (e.modules || []).map(m => MODULE_META[m] ? MODULE_META[m].icon : '').join('');
return `<button class="gl-card" data-key="${esc(e.key_slug)}">
${imgBlock}
<div class="gl-card-body">
${e.category ? `<span class="gl-card-category">${esc(categoryLabel(e.category))}</span>` : ''}
<div class="gl-card-title">${esc(e.title)}</div>
<div class="gl-card-short">${esc(e.short || '')}</div>
${modEmoji ? `<div class="gl-card-modules"><span class="gl-card-mod">${modEmoji}</span></div>` : ''}
</div>
</button>`;
}
function render() {
const grid = document.getElementById('gl-grid');
const info = document.getElementById('gl-grid-info');
const nav = document.getElementById('gl-letter-nav');
const s = STATE.filter.search.trim().toLowerCase();
const filtered = STATE.all.filter(e => {
if (STATE.filter.module && !(e.modules || []).includes(STATE.filter.module)) return false;
if (STATE.filter.category && e.category !== STATE.filter.category) return false;
if (s) {
const hay = (e.title + ' ' + (e.short || '') + ' ' + (e.key_slug || '')).toLowerCase();
if (!hay.includes(s)) return false;
}
return true;
});
if (filtered.length === 0) {
info.innerHTML = '';
nav.innerHTML = '';
nav.style.display = 'none';
grid.innerHTML = `<div class="gl-empty" style="grid-column:1/-1">
<strong>Kein Begriff gefunden.</strong>
${s ? 'Versuche einen anderen Suchbegriff oder entferne den Filter.' : 'Ändere den Filter oben, um andere Kategorien zu sehen.'}
</div>`;
return;
}
info.innerHTML = `<strong>${filtered.length}</strong> Begriff${filtered.length === 1 ? '' : 'e'}${s ? ' für „' + esc(s) + '"' : ''}`;
// Alphabetisch sortieren (Deutsch, Umlaute korrekt)
filtered.sort((a, b) => a.title.localeCompare(b.title, 'de', { sensitivity: 'base' }));
// Nach Anfangsbuchstaben gruppieren
const groups = new Map();
filtered.forEach(e => {
const L = normFirstLetter(e.title);
if (!groups.has(L)) groups.set(L, []);
groups.get(L).push(e);
});
// Sprung-Nav: alle Buchstaben, aktive sind Links, fehlende sind graue Spans
nav.style.display = '';
let navHtml = '';
ALPHABET.forEach(L => {
if (groups.has(L)) {
navHtml += `<a href="#gl-letter-${L === '#' ? 'hash' : L}" data-letter="${L}">${L}</a>`;
} else {
navHtml += `<span>${L}</span>`;
}
});
nav.innerHTML = navHtml;
// Grid: Banner + Cards pro Buchstabe
let html = '';
groups.forEach((entries, L) => {
const anchor = L === '#' ? 'hash' : L;
html += `<div class="gl-letter-banner" id="gl-letter-${anchor}">
<span>${esc(L)}</span>
<span class="gl-letter-banner-count">${entries.length} Begriff${entries.length === 1 ? '' : 'e'}</span>
</div>`;
entries.forEach(e => { html += renderCard(e); });
});
grid.innerHTML = html;
grid.querySelectorAll('.gl-card').forEach(card => {
card.addEventListener('click', () => openDetail(card.dataset.key, true));
});
}
function moduleEmoji(modules) {
if (!modules || !modules.length) return '📘';
return MODULE_META[modules[0]] ? MODULE_META[modules[0]].icon : '📘';
}
// ============ DETAIL MODAL ============
async function openDetail(key, pushUrl) {
const modal = document.getElementById('gl-modal');
const content = document.getElementById('gl-modal-content');
modal.classList.add('open');
document.body.style.overflow = 'hidden';
content.innerHTML = '<div class="gl-loading">Lädt …</div>';
STATE.currentKey = key;
if (pushUrl) {
try { history.pushState({ term: key }, '', '?term=' + encodeURIComponent(key)); } catch {}
}
try {
const url = API + '?key=' + encodeURIComponent(key) + (STATE.easy ? '&easy=1' : '');
const res = await fetch(url);
if (!res.ok) throw new Error('Eintrag nicht gefunden.');
const d = await res.json();
if (d.error) throw new Error(d.error);
// Hat dieser Eintrag überhaupt eine Leichte-Sprache-Version?
// (Wenn kein text_easy, zeigt API automatisch Normaltext — Toggle bleibt sichtbar als Info)
const hasEasy = STATE.easy ? d._easy === true : true;
const img = fmtImg(d.image_path);
let html = '';
// Toggle: Standard / Leichte Sprache
html += `<div class="gl-easy-toggle" role="tablist" aria-label="Sprach-Niveau">
<button type="button" data-easy="0" class="${STATE.easy ? '' : 'active'}" role="tab" aria-selected="${!STATE.easy}">📖 Standard</button>
<button type="button" data-easy="1" class="${STATE.easy ? 'active' : ''}" role="tab" aria-selected="${STATE.easy}">🙂 Leichte Sprache</button>
</div>`;
if (d.category) html += `<span class="gl-detail-category">${esc(categoryLabel(d.category))}</span>`;
html += `<h1 id="gl-detail-title" class="gl-detail-title">${esc(d.title)}</h1>`;
if (d.short) html += `<div class="gl-detail-short">${esc(d.short)}</div>`;
if (d.modules && d.modules.length) {
html += '<div class="gl-modules-row">';
d.modules.forEach(m => {
const meta = MODULE_META[m] || { icon: '•', label: m };
html += `<span class="gl-chip">${meta.icon} ${meta.label}</span>`;
});
html += '</div>';
}
if (img) {
html += `<div class="gl-detail-img" data-src="${esc(img)}" data-alt="Vergleichsgrafik ${esc(d.title)}" role="button" tabindex="0" aria-label="Grafik im Vollbild ansehen"><img src="${esc(img)}" alt="Vergleichsgrafik ${esc(d.title)}"></div>`;
}
if (d.text) html += `<div class="gl-detail-text">${renderArticle(d.text)}</div>`;
if (d.unit) html += `<div class="gl-detail-unit">${esc(d.unit)}</div>`;
// Interaktives Experiment, falls für diesen key_slug registriert
if (window.GlossarExperiments && window.GlossarExperiments[d.key_slug]) {
html += `<div class="gl-section-title">Selbst ausprobieren</div>
<div class="gl-experiment" id="gl-experiment-root"></div>`;
}
if (d.examples && d.examples.length) {
html += '<div class="gl-section-title">Zahlen zum Einordnen</div><ul class="gl-examples">';
d.examples.forEach(ex => {
const txt = typeof ex === 'string' ? ex : ex.text;
const ref = typeof ex === 'object' && ex.source_ref ? ` <sup class="gl-ref"><a href="#gl-source-${ex.source_ref}" data-ref="${ex.source_ref}">${ex.source_ref}</a></sup>` : '';
html += '<li>' + renderInline(txt) + ref + '</li>';
});
html += '</ul>';
}
if (d.sources && d.sources.length) {
html += '<div class="gl-section-title">Quellen</div><ol class="gl-sources">';
d.sources.forEach(s => {
html += `<li id="gl-source-${s.ref_num}" value="${s.ref_num}">` +
(s.url ? `<a href="${esc(s.url)}" target="_blank" rel="noopener">${esc(s.label)}</a>` : esc(s.label)) +
(s.access_date ? ` <span class="gl-source-date">(${fmtDate(s.access_date)})</span>` : '') +
'</li>';
});
html += '</ol>';
}
if (d.links && d.links.length) {
html += '<div class="gl-section-title">Weiterführende Links</div><div class="gl-links">';
d.links.forEach(l => { html += `<a class="gl-link" href="${esc(l.url)}" target="_blank" rel="noopener">${esc(l.label)} →</a>`; });
html += '</div>';
}
if (d.related && d.related.length) {
html += '<div class="gl-section-title">Verwandte Begriffe</div><div class="gl-related">';
d.related.forEach(r => { html += `<a data-key="${esc(r.key_slug)}" href="?term=${encodeURIComponent(r.key_slug)}">${esc(r.title)}</a>`; });
html += '</div>';
}
content.innerHTML = html;
document.title = 'Glossar geograsim.at — ' + d.title;
// Related + Inline-Links im Modal abfangen (zu anderen Glossar-Begriffen)
content.querySelectorAll('.gl-related a, .gl-inline-link').forEach(a => {
a.addEventListener('click', ev => {
ev.preventDefault();
openDetail(a.dataset.key, true);
});
});
// Footnote-Klicks: smooth scroll zur Quelle
content.querySelectorAll('.gl-ref a').forEach(a => {
a.addEventListener('click', ev => {
ev.preventDefault();
const ref = a.dataset.ref;
const t = content.querySelector('#gl-source-' + ref);
if (t) t.scrollIntoView({ behavior: 'smooth', block: 'center' });
if (t) { t.classList.add('gl-source-hi'); setTimeout(() => t.classList.remove('gl-source-hi'), 1500); }
});
});
// Experiment instanziieren, falls Container vorhanden
const expRoot = content.querySelector('#gl-experiment-root');
if (expRoot && window.GlossarExperiments && window.GlossarExperiments[d.key_slug]) {
try { window.GlossarExperiments[d.key_slug](expRoot); }
catch (e) { expRoot.innerHTML = '<div class="gl-empty">Experiment konnte nicht geladen werden.</div>'; }
}
// Easy-Toggle: umschalten und neu laden
content.querySelectorAll('.gl-easy-toggle button').forEach(btn => {
btn.addEventListener('click', () => {
const want = btn.dataset.easy === '1';
if (STATE.easy === want) return;
STATE.easy = want;
localStorage.setItem('ggs-glossar-easy', want ? '1' : '0');
openDetail(STATE.currentKey, false);
});
});
// Infografik-Klick öffnet Lightbox — robust: nimm data-src ODER inneres <img>
content.querySelectorAll('.gl-detail-img').forEach(el => {
const getSrc = () => el.dataset.src || el.querySelector('img')?.getAttribute('src') || '';
const getAlt = () => el.dataset.alt || el.querySelector('img')?.getAttribute('alt') || '';
el.addEventListener('click', ev => { ev.stopPropagation(); openLightbox(getSrc(), getAlt()); });
el.addEventListener('keydown', ev => {
if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); openLightbox(getSrc(), getAlt()); }
});
});
} catch (err) {
content.innerHTML = '<div class="gl-empty"><strong>Eintrag nicht verfügbar</strong>' + esc(err.message) + '</div>';
}
}
function closeDetail(replaceUrl) {
const modal = document.getElementById('gl-modal');
modal.classList.remove('open');
document.body.style.overflow = '';
document.title = 'Glossar geograsim.at';
STATE.currentKey = null;
if (replaceUrl) {
try { history.pushState({}, '', location.pathname); } catch {}
}
}
// ============ LIGHTBOX ============
function openLightbox(src, alt) {
if (!src) return;
const box = document.getElementById('gl-lightbox');
const img = document.getElementById('gl-lightbox-img');
img.src = src;
img.alt = alt || '';
box.classList.add('open');
document.body.style.overflow = 'hidden';
}
function closeLightbox() {
const box = document.getElementById('gl-lightbox');
box.classList.remove('open');
document.getElementById('gl-lightbox-img').src = '';
if (!document.getElementById('gl-modal').classList.contains('open')) {
document.body.style.overflow = '';
}
}
document.getElementById('gl-lightbox').addEventListener('click', closeLightbox);
// ============ EVENTS ============
document.getElementById('gl-modal-close').addEventListener('click', () => closeDetail(true));
document.getElementById('gl-modal').addEventListener('click', ev => {
if (ev.target.id === 'gl-modal') closeDetail(true);
});
document.addEventListener('keydown', ev => {
if (ev.key !== 'Escape') return;
// Priorität: erst Lightbox schließen, dann Modal
if (document.getElementById('gl-lightbox').classList.contains('open')) {
closeLightbox();
} else if (document.getElementById('gl-modal').classList.contains('open')) {
closeDetail(true);
}
});
const search = document.getElementById('gl-search');
const searchClear = document.getElementById('gl-search-clear');
search.addEventListener('input', () => {
STATE.filter.search = search.value;
searchClear.classList.toggle('show', !!search.value);
render();
});
searchClear.addEventListener('click', () => {
search.value = ''; STATE.filter.search = '';
searchClear.classList.remove('show');
search.focus(); render();
});
// Back-Button schließt Detail oder navigiert zurück
window.addEventListener('popstate', ev => {
const term = new URLSearchParams(location.search).get('term');
if (term) openDetail(term, false);
else closeDetail(false);
});
// Initial: Modul-Filter aus URL übernehmen
if (INIT_MODULE) STATE.filter.module = INIT_MODULE;
loadAll();
</script>
</body>
</html>