# SwimMonitor Style

Gestaltungsregeln für den SwimMonitor-Bereich (Container-Überwachung, Administration, Kalender, Signage).
Die Umsetzung steckt in `public/css/swimmonitor.css` und `public/js/swimmonitor.js`. Diese Datei ist die
Referenz: Was hier nicht steht, wird nicht erfunden, sondern zuerst hier ergänzt.

Vorschau aller Bausteine: `styleguide.html` (im Browser öffnen, über einen lokalen Webserver).

---

## 1. Haltung

SwimMonitor ist ein Werkzeug für Technik und Betrieb, kein Marketing-Auftritt. Die Oberfläche soll wirken
wie ein gutes Messgerät: ruhig, präzise, Zahlen stehen im Vordergrund.

**So ja**

- Flache Flächen, 1 px Linien, kleine Radien (4 px, bei Tags 2 px).
- Farbe nur mit Bedeutung: Markenblau für Navigation und Hauptaktion, Grün/Gelb/Rot nur für Zustände.
- Zahlen immer in IBM Plex Mono mit gleich breiten Ziffern, damit Spalten sauber untereinander stehen.
- Kurze, sachliche Beschriftungen auf Deutsch. Lieber „Warnung“ als „Achtung!“.
- Dichte Informationen in Zeilen und Tabellen statt einzelner Karten.

**So nicht** (wirkt schnell generisch bzw. „nach KI“)

- Keine Farbverläufe, kein Glas-Effekt, keine weichen Schatten auf Flächen.
- Keine Pillen-Badges und keine runden „Bubbles“, keine Emojis.
- Keine Icons vor jedem Menüpunkt oder jeder Überschrift. Icons nur, wo sie schneller lesbar sind als Text
  (z. B. Bearbeiten/Löschen in engen Tabellenspalten), dann Font Awesome, einfarbig.
- Keine Hover-Animationen, die Elemente anheben oder vergrößern.
- Keine Füllwörter in Überschriften („Willkommen in deiner Übersicht“). Die Überschrift sagt, was die Seite ist.

---

## 2. Technische Regeln

1. **Kein Inline-CSS.** Kein `style="…"`, keine `<style>`-Blöcke in Templates. Fehlt ein Baustein, wird er in
   `swimmonitor.css` ergänzt und hier dokumentiert.
2. **Kein Inline-JavaScript.** Keine `<script>`-Blöcke mit Code, keine `onclick`, `onchange`, `onsubmit`.
   Verhalten kommt über `data-sm-*`-Attribute (Abschnitt 7) oder über eine eigene Datei unter `public/js/`.
   Daten für Skripte gehen über `data-*`-Attribute oder JSON-Endpunkte.
3. **Kein CDN.** Alle Bibliotheken und Schriften liegen lokal unter `public/`. Ausnahme: Kartenkacheln von
   OpenStreetMap, weil sie sich nicht sinnvoll lokal ablegen lassen.
4. **Ladereihenfolge** (genau so, nichts dazwischen):

   ```twig
   <head>
       <script src="{{ asset('js/swimmonitor-theme.js') }}?v={{ sm_v }}"></script>   {# synchron, ohne defer #}
       <link rel="stylesheet" href="{{ asset('css/vendor/bootstrap.min.css') }}">
       <link rel="stylesheet" href="{{ asset('css/vendor/fontawesome/css/all.min.css') }}">
       {% block vendor_stylesheets %}{% endblock %}                         {# z. B. leaflet.css, fullcalendar.min.css #}
       <link rel="stylesheet" href="{{ asset('css/swimmonitor.css') }}?v={{ sm_v }}">
       {% block stylesheets %}{% endblock %}                                {# seitenspezifisches CSS, z. B. signage.css #}
   </head>
   …
   <script src="{{ asset('js/vendor/bootstrap.bundle.min.js') }}" defer></script>
   {% block vendor_javascripts %}{% endblock %}                             {# z. B. leaflet.js, chart.umd.min.js #}
   <script src="{{ asset('js/swimmonitor.js') }}?v={{ sm_v }}" defer></script>
   {% block javascripts %}{% endblock %}                                    {# seitenspezifische Datei #}
   ```

   `sm_v` wird oben in `swimmonitor/base.html.twig` gesetzt und ist die Cache-Version für eigene Dateien.
   Bei Änderungen an CSS oder JS hochzählen (Datum, z. B. `20261031`). Vendor-Dateien bekommen keine Version.

5. **Content-Security-Policy.** Sobald alle betroffenen Seiten umgestellt sind, kann für diese Seiten
   `script-src 'self'; style-src 'self'` gesetzt werden. Das ist der Prüfstein, ob die Regeln 1 bis 3 eingehalten sind.
6. **Theme.** `data-bs-theme` auf `<html>` steuert hell/dunkel. Gespeichert wird die Wahl unter dem
   localStorage-Schlüssel `theme` mit den Werten `light`, `dark`, `auto` (kompatibel zum bisherigen Umschalter).
   Der URL-Parameter `?theme=dark` (bzw. `light`, `auto`) erzwingt ein Theme für diesen Aufruf, z. B. für Screenshots.

### Dateien

| Datei | Zweck |
|---|---|
| `public/css/swimmonitor.css` | Tokens, Bootstrap-Anpassung, alle Bausteine |
| `public/js/swimmonitor-theme.js` | Theme vor dem ersten Rendern setzen |
| `public/js/swimmonitor.js` | Theme-Schalter, Mobilmenü, Skalen, Filter, Bestätigungen, Karte, Uhr, Farben und Maße aus `data-*` |
| `public/js/pages/*.js` | seitenspezifische Logik (Kalender, Terminarten, To-Do, Lilian Labs, Wasservergleich, Container) |
| `public/css/admin/signage.css`, `public/js/admin/signage-*.js` | Signage-Verwaltung (`.sg-*`), nutzt die `--sm-*`-Tokens |
| `public/css/admin/inspection-templates.css`, `public/js/admin/inspection-templates.js` | Inspektionsvorlagen-Editor |
| `public/css/container/*.css`, `public/js/container/*.js` | Dokumente und Inspektionen auf `/container/{id}` |
| `config/swimmonitor/changelog.yaml` | Inhalt von `/changelog` |
| `src/Twig/SwimMonitorExtension.php` | Twig-Funktion `sm_nav_containers()` für das Container-Menü |
| `public/fonts/ibm-plex/*.woff2` | IBM Plex Sans 400–700, IBM Plex Mono 400–600 (OFL-Lizenz) |
| `public/images/swimmonitor-mark.svg` | Logo-Zeichen flach, für die dunkle Navigationsleiste |
| `public/images/swimmonitor-icon.svg` | Logo-Zeichen farbig, für Favicon und helle Flächen |
| `public/css/vendor/`, `public/js/vendor/` | lokale Kopien: Bootstrap 5.3.3, Font Awesome 6.7.2, Leaflet 1.9.4, FullCalendar 5.11.3, Chart.js 4.4.0 |

