# CluPilot — verbindliche Regeln (Repo-Teil) Die **Vollfassung der Regeln R1–R17 liegt beim Nutzer**, nicht im Repo. Die Kurzfassung steht in `docs/handoffs/2026-07-25-clupilot-state-handoff.md` §9. Bei Konflikt: **STOP & fragen.** Diese Datei hält die Regeln fest, die aus konkreten Fehlern im laufenden Betrieb entstanden sind — sie sind nicht verhandelbar und werden per Test erzwungen. --- ## R18 — Icon-Größe und Zeilenumbruch **Ein Icon steht neben seinem Text, nie darüber, und nie größer als bestellt.** Verboten: 1. **Icon zwingt den Text auf eine zweite Zeile.** Ein Navigationseintrag, ein Button, ein Tabellen-Action ist **einzeilig**. Zwei Zeilen sind nur erlaubt, wenn der Text selbst bewusst zweizeilig gesetzt ist (Label + Unterzeile) — dann steht das Icon links davon, nicht darüber. 2. **Icon größer als die Zeile, in der es sitzt.** Standard ist `size-5` (20px) in der Navigation, `size-4` (16px) in Buttons, Tabellen und Fließtext. Größer nur, wenn es als eigenständiges Element gemeint ist (Leerzustand, Statusplakette). ### Warum das zweimal schiefging - **Zeilenumbruch.** Tailwinds Preflight setzt `svg { display: block }`. Ein Icon in einem *inline*-Elternteil schiebt den folgenden Text damit auf die nächste Zeile. Genau so wurde aus dem Eintrag „Zugangsdaten" ein doppelt so hoher Kasten mit Schloss oben und Wort darunter. - **Größe.** `.size-4` und `.size-5` haben **dieselbe Spezifität**, also entscheidet die Reihenfolge im Stylesheet — und Tailwind gibt `.size-4` *vor* `.size-5` aus. Eine Komponente, die `size-5` bedingungslos mitmergt, überstimmt damit **jedes** `class="size-4"` am Aufrufort. Alle so geschriebenen Icons liefen still auf 20px. ### Wie es jetzt gebaut ist - `resources/views/components/ui/icon.blade.php` setzt seine Standardgröße **nur**, wenn der Aufrufort keine `size-`/`w-`/`h-`-Klasse mitgibt, und rendert `inline-block shrink-0 align-middle` statt des Preflight-`block`. - `resources/views/components/ui/nav-item.blade.php` legt Label und Icon in eine eigene Flex-Zeile, damit ein Icon auch im falschen Slot daneben landet. - Icons in `` gehören in ``, nicht in den Default-Slot. ### Erzwungen durch `tests/Feature/IconLayoutTest.php` — Größe des Aufruforts gewinnt, Icon bleibt `inline-block`, Nav-Eintrag bleibt einzeilig aus beiden Slots, und kein Blade-File im Repo darf ein Icon am Icon-Slot vorbeischmuggeln. --- ## R19 — Zeitzone: gespeichert in UTC, angezeigt auf der Wanduhr **Jede Zeit, die ein Mensch liest, geht vorher durch `->local()`.** Verboten: 1. **Einen gespeicherten Zeitstempel direkt formatieren.** `$model->created_at->isoFormat(…)` liefert UTC. Richtig ist `$model->created_at->local()->isoFormat(…)`. 2. **`->timezone(config('app.timezone'))`.** Das ist die *Speicher*zone und bleibt UTC — der Aufruf sieht aus wie eine Umrechnung und ist keine. 3. **Ein `datetime-local`-Feld nur in eine Richtung behandeln.** Rein und raus gehören zusammen: `LocalTime::toField()` und `LocalTime::fromField()`. Ein Feld hat keine Zeitzone; es sind die Ziffern, die jemand auf der eigenen Uhr abliest. Ausgenommen: `diffForHumans()` ist relativ und in jeder Zone gleich. ### Warum das durchgerutscht ist Die Konsole kündigte eine Aktualisierung „spätestens um 15:21" an, während die Uhr 17:21 zeigte. Zwei der vierzehn betroffenen Ansichten sahen sogar behandelt aus — sie riefen `->timezone(config('app.timezone'))`, was sich liest wie „in Ortszeit umrechnen" und, weil diese Zone UTC ist, nichts tut. Eine Attrappe ist schlimmer als gar kein Aufruf: sie hält den Nächsten vom Nachsehen ab. Schlimmer als die Anzeigen waren zwei **Formulare**: Wartungsfenster und Paketversionen füllten ihre Felder mit UTC und lasen sie als UTC zurück. Ein eingetragenes „21:00" wurde zu 23:00 Ortszeit. Und die Tests deckten es nicht auf, weil sie den erwarteten Wert **mit demselben falschen Aufruf** bildeten. Ein Test, der die Implementierung nachrechnet, prüft nichts. ### Wie es jetzt gebaut ist - `config('app.display_timezone')` (`APP_DISPLAY_TIMEZONE`, Vorgabe `Europe/Vienna`) getrennt von `app.timezone`, das UTC bleibt. - `->local()` als Carbon-Makro in `AppServiceProvider`. Es **kopiert** vor dem Umstellen: `Illuminate\Support\Carbon` ist mutabel, sonst schriebe das bloße Anzeigen das Modellattribut um. Beide Klassen bekommen denselben Rumpf — Carbon führt Makros in *einer* globalen Tabelle, die zweite Registrierung ersetzt die erste für alle. - `App\Support\LocalTime` hält beide Feldrichtungen nebeneinander, damit niemand eine ändert, ohne die andere zu sehen. ### Erzwungen durch `tests/Feature/DisplayTimezoneTest.php` — kein Blade und kein Livewire-Bauteil darf absolut formatieren ohne `->local()`, die UTC-Attrappe ist verboten, Speicherzone bleibt UTC, Sommer- **und** Winterzeit werden geprüft, `->local()` verändert das Original nicht, und der Feld-Round-Trip kommt als derselbe Zeitpunkt zurück. --- ## R20 — Bearbeiten passiert im Modal, nie in der Zeile **Sobald etwas Eingabefelder hat, geht ein Modal auf.** Verboten: 1. **Inline-Bearbeitung in einer Tabellenzeile.** Kein ``, kein `