Die Bootstrap-Klassen bleiben nutzbar. `swimmonitor.css` passt Buttons, Formulare, Tabellen, Alerts, Modals,
Tabs, Pagination und `.card` an den Stil an, damit Unterseiten auch vor ihrem Umbau schon passend aussehen.
Neue oder umgebaute Seiten verwenden die `sm-`-Bausteine.

---

## 3. Farben

Alle Farben kommen aus CSS-Variablen. **Keine Hex-Werte in Templates oder seitenspezifischem CSS.**

| Token | Hell | Dunkel | Verwendung |
|---|---|---|---|
| `--sm-bg` | `#F4F5F2` | `#0C1118` | Seitenhintergrund |
| `--sm-surface` | `#FFFFFF` | `#121A24` | Panels, Tabellen, Eingabefelder |
| `--sm-surface-2` | `#FAFBF9` | `#151E2A` | Hover, Kopfzeilen, deaktivierte Felder |
| `--sm-line` | `#DFE2DC` | `#212B38` | Trennlinien |
| `--sm-line-strong` | `#BFC4BB` | `#33404F` | Rahmen von Eingabefeldern und Buttons |
| `--sm-text` | `#14181F` | `#E6EAEF` | Text |
| `--sm-muted` | `#5E6673` | `#8C96A4` | Beschriftungen, Nebeninfos |
| `--sm-brand` | `#0A3D8F` | `#6E9BF2` | Hauptaktion, Links, aktive Zustände |
| `--sm-accent` | `#0093B0` | `#3CC7E2` | nur im Logo und für die Markierung in der Navigation |
| `--sm-nav-bg` | `#0A3D8F` | `#0E1724` | Navigationsleiste |
| `--sm-ok` | `#1E7F45` | `#46B877` | im Sollbereich, online, erledigt |
| `--sm-warn` | `#A86A0B` | `#E2A445` | Warnschwelle erreicht |
| `--sm-alarm` | `#BF3129` | `#EE5D52` | Grenze überschritten, offline, Löschen |
| `--sm-none` | `#8A9099` | `#5F6977` | keine Daten |

Zu jedem Zustand gibt es eine Hintergrundfarbe (`--sm-ok-bg`, `--sm-warn-bg`, `--sm-alarm-bg`, `--sm-info-bg`)
für Alerts und Tags. Flächig eingefärbt werden nur Alerts und Tags, niemals ganze Panels oder Kacheln.

**Zustände der Messwerte** (Werte aus `t1_status`, `chlor_status`, … der API):

| API-Wert | Klasse | Text |
|---|---|---|
| `success` | `is-ok` | im Soll |
| `warning` | `is-warn` | Warnung |
| `danger` | `is-alarm` | Alarm |
| `secondary` / leer | `is-none` | keine Daten |

---

## 4. Schrift und Abstände

- **IBM Plex Sans** für Text, **IBM Plex Mono** für Zahlen, Zeiten, Kennungen, Spaltenköpfe und kleine Labels.
- Grundgröße 15 px. Überschriften: `h1` 28 px, `h2` 20 px, `h3` 17 px, alles darunter 15 px. Gewicht 600.
- Kleine Labels (Spaltenköpfe, Kennzahlen-Titel): Klasse `.sm-label` – Mono, 11 px, Großbuchstaben, 0,08 em Sperrung.
- Zahlen in Fließtext oder Tabellen: Klasse `.sm-num`; in Tabellen rechtsbündig mit `td.sm-num`.
- Einheiten immer mit Leerzeichen und kleiner/gedämpft: `<span class="sm-unit">mg/l</span>`.
- Dezimaltrennzeichen Komma: in Twig `|number_format(2, ',', '.')`.
- Abstände in 4-px-Schritten: 4, 8, 12, 16, 20, 24, 28. Standard zwischen Panels: 16 px (`--sm-gap`).
- Seitenbreite maximal 1400 px (`.sm-page`); Formulare und Textseiten 880 px (`.sm-page--narrow`).

---

## 5. Seitenaufbau

Jede Seite hat dieselbe Abfolge: Navigationsleiste, Breadcrumb, Seitenkopf, Inhalt, Fußzeile.

```twig
{% extends 'swimmonitor/base.html.twig' %}

{% block title %}Benutzer{% endblock %}

{% block breadcrumb %}
    <li><a href="{{ path('admin_user_index') }}">Administration</a></li>
    <li>Benutzer</li>
{% endblock %}

{% block page_head %}
    <div>
        <h1>Benutzer</h1>
        <p class="sm-lead">Zugänge zum SwimMonitor und ihre Rollen.</p>
    </div>
    <div class="sm-page-actions">
        <a class="btn btn-primary" href="{{ path('admin_user_register') }}">Neuer Benutzer</a>
    </div>
{% endblock %}

{% block content %}
    …
{% endblock %}
```

Der Seitenkopf rechts enthält entweder Aktionen (`.sm-page-actions`) oder Kennzahlen (`.sm-summary`), nicht beides.
Der aktive Menüpunkt ergibt sich in `_topbar.html.twig` aus der Route (`_route`), Seiten müssen dafür nichts setzen.

### Blöcke von `templates/swimmonitor/base.html.twig`

| Block | Inhalt |
|---|---|
| `title` | Seitentitel ohne „· SwimMonitor“ |
| `page_modifier` | z. B. ` sm-page--narrow` (mit führendem Leerzeichen) |
| `body_attr` | Attribute für `<body>`, z. B. ` class="sg-app"` oder ` data-sm-refresh="300"` |
| `breadcrumb` | `<li>`-Einträge nach „SwimMonitor“ |
| `page_head` | Überschrift, Lead und Aktionen bzw. Kennzahlen |
| `flashes` | Standard: `_flashes.html.twig`; überschreiben nur für Sonderfälle (Signage-Einrichtungscode), dann `{{ parent() }}` |
| `content` | Inhalt |
| `vendor_stylesheets`, `stylesheets`, `vendor_javascripts`, `javascripts` | siehe Ladereihenfolge |

### Grundgerüst `templates/swimmonitor/base.html.twig`

```twig
{% set sm_v = '20261031' %}
<!DOCTYPE html>
<html lang="de" data-bs-theme="light">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{% block title %}Übersicht{% endblock %} · SwimMonitor</title>
    <link rel="icon" type="image/svg+xml" href="{{ asset('images/swimmonitor-icon.svg') }}">
    {# … Ladereihenfolge wie in Abschnitt 2 … #}
</head>
<body{% block body_attr %}{% endblock %}>
    {% include 'swimmonitor/_topbar.html.twig' %}

    <main class="sm-page{% block page_modifier %}{% endblock %}">
        <ol class="sm-crumbs">
            <li><a href="{{ path('api_data') }}">SwimMonitor</a></li>
            {% block breadcrumb %}{% endblock %}
        </ol>
        <header class="sm-page-head">{% block page_head %}<h1>{{ block('title') }}</h1>{% endblock %}</header>
        {% block flashes %}{% include 'swimmonitor/_flashes.html.twig' %}{% endblock %}
        {% block content %}{% endblock %}
    </main>

    <footer class="sm-footer">
        <div class="sm-footer-inner">
            <span>SwimMonitor · dp-smartsolutions.de</span>
            <span>
                <a href="{{ path('app_privacy') }}">Datenschutz</a>
                <a href="{{ path('app_imprint') }}">Impressum</a>
            </span>
        </div>
    </footer>

    {# … Skripte wie in Abschnitt 2 … #}
</body>
</html>
```

Der bestehende `base.html.twig` bleibt für alle Seiten, die nicht umgebaut werden (Buchungstool, Kursleiter-Portal,
Event Center usw.). Dort sind nur die Menülinks auf andere Bereiche entfernt; die Seiten bleiben unter ihrer URL
erreichbar und sehen aus wie bisher. Umgebaute Seiten erben von `swimmonitor/base.html.twig`.

### Twig-Fallen

- Makros im selben Template: `{% import _self as m %}` **innerhalb** des Blocks, in dem sie genutzt werden.
  `{% set m = _self %}` funktioniert nicht.
- In einem Kind-Template sind auf oberster Ebene nur `{% set %}`, `{% import %}` und Blöcke erlaubt.
  Bedingungen (`{% if %}`) gehören in einen Block.
- Symfony-Forms: `{% form_theme form 'bootstrap_5_layout.html.twig' %}` oben im Template.
  Feldnamen wie `title` oder `name` kollidieren im JavaScript mit Eigenschaften des Formulars, dort `form.elements.title` verwenden.
- Textareas bekommen ihre Höhe über `rows`, nicht über CSS.

---

## 6. Navigation

### Inhalt des Menüs

Nur die SwimMonitor-Seiten stehen im Menü. Alle anderen Seiten bleiben unter ihrer URL erreichbar,
werden aber nicht mehr verlinkt.

| Menüpunkt | Einträge | Route / URL |
|---|---|---|
| **Übersicht** | – | `/api-data` (aktiv auch auf `/container/…`) |
| **Container** ▾ | ein Eintrag je Container, rechts `online`/`offline` | `/container/{id}` |
| **Wasser** ▾ | Wasservergleich | `/admin/water-comparison` |
| | Inspektionsvorlagen | `/admin/inspection-templates` |
| | Lilian Labs | `/admin/lilian-labs` |
| **Termine** ▾ | Kalender | `/calendar` |
| | Terminarten | `/admin/appointment-types` |
| **Signage** | – | `/signage` |
| **Buchungssystem** | – (Absprung ins alte Layout; nur `ROLE_ADMIN`, `ROLE_BOOKING_ADMIN`, `ROLE_SUPERADMIN`, `ROLE_DEVELOPER`) | `/appointments/admin/dashboard` |
| **Administration** ▾ | Benutzer | `/admin/user` |
| | Neuer Benutzer | `/admin/user/register` |
| | Einstellungen | `/admin/settings` |
| | To-Do | `/admin/todo` |
| | Changelog | `/changelog` |
| Fußzeile | Datenschutz, Impressum | `/appointments/privacy`, `/appointments/imprint` |

**Nicht mehr im Menü** (weiter erreichbar): Willkommen, QR-Code Scanner, Interaktive Tour,
Buchungssystem-Dokumentation, Buchungstool, Buchungstool Admin, Kalender Admin (alt) sowie alle übrigen Seiten.

Rollenprüfungen (`is_granted(...)`) bleiben genau wie bisher an den Einträgen. Ein Dropdown, dessen Einträge
für die Rolle alle ausgeblendet sind, wird komplett weggelassen.

### Markup `templates/swimmonitor/_topbar.html.twig`

```twig
{% set sm_route = app.request.attributes.get('_route') ?? '' %}
{% set sm_area = sm_route == 'api_data' ? 'overview' : (sm_route starts with 'container_' ? 'container' : …) %}
<header class="sm-topbar">
    <div class="sm-topbar-inner">
        <a class="sm-brand" href="{{ path('api_data') }}">
            <img src="{{ asset('images/swimmonitor-mark.svg') }}" alt="">
            <span class="sm-brand-name">Swim<span>Monitor</span></span>
            <span class="sm-brand-version">2.0</span>   {# Hauptversion, bei Major-Release anpassen #}
        </a>
        <button class="sm-nav-toggle" type="button" data-sm-nav-toggle aria-expanded="false">Menü</button>

        <ul class="sm-menu">
            <li><a href="{{ path('api_data') }}" class="{{ sm_area == 'overview' ? 'active' }}">Übersicht</a></li>
            <li class="dropdown">
                <button type="button" class="{{ sm_area == 'container' ? 'active' }}" data-bs-toggle="dropdown" aria-expanded="false">
                    Container <i class="sm-caret"></i>
                </button>
                <ul class="dropdown-menu">
                    {% for c in sm_nav_containers() %}
                        <li><a class="dropdown-item" href="{{ path('container_show', {id: c.id}) }}">
                            {{ c.name }} <span class="sm-hint{{ c.online ? '' : ' is-off' }}">{{ c.online ? 'online' : 'offline' }}</span>
                        </a></li>
                    {% endfor %}
                </ul>
            </li>
            {# … Wasser, Termine, Signage, Administration nach demselben Muster … #}
        </ul>

        <div class="sm-tools">
            <div class="sm-theme-switch" role="group" aria-label="Darstellung">
                <button type="button" data-sm-theme-value="light">Hell</button>
                <button type="button" data-sm-theme-value="dark">Dunkel</button>
                <button type="button" data-sm-theme-value="auto">Auto</button>
            </div>
            {# Benutzer-Dropdown: .sm-user mit .sm-initials (Avatar-Bild oder Initialen) #}
        </div>
    </div>
</header>
```

- Menüpunkte ohne Icons. Aktiver Punkt: Klasse `active` (Unterstrich in `--sm-nav-indicator`).
- Dropdown-Auslöser sind `<button>`, keine `<a href="#">`.
- Gruppen in Dropdowns mit `<h6 class="dropdown-header">` und `<hr class="dropdown-divider">`.
- `sm_nav_containers()` ist eine Twig-Funktion aus `src/Twig/SwimMonitorExtension.php`, damit das Menü auf allen Seiten
  gleich ist (bisher kam `containers` nur auf manchen Seiten an). Sie wird nur für angemeldete Benutzer aufgerufen.

---

## 7. Bausteine

### Panel

```html
<section class="sm-panel">
    <div class="sm-panel-head"><h2>Meldungen</h2><span class="sm-panel-meta">letzte 24 h</span></div>
    <div class="sm-panel-body">…</div>
    <div class="sm-panel-foot"><button class="btn btn-primary">Speichern</button></div>
</section>
```

`.card`, `.card-header`, `.card-body` sehen identisch aus, damit alte Unterseiten nicht aus dem Rahmen fallen.

### Kennzahlen im Seitenkopf

```html
<dl class="sm-summary">
    <div><dt>Container</dt><dd>3</dd></div>
    <div><dt>Offline</dt><dd class="is-alarm">1</dd></div>
    <div><dt>Stand</dt><dd>22:58</dd></div>
</dl>
```

Höchstens fünf Kennzahlen. Farbe nur, wenn der Wert ein Zustand ist.

### Buttons

| Klasse | Wann |
|---|---|
| `btn btn-primary` | die eine Hauptaktion einer Seite oder eines Formulars |
| `btn btn-secondary` | alle weiteren Aktionen (Abbrechen, Exportieren, Filtern) |
| `btn btn-outline-danger` | Löschen, Sperren; immer mit `data-sm-confirm` |
| `sm-link-action` | „Details →“ in Listen und Tabellen |

Pro Bereich höchstens ein `btn-primary`. Texte als Verb: „Speichern“, „Benutzer anlegen“, nicht „OK“.

### Toolbar mit Suche und Filter

```html
<div class="sm-toolbar">
    <input class="sm-search" type="search" placeholder="Container suchen …" data-sm-search="#container-list">
    <div class="sm-segment" data-sm-filter="#container-list">
        <button type="button" class="active" data-sm-filter-value="all">Alle<span class="sm-count">3</span></button>
        <button type="button" data-sm-filter-value="offline">Offline<span class="sm-count">1</span></button>
    </div>
</div>
<div id="container-list">
    <div data-sm-item data-sm-tags="online warning">…</div>
    <p class="sm-empty" data-sm-empty hidden><strong>Keine Treffer</strong>Suchbegriff oder Filter ändern.</p>
</div>
```

### Tabelle

```html
<section class="sm-panel">
    <div class="table-responsive">
        <table class="table">
            <thead><tr><th>Name</th><th>Rolle</th><th class="sm-num">Letzter Login</th><th></th></tr></thead>
            <tbody>
                <tr>
                    <td>Dominique P.</td>
                    <td><span class="sm-tag sm-tag--info">Superadmin</span></td>
                    <td class="sm-num">31.10.2026 22:41</td>
                    <td class="sm-actions"><a class="sm-link-action" href="…">Bearbeiten</a></td>
                </tr>
            </tbody>
        </table>
    </div>
</section>
```

Keine gestreiften Tabellen (`table-striped`), keine dunklen Kopfzeilen (`table-dark`).

### Status, Punkt, Tag

```html
<span class="sm-status is-ok">online</span>          <!-- Punkt + Text -->
<span class="sm-dot is-off"></span>                  <!-- nur Punkt, neben einem Namen -->
<span class="sm-tag sm-tag--warn">Warnung</span>     <!-- kompakte Markierung in Tabellen -->
```

Bestehende `.badge bg-success` usw. werden automatisch als Tag dargestellt.

### Messwert mit Skala

```html
<div class="sm-metric">
    <div class="sm-metric-label">Freies Chlor</div>
    <div class="sm-metric-value">0,28<span class="sm-unit">mg/l</span></div>
    <div class="sm-scale" data-sm-scale data-min="0" data-max="1"
         data-target-min="0.3" data-target-max="0.6" data-value="0.28" data-state="warn"></div>
    <div class="sm-metric-foot"><span class="sm-target">Soll 0,3–0,6</span><span class="sm-state is-warn">Warnung</span></div>
</div>
```

- Die Skala zeichnet `swimmonitor.js` aus den `data-`-Attributen. Werte mit Punkt als Dezimaltrenner.
- Der hinterlegte Bereich ist der Sollbereich der Poolsteuerung (`*_untere_grenze` bis `*_obere_grenze`),
  die Skalenenden sind feste Anzeigegrenzen je Messgröße:

  | Messgröße | `data-min` | `data-max` |
  |---|---|---|
  | Temperatur (°C) | 20 | 34 |
  | Freies Chlor (mg/l) | 0 | 1 |
  | Redox (mV) | 500 | 900 |
  | pH | 6.6 | 7.8 |

- Kein Wert: `<div class="sm-metric-value is-empty">—<span class="sm-unit">mg/l</span></div>`, `data-value` weglassen,
  Zustand „keine Daten“.
- Varianten: `.sm-metric--lg` (große Zahl auf Detailseiten), `.sm-metric-strip` (vier Messwerte nebeneinander).

### Container-Zeile (Übersicht)

```html
<section class="sm-panel">
    <div class="sm-crow-grid sm-crow-head">
        <div class="sm-label">Container</div><div class="sm-label">Temperatur</div><div class="sm-label">Freies Chlor</div>
        <div class="sm-label">Redox</div><div class="sm-label">pH-Wert</div><div></div>
    </div>
    <div id="container-list">
        <div class="sm-crow-grid sm-crow" data-sm-item data-sm-tags="online">
            <div>
                <div class="sm-crow-name"><span class="sm-dot is-on"></span><a href="…">Schwimmcontainer Düren</a></div>
                <div class="sm-crow-loc">Düren / NRW / Deutschland</div>
                <div class="sm-crow-conn">online · vor 2 min</div>
            </div>
            <div class="sm-metric">…</div> <!-- viermal -->
            <div class="sm-crow-action"><a class="sm-link-action" href="…">Details</a></div>
        </div>
    </div>
</section>
```

### Schlüssel/Wert-Liste

```html
<dl class="sm-kv">
    <dt>Obere Grenze</dt><dd>0,60 mg/l</dd>
    <dt>Untere Grenze</dt><dd>0,30 mg/l</dd>
</dl>
```

Für Einstellungen der Poolsteuerung, Gerätedaten, Benutzerdetails.

### Protokoll / Meldungen

```html
<ul class="sm-log">
    <li class="sm-log--alarm">
        <span class="sm-log-time">18:12</span>
        <div><div class="sm-log-text"><b>Kreuzau</b> nicht erreichbar</div><div class="sm-log-detail">Router antwortet nicht</div></div>
    </li>
</ul>
```

Randfarbe über `sm-log--warn`, `sm-log--alarm`, `sm-log--ok`; ohne Modifikator grau (Information).
Nicht die `is-…`-Klassen verwenden, die färben den ganzen Text.

### Formulare

```html
<form class="sm-form" method="post">
    <fieldset class="sm-fieldset">
        <legend>Zugang</legend>
        <div class="sm-form-grid">
            <div><label class="form-label" for="email">E-Mail</label><input class="form-control" id="email" type="email"></div>
            <div><label class="form-label" for="role">Rolle</label><select class="form-select" id="role">…</select></div>
            <div class="sm-span-2"><div class="form-text">Das Passwort wird per E-Mail gesetzt.</div></div>
        </div>
    </fieldset>
    <div class="sm-form-actions">
        <a class="btn btn-secondary" href="…">Abbrechen</a>
        <button class="btn btn-primary" type="submit">Benutzer anlegen</button>
    </div>
</form>
```

Symfony-Forms: Theme `bootstrap_5_layout.html.twig` funktioniert direkt. Felder in `.sm-form-grid` gruppieren,
Abschnitte mit `.sm-fieldset`. Labels über dem Feld, nie Platzhalter statt Label.

### Meldungen nach Aktionen (Flash)

```twig
{# templates/swimmonitor/_flashes.html.twig – liest nur die bekannten Typen, andere (z. B. enrollment_code) bleiben liegen #}
{% set sm_flash_map = { success: 'success', notice: 'info', info: 'info', warning: 'warning', error: 'danger', danger: 'danger' } %}
{% set sm_flashes = app.flashes(sm_flash_map|keys) %}
{% if sm_flashes|filter(m => m is not empty) is not empty %}
    <div class="sm-flash-stack">
        {% for type, messages in sm_flashes %}
            {% for message in messages %}
                <div class="alert alert-{{ sm_flash_map[type] }} alert-dismissible" role="alert">
                    {{ message }}
                    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Schließen"></button>
                </div>
            {% endfor %}
        {% endfor %}
    </div>
{% endif %}
```

### Changelog

Siehe Abschnitt 9 unter `/changelog`.

### Leerzustand

```html
<div class="sm-empty"><strong>Noch keine Inspektionsvorlagen</strong>Lege die erste Vorlage an, um Inspektionen zu starten.</div>
```

### Frei gewählte Farben (Chips, Terminarten, To-Do-Tags)

Farben, die Benutzer selbst festlegen (Terminarten, To-Do-Tags), kommen als `data-sm-color="#hex"` ins Markup.
`swimmonitor.js` setzt daraus `--sm-color` per CSSOM; das ist mit `style-src 'self'` erlaubt, ein `style=`-Attribut nicht.

```html
<span class="sm-chip" data-sm-color="#3498db">Wartung</span>                       <!-- Punkt + Text, Rand in der Farbe -->
<a class="sm-chip" href="…" data-sm-color="#1E7F45">Düren</a>                     <!-- als Link -->
<div class="sm-chip-row">
    <label class="sm-chip sm-chip--check" data-sm-color="#3498db">
        <input type="checkbox" checked> Standard                                  <!-- Filter/Mehrfachauswahl -->
    </label>
</div>
<span class="sm-type-preview" data-sm-color="#3498db"><i class="fas fa-wrench"></i>Wartung</span>  <!-- Terminart mit Icon -->
```

Nach dynamisch eingefügtem Markup `SwimMonitor.initColors(container)` aufrufen. Ungültige Werte werden ignoriert.

### Maße aus Daten

Fortschrittsbalken und Zeitleisten bekommen Breite, Position und Anteil als `data-sm-width="42"`, `data-sm-left="10"`
(Prozent) und `data-sm-grow="3"` (`flex-grow`). `swimmonitor.js` setzt die Werte per CSSOM
(`SwimMonitor.initGeometry(container)` nach dynamischem Einfügen). Skripte, die Werte laufend ändern, schreiben direkt
`el.style.width = …`.

### Aktion mit Erklärung

```html
<div class="sm-action-row">
    <div><strong>Jetzt synchronisieren</strong><div class="form-text">Holt die Messwerte der letzten 30 Tage.</div></div>
    <button type="button" class="btn btn-secondary">Synchronisieren</button>
</div>
```

Mehrere `.sm-action-row` untereinander werden durch eine Linie getrennt. Für Konfigurationsfelder mit JSON oder
Code: `textarea.form-control.sm-code-input` (Mono, kleiner).

### Weitere Bausteine

| Klasse | Zweck |
|---|---|
| `.sm-initials`, `--light`, `--xl` | Avatar mit Initialen bzw. Bild; `--xl` auf Profilseiten |
| `.sm-user-cell`, `.sm-avatar-row` | Name mit Avatar in Tabellen bzw. Formularen |
| `.sm-tag-list` | Tags umbrechend nebeneinander |
| `.sm-input-narrow` | schmales Eingabefeld für Zahlen (z. B. Sekunden) |
| `.sm-toolbar--panel` | Toolbar innerhalb eines Panels, mit Trennlinie |
| `.sm-todo`, `.sm-todo-add`, `.sm-todo-actions`, `.sm-tag-manage` | To-Do-Liste mit Tags |
| `.sm-tpl-grid`, `.sm-tpl`, `.sm-field-*` | Inspektionsvorlagen und ihr Feld-Editor (`inspection-templates.css`) |
| `.sm-wc-chart`, `.sm-wc-legend`, `.sm-delta` | Diagramme und Abweichungen im Wasservergleich |
| `.sm-cal-event`, `.sm-cal-event-meta` | Inhalt eines FullCalendar-Termins |
| `.sm-icon-grid`, `.sm-icon-option` | Icon-Auswahl im Modal (Terminarten) |
| `.sm-legal` | Rechtstexte in `.sm-prose`, gleicht die Klassen aus `privacy_content.html.twig` an |

---

## 8. JavaScript-Hooks

Alles über Attribute, kein Code im Template.

| Attribut | Wirkung |
|---|---|
| `data-sm-theme-value="light\|dark\|auto"` | Theme umschalten (Buttons im Theme-Schalter) |
| `data-sm-nav-toggle` | Menü auf kleinen Bildschirmen auf-/zuklappen |
| `data-sm-scale` + `data-min/max/target-min/target-max/value/state` | Skala zeichnen |
| `data-sm-search="#liste"` | Liste live durchsuchen (`[data-sm-item]`, optional `data-sm-text`) |
| `data-sm-filter="#liste"` + `data-sm-filter-value` | Liste nach `data-sm-tags` filtern; `all` zeigt alles |
| `data-sm-empty` | Element innerhalb der Liste, das bei 0 Treffern erscheint |
| `data-sm-confirm="Text"` | Rückfrage vor Absenden eines Formulars oder Klick (ersetzt `onclick="return confirm()"`) |
| `data-sm-autosubmit` | Formular bei Änderung eines Feldes absenden (Filter-Selects) |
| `data-sm-clock` / `data-sm-clock="date"` | Uhrzeit bzw. Datum anzeigen und aktualisieren |
| `data-sm-refresh="300"` auf `<body>` | Seite alle n Sekunden neu laden (mindestens 15) |
| `data-sm-map` | Leaflet-Karte; Marker aus `data-sm-map-src` (URL) oder `data-sm-map-markers` (JSON). Mit `model` (GLB-Pfad) steht das Anlagen-Modell als kleines, stehendes Objekt auf der Karte |
| `data-sm-map-link="/container/__ID__"` | Klick auf Marker öffnet diese URL (`__ID__` wird ersetzt) |
| `data-bs-toggle="tooltip"` | Bootstrap-Tooltip |
| `data-sm-color="#hex"` | setzt `--sm-color` (Chips, Terminarten, Farbmarker) |
| `data-sm-width`, `data-sm-left`, `data-sm-grow` | Breite/Position in Prozent bzw. `flex-grow` |

Seitenspezifische Logik (z. B. FullCalendar-Initialisierung) kommt in eine eigene Datei
`public/js/pages/<seite>.js`, eingebunden über `{% block javascripts %}` mit `defer`. Konfiguration aus dem Controller
wird als `data-`-Attribut am Wurzelelement übergeben, z. B. `data-url-list="{{ path('api_container_appointments_list') }}"`.
URLs mit ID bekommen den Platzhalter `__ID__`, den das Skript ersetzt:
`data-url-item="{{ path('api_container_appointments_update', {id: '__ID__'}) }}"`.

- Sichtbarkeit wird mit dem Attribut `hidden` umgeschaltet, nicht mit Klassen oder `style.display`.
- DOM aus Daten wird mit `textContent` bzw. `createElement` gebaut; `innerHTML` nur für feste Zeichenketten ohne Daten
  und ohne `style=`/`on…`.
- Wiederkehrendes Markup (z. B. ein Formularfeld im Editor) steht als `<template>` im Twig und wird geklont.

`window.SwimMonitor` stellt `initScales`, `initColors`, `initGeometry` und `applyTheme` bereit.
Das Ereignis `sm:themechange` auf `document` meldet einen Theme-Wechsel (z. B. für Diagramme, die ihre Farben
aus den Tokens lesen).

---

## 9. Seitenmuster

Für jede betroffene Seite und ihre Unterseiten (Anlegen, Bearbeiten, Detail) gilt das hier beschriebene Muster.

### `/api-data` – Container-Übersicht
- Kopf: `h1` „Container-Übersicht“, rechts `.sm-summary` (Container, Online, Offline, Warnungen, Stand).
- Toolbar: Suche + Filter Alle / Online / Offline / Mit Warnung.
- Panel mit Container-Zeilen (Abschnitt 7). Klick auf einen Messwert öffnet kein Modal mehr; Grenzen und
  Warnschwellen stehen auf der Detailseite.
- Darunter `.sm-grid-aside`: links Karte mit Mini-3D-Modell je Container (`data-sm-map-markers`, Feld `model` aus `ContainerSceneCatalog`),
  rechts Panel „Meldungen“ (`.sm-log`) mit den letzten Grenzwert-Verletzungen. Script: `public/js/pages/api-map-models.js`.

### `/container/{id}` – Container-Detail
- Oben `.sm-ansicht`: 3D-Modell aus `public/container-ansicht/` (Three.js als Modul `public/js/container/ansicht.js`, Stil `public/css/container/ansicht.css`). Die Werte kommen als JSON in `data-scene`. Klick fährt in Technik, Nassbereich oder Trockenbereich, das Dach geht weg. `prefers-reduced-motion` überspringt die Fahrt und die Wasserwelle.
- Tab Verläufe: eigene Diagramme (`public/js/container/charts.js`, Daten in `#sm-container-charts`), keine Grafana-Rahmen. Viertelstunden der letzten 24 Stunden.
- Breadcrumb: SwimMonitor / Container / Name. Kopf: Name, darunter Ort und `.sm-status`.
- `.sm-metric-strip` mit den vier Messwerten in `.sm-metric--lg`.
- Pro Messgröße ein Panel mit `.sm-kv`: „Poolsteuerung“ (obere/untere Grenze, Sollwert) und „SwimMonitor-Warnschwellen“.
- Verläufe als Diagramm-Panel; Diagrammfarben aus den Tokens (`--sm-brand`, Sollbereich `--sm-band`).
- Gerätesteuerung (Bayrol/Dulcopool) als `btn btn-secondary` „Gerätesteuerung öffnen“, öffnet in neuem Tab.

### `/admin/user` – Benutzer
- Tabelle: Name (mit `.sm-initials`), E-Mail, Rollen als `.sm-tag`, letzter Login (`.sm-num`), Aktionen.
- Toolbar: Suche, Filter nach Rolle. Hauptaktion „Neuer Benutzer“.
- Unterseiten Bearbeiten/Profil: `.sm-page--narrow`, Formularmuster; Löschen als `btn-outline-danger`
  mit `data-sm-confirm` in eigenem Panel „Gefahrenbereich“ am Ende.

### `/admin/user/register` – Neuer Benutzer
- `.sm-page--narrow`, Fieldsets „Person“, „Zugang“, „Rollen“ (Checkboxen untereinander mit Kurzbeschreibung).

### `/admin/settings` – Einstellungen
- `.sm-page--narrow`, ein Formular mit Fieldsets je Thema (Benachrichtigungs-E-Mails, TV-Karussell).
- Kommen weitere Themen dazu: `.sm-grid-aside--left` mit Abschnittsliste links und je Abschnitt ein Panel mit
  eigenem „Speichern“ in `.sm-panel-foot`.

### `/changelog` – Changelog
Wird zur nachvollziehbaren Änderungshistorie ausgebaut. Jeder Eintrag ist eine Version:

```html
<div class="sm-changelog">
    <article class="sm-release" id="v2-0-0">
        <div class="sm-release-meta">
            <div class="sm-release-version">2.0.0</div>
            <div class="sm-release-date">31.10.2026</div>
            <div class="sm-release-scope"><span class="sm-tag">Oberfläche</span><span class="sm-tag">Container</span></div>
        </div>
        <div class="sm-release-body">
            <h3>Neuer SwimMonitor-Stil</h3>
            <p>Kurze Zusammenfassung in ein bis zwei Sätzen.</p>
            <div class="sm-change"><span class="sm-change-type is-new">Neu</span><div>Messwerte mit Sollbereich-Skala<span class="sm-change-area">/api-data</span></div></div>
            <div class="sm-change"><span class="sm-change-type is-improved">Verbessert</span><div>…</div></div>
            <div class="sm-change"><span class="sm-change-type is-fixed">Behoben</span><div>…</div></div>
        </div>
    </article>
</div>
```

- Typen: **Neu** (`is-new`), **Verbessert** (`is-improved`), **Behoben** (`is-fixed`), **Entfernt** (`is-removed`),
  **Sicherheit** (`is-security`).
- Jede Änderung nennt den betroffenen Bereich bzw. die URL (`.sm-change-area`).
- Versionsnummern nach SemVer: Hauptversion bei Umbauten wie diesem, Nebenversion bei neuen Funktionen,
  Patch bei Fehlerbehebungen.
- Toolbar: Suche und Filter nach Typ (`data-sm-tags="new fixed"` an `.sm-release`).
- Inhalt steht in `config/swimmonitor/changelog.yaml`, neueste Version oben:

  ```yaml
  releases:
    - version: 2.0.0
      date: 2026-10-31
      title: Neuer SwimMonitor-Stil
      summary: Ein bis zwei Sätze.            # optional
      changes:
        - { type: new, area: /api-data, text: Messwerte mit Sollbereich-Skala }
  ```

  `type` ist `new`, `improved`, `fixed`, `removed` oder `security`. Jede Version ist unter `/changelog#v2.0.0` verlinkbar.
- **Pflicht:** Jede Änderung am SwimMonitor-Bereich bekommt einen Eintrag, bevor sie live geht.

### `/admin/todo` – To-Do
- Links Panel „Aufgaben“: Eingabe mit Tag-Auswahl (`.sm-chip--check`), Toolbar mit Suche und Filter Offen / Erledigt / Alle,
  darunter `.sm-todo`. Rechts Panel „Tags“ zum Anlegen, Umbenennen, Umfärben und Löschen.
- Logik in `public/js/pages/admin-todo.js` gegen die bestehenden JSON-Routen; Aktionen über `data-todo-action`
  bzw. `data-tag-action`, Tag-Farben über `data-sm-color`.

### `/admin/inspection-templates` – Inspektionsvorlagen
- Toolbar mit Suche und Filter Aktiv / Inaktiv, darunter `.sm-tpl-grid` mit einer Karte je Vorlage
  (Name, Beschreibung, Anzahl Felder, Stand, Aktionen Vorschau / Kopieren / Bearbeiten).
- Editor im Modal: Grunddaten, Feldliste `.sm-field-list` mit Sortieren per Ziehen am Griff (natives Drag-and-drop,
  keine Bibliothek), Feld-Markup aus `<template id="fieldItemTemplate">`. Vorschau in einem zweiten Modal.

### `/admin/lilian-labs` – Lilian Labs
- Übersicht: `.sm-grid-2` mit Panel „Status“ (`.sm-kv`: Token, letzte Synchronisierung, Zeitraum) und Panel „Aktionen“
  (`.sm-action-row`), darunter die Standorte als `.sm-chip`-Links zu den Messwerten.
- Konfiguration: Token als Passwortfeld mit Anzeigen-Knopf (`data-sm-reveal`), JSON-Felder als `.sm-code-input`
  mit Prüfen/Formatieren.
- Messwerte: Tabelle mit `.sm-num`, Filter-Select mit `data-sm-autosubmit`, Pagination.

### `/admin/water-comparison` – Wasservergleich
- Kopf mit Standort und Zeitraum als Selects (`data-sm-autosubmit`, 1 bis 365 Tage), Link zur Detailtabelle.
- Kennzahlen-Panel, darunter je Messgröße (pH, freies Chlor, Gesamtchlor) ein Diagramm-Panel mit Chart.js.
  Farben aus den Tokens (`--sm-brand` SwimMonitor, `--sm-accent` Labor), neu gezeichnet bei `sm:themechange`.
- Details: Tabelle mit beiden Werten und Abweichung (`.sm-delta`).

### `/calendar` – Kalender
- FullCalendar lokal (`public/js/vendor/fullcalendar.min.js` + `fullcalendar-de.js`), Logik in `public/js/pages/calendar.js`.
- Toolbar mit Container-Auswahl und Terminarten-Filter (`.sm-chip--check` mit `data-sm-color`), Kalender in `.sm-panel`.
- Ansichten Monat / Woche / Liste. Zeiten in Ortszeit. Termin-Modal aus `calendar/_appointment_modal.html.twig`.
- Ereignisfarben aus der Terminart, sonst `--sm-brand`.
- `/calendar/container/{id}` zeigt dieselbe Ansicht fest für einen Container.
- Ohne Anmeldung (die Route ist öffentlich, die Termin-API nicht) erscheint ein Hinweis und keine Bearbeitung.

### `/admin/appointment-types` – Terminarten
- `.sm-grid-aside`: links Tabelle (Terminart als `.sm-type-preview`, Farbe, Icon, Aktionen), rechts Panel „Neue Terminart“.
- Felder in `admin/appointment_type/_form_fields.html.twig` mit Live-Vorschau; Icon-Auswahl im Modal
  (`_icon_picker.html.twig`, Logik in `public/js/pages/appointment-types.js`).
- Farben sind frei wählbar (Farbfeld) und kommen über `data-sm-color` ins Markup.
- Löschen per POST mit CSRF-Token und `data-sm-confirm`.

### `/signage` – Signage-Verwaltung
- `signage/_layout.html.twig` erbt von `swimmonitor/base.html.twig`; die Bereichsnavigation (Geräte, Videos, Playlists,
  Text-Videos, Protokoll) steht im Seitenkopf als Unterstrich-Tabs (`.sg-nav`).
- Kind-Templates füllen `signage_title`, `signage_content` und `signage_javascripts`; `sg_v` ist die Cache-Version.
- Eigene Bausteine `.sg-*` in `public/css/admin/signage.css`. Deren Variablen (`--sg-surface`, `--sg-accent`, …)
  zeigen auf die `--sm-*`-Tokens, damit Hell/Dunkel, Radien und Farben einheitlich sind. Die Datenfarben
  `.sg-c0` … `.sg-c7` (Playlists in Zeitleisten) bleiben als feste Palette.
- Die Geräteseite lädt FullCalendar lokal für den Zeitplan.
- **Erste-Schritte-Führung** (`public/js/admin/signage-tour.js`, Start über „Anleitung“ in `.sg-nav`): führt in zehn
  Schritten von „Video hochladen“ über Playlist und Gerät bis „Veröffentlichen“ und Sofortwiedergabe. Jeder Schritt hebt
  ein vorhandenes Element hervor (`.sg-tour-spot`, die Seite bleibt bedienbar) und erklärt im Popover (`.sg-tour-pop`),
  was zu tun ist und was der Klick auslöst.
  - Seitenzuordnung über `<body data-sg-route="…">` (im `body_attr` von `signage/_layout`), Fortschritt in
    `localStorage` (`sg-tour`), damit die Führung beim Seitenwechsel weiterläuft.
  - Ziele werden über bestehende Klassen und Formular-Aktionen gefunden (`[data-dropzone]`, `.sg-new-card`,
    `.sg-library`, `form[action$="/publish"]` …); Templates brauchen keine Extra-Attribute. Fehlt ein Ziel (Rechte, keine
    Inhalte), erscheint der Schritt mittig. Schritte in Tabs öffnen den Tab vorher.
  - Aktionsschritte warten auf den Klick bzw. das Absenden (bei `data-confirm` erst nach der Bestätigung).
    Neue Schritte: Eintrag in `STEPS` mit `route`, `target`, optional `tab`, `card`, `waitFor`, `skippable`.
  - Position per CSSOM (`style.top/left`), Esc beendet. Ändert sich ein Ziel-Template, die Selektoren in `STEPS` prüfen.

### `/appointments/privacy`, `/appointments/imprint` – Datenschutz, Impressum
- `.sm-page--narrow`, Inhalt in `.sm-panel` mit `.sm-prose.sm-legal`. Ohne Anmeldung erreichbar; für Gäste zeigt die
  Navigationsleiste nur das Logo und „Anmelden“.
- Der Datenschutztext kommt aus `privacy_content.html.twig`, das auch die Buchungsseiten einbinden; dort nichts umbauen.

---

## 10. Umstellung einer Seite – Checkliste

1. Template erbt von `swimmonitor/base.html.twig` (Signage: von `signage/_layout.html.twig`), Breadcrumb gesetzt.
2. Keine `style=`-Attribute, kein `<style>`, kein `<script>` mit Code, keine `on…`-Attribute mehr im Template
   (Suche: `style=|<style|<script>|onclick=|onchange=|onsubmit=`). Auch Skripte erzeugen per `innerHTML` keine
   `style=`- oder `on…`-Attribute.
3. Keine CDN-URLs (`cdn.`, `unpkg`, `jsdelivr`, `cloudflare`, `googleapis`, `code.jquery.com`).
4. Seitenspezifisches CSS nur, wenn kein Baustein passt; dann als neuer Baustein in `swimmonitor.css` und hier dokumentiert.
5. Am besten das ausgelieferte HTML prüfen, nicht nur das Template (eingebundene Partials). Im Dev-Modus bringt die
   Symfony-Debug-Toolbar eigene `<script nonce>`/`<style nonce>` mit; die zählen nicht.
6. Hell und Dunkel geprüft, Breite 1440 px und 390 px geprüft.
7. Changelog-Eintrag in `config/swimmonitor/changelog.yaml` geschrieben.
