# Fraindly Design-System — Regeln für Menschen und Agenten

Referenz: [`../mood.html`](../mood.html). Tokens: [`tokens.css`](tokens.css) (Quelle), [`tokens.json`](tokens.json). Komponenten: [`components.css`](components.css), [`components.js`](components.js).

Fraindly AG ist eine Schweizer Software-Beratungs- und Umsetzungsfirma. Stil: minimalistisch, modern, warme Töne, Swiss Crimson als einziger Akzent, Typografie statt Karten.

## Einbinden

```html
<link rel="stylesheet" href="design/tokens.css">
<link rel="stylesheet" href="design/components.css">
<script src="design/components.js"></script>
<script>FR.init();</script>
```

Theme über `data-theme="light|dark"` auf `<html>`. Vor dem ersten Paint setzen, in `localStorage` unter `fr-theme` speichern.

## Harte Regeln

1. **Keine Cards.** Keine Boxen mit Rahmen, Schatten oder Füllung. Gruppierung über Nähe: Abstand zwischen Gruppen mindestens doppelt so gross wie innerhalb. Trennung über Haarlinien (`1px solid var(--line)`).
2. **Typografie trägt die Hierarchie.** Grösse und Gewicht (300 / 400 / 500), nicht Farbflächen. Keine Eyebrows: keine kleinen Texte, Versalien-Labels oder Nummern («01») über Überschriften und Titeln.
3. **Schrift.** Plus Jakarta Sans für alles. JetBrains Mono ausschliesslich für Zahlen, Zeiten, Kennungen, Code, immer `font-variant-numeric: tabular-nums` (Klasse `fr-num`). Keine Serifen.
4. **Farben nur als Tokens.** Keine Hex-Werte im Code, keine Tailwind-Standardfarben, kein reines Weiss/Schwarz.
5. **Crimson ist Akzent.** Primärbutton, Fokusring, aktiver Tab/Sortierung, Auswahlbalken. Ein Primärbutton pro Ansicht. Nie für Fehler (`--error`) und nie als grosse Fläche.
6. **Form.** Radius `2px` (`--radius`). Schatten nur bei Overlays (`--shadow-overlay`). Pillen nur bei Switch.
7. **Zustand nie nur Farbe.** Immer Icon, Punkt oder Wort dazu.
8. **Bedienbarkeit.** Jede Komponente hat Hover, Fokus (`:focus-visible`, Crimson 2px), Disabled, Laden und Fehler. Alles per Tastatur nutzbar, Touch-Ziel mindestens 40px. `prefers-reduced-motion` respektieren.
9. **Kontrast.** Text mindestens 4.5:1. Muted-Text `--text-muted`, nicht heller.

## Komponenten-Auswahl

| Bedarf | Komponente |
|---|---|
| 2–4 Optionen, alle sichtbar | Radio oder `fr-seg` |
| 5–6 Optionen, keine Suche | `fr-select` |
| Ab 7 Optionen oder mit Suche | `FR.combo(el, { options })` |
| Mehrere Werte, Filter, Tags | `FR.combo(el, { options, multiple: true })` |
| Ab ca. 500 Einträgen | `FR.combo(el, { options: (q, signal) => Promise, minChars: 2 })` |
| Daten in Zeilen | `table.fr-table`, Spalten mit `data-sort="text\|num\|date"` |
| Details bearbeiten | `dialog.fr-drawer` (von rechts), kein zentriertes Modal |
| Rückmeldung | `FR.toast(msg, "info\|success\|error")` oder `fr-alert` |
| Löschen bestätigen | Einzelner Button oder Tabellenzeile: `fr-slide` (Zeile: `fr-slide--fixed`). Folgenreich: `dialog.fr-modal` |

### Löschen bestätigen

**Niemals** native Browser-Dialoge (`confirm()`, `alert()`, `prompt()`) verwenden. Zwei erlaubte Muster:

1. **Slide-Button** (`span.fr-slide` mit `button[data-slide-trigger]`, optional `data-name`): Im Rechteck des Buttons schiebt sich von unten «Sicher?» herein, nach 650 ms folgen Abbrechen (✕, links) und Löschen (Papierkorb, rechts) als Icon-Buttons, ebenfalls von unten. Abbrechen schickt sie nach unten weg, der Button kommt wieder von unten zurück. Nie von oben. Der Button behält seine Grösse, das Layout springt nicht. Icon-Buttons haben hier `aria-label` und `title` statt Tooltip (Overflow ist abgeschnitten), Fokus landet auf Abbrechen, Esc und Klick daneben brechen ab.
   - **Einzelner Button**: Grösse wird beim Klick vom Button übernommen, Icon-Buttons 40px.
   - **Tabellenzeile** (`fr-slide fr-slide--fixed` in `td` mit `data-num`, Zeile mit `data-row` und `data-name`): feste Breite (`--slide-w`, Standard 96px) und Höhe 32px, Trigger ist ein Ghost-Icon-Button rechts, Icon-Buttons 32px. Spaltenbreiten bleiben in jedem Zustand konstant. Die Zeile wird nach Bestätigung entfernt.
2. **Modal-Dialog** (`dialog.fr-modal`, `showModal()`): Für folgenreiche oder unwiderrufliche Aktionen (Projekte, Sammellöschungen, Kaskaden). Titel mit Objektname als Frage, ein Satz zur Folge, Abbrechen mit `autofocus`. Bei Unwiderruflichem Namenseingabe (`data-confirm-match`), Bestätigen bleibt bis dahin deaktiviert.

Der Bestätigen-Button ist `fr-btn--danger fr-btn--solid` und benennt die Aktion («Löschen», «Projekt löschen»), nie «OK» oder «Ja». Danach Toast «… gelöscht.». Ist die Aktion rückgängig machbar, ist ein Toast mit «Rückgängig» besser als jede Bestätigung.

### Toast und Status

Toast-Typen unterscheiden sich über Fläche **und** Icon, nie über einen Akzentstreifen: Info = Charcoal + Info-Icon, Erfolg = Grün (`--success`) + Haken, Fehler = `--error` + Warndreieck (bleibt stehen). Crimson kommt in Toasts nicht vor.

Status-Punkte (`fr-status`) sind 16px gross, so gross wie Radio-Buttons (18px).

### Icon-Buttons

- Nur Icon: `fr-btn fr-btn--icon` (40px, Icon 20px). Pflicht: `aria-label` und `data-tip`. Im Header `data-tip-pos="bottom-end"`, damit der Tooltip nicht abgeschnitten wird.
- Zeilenaktionen in Tabellen und Listen: `fr-btn--icon fr-btn--ghost fr-btn--sm` (32px, Icon 16px).
- Mit Text: Icon links, 16px. Rechts nur Pfeile für «Weiter».
- Umschalter: `data-toggle` mit `aria-pressed`. Laden: `aria-busy="true"` (Icon wird durch Spinner ersetzt).
- Varianten wie bei Text-Buttons: `--primary` (höchstens einer pro Ansicht), `--danger`, `disabled`.
- Theme-Wechsel im Header ist ein Icon-Button (Mond / Sonne), kein Text.

### Combobox

- Eingabe nur mit Unterlinie, Liste mit Haarlinien, Treffer-Teil fett in Crimson-Text.
- Tastatur: ↑ ↓ Home End, Enter wählt, Esc schliesst (zweites Esc leert bei Einzelauswahl), Rücktaste entfernt letzten Token bei Mehrfachauswahl.
- Mehrfach: Text-Token mit ✕ (keine Pillen), «Alle / Keine», Zähler in Mono ab 2 Auswahlen.
- Zustände: Laden (Spinner), «Mindestens n Zeichen», «Keine Treffer», optional «… neu anlegen» (`allowCreate`).
- ARIA: `role="combobox"`, `aria-expanded`, `aria-controls`, `aria-activedescendant`, Liste `role="listbox"`, bei Mehrfach `aria-multiselectable`.
- Ereignis `fr-change` mit `detail.values`. Rendert höchstens 100 Einträge (`maxRender`).

### Tabellen

- Jede Tabelle sortierbar: aufsteigend, absteigend, ursprünglich. `aria-sort` am `th`. Aktive Spalte mit Crimson-Pfeil.
- Zahlen rechtsbündig (`data-num`), Mono, Einheit im Wert (`12 ms`). Zahlenwert zum Sortieren in `data-v`.
- Kopfzeile sticky im Scroll-Container (`fr-table-wrap`), Zeilenzähler, Leerzustand mit Ausweg.
- Auswahl: `data-selectable`, Zeile erhält Crimson-Balken links.

## Content

- Deutsch (Schweiz), Sie-Form, aktiv, ein Gedanke pro Satz. «ss» statt «ß». Anführungszeichen «…» oder „…“ konsistent.
- Haltung: beratend, direkt, ruhig. Keine Superlative, keine Buzzwords, keine Emojis, keine Ausrufezeichen.
- Headline höchstens 8 Wörter. Button 1–3 Wörter, Verb zuerst. Absatzbreite höchstens 65 Zeichen.
- Formate: Datum `05.11.2026`, Zeit `14:30`, Tausender `1’024`, Währung `CHF 12’500.00`, Prozent `99.98 %`.
- Fehlertexte: was ist passiert, was kann die Person tun. Kein «Oops».

## Layout

### Marketing- und Inhaltsseiten

12 Spalten, maximal 1280px, linksbündig und asymmetrisch (Titel links 3 Spalten, Inhalt rechts 9). Abstände nur aus `--s-1` bis `--s-6`. Breakpoints 900px (eine Spalte) und 700px (alles gestapelt).

### App-Layout (Standard für Anwendungen)

Gilt für Dashboards, Verwaltung und alle Produkt-Oberflächen, die Daten in mehreren Bereichen zeigen. Die Regeln sind verbindlich, bevor eigene Varianten entstehen. Das App-Layout ist nicht an `--page-max` gebunden und füllt die Höhe des Viewports.

**Navigation (links)**

- Die Navigation steht links, der Inhalt rechts. Die Trennung erfolgt über eine Haarlinie (`1px solid var(--line)`), nicht über eine Fläche.
- Oben in der Navigation steht der angemeldete Benutzer: Avatar, Name, Rolle oder Mandant (`fr-small fr-muted`). Avatar ist ein Quadrat mit `--radius` und Initialen, kein Kreis. Das Benutzermenü (Profil, Abmelden) öffnet sich von hier. Darunter folgt Abstand `--s-5`, keine Linie. Die Nähe trennt Benutzer und Einträge, eine Linie würde die Gruppe zerschneiden.
- Einträge bestehen aus Icon (20px) und Text. Gruppen trennen sich durch `--s-4` Abstand und Haarlinie. Keine Überschriften in Versalien über Gruppen (Eyebrows).
- Aktiver Eintrag: Gewicht 500 und `--text`, zusätzlich ein Crimson-Balken links (2px). Der Balken ist das Auswahlbalken-Muster und zählt als einziger Akzent der Navigation.
- Einklappen: Ein Icon-Button (40px) am Kopf der Navigation schaltet zwischen Vollansicht (`--app-nav-w`, 240px) und Icon-Leiste (`--app-nav-w-collapsed`, 56px). Button hat `aria-expanded` und `data-tip`.
- In der Icon-Leiste bleiben Benutzer-Avatar und Icons sichtbar, Text fällt weg. Jedes Icon hat `aria-label` und `data-tip` mit dem Namen des Eintrags.
- Die Navigationsbreite ist fest. Sie lässt sich nicht ziehen, nur ein- und ausklappen.
- Unter 900px Viewport-Breite startet die Navigation eingeklappt, solange keine Einstellung gespeichert ist. Unter 700px ist die Navigation ein Overlay (Drawer-Muster), geöffnet über einen Icon-Button im Kopf.
- Der Zustand (aufgeklappt / eingeklappt) wird im Browser gespeichert (`localStorage`, Schlüssel `fr-app-nav`). Keine Speicherung auf dem Server.

**Spalten (Inhalt)**

- Standard ist **Master/Detail**: Spalte 1 zeigt die Liste der Objekte (Master), Spalte 2 das Detail des gewählten Eintrags. Die Auswahl ändert nur das Detail, die Liste bleibt stehen. Der gewählte Eintrag ist in der Liste markiert (Crimson-Balken, `aria-current="true"`). Status, Einstellungen und Verlauf eines Objekts gehören ins Detail, nicht in eine eigene Spalte neben dem Detail. Der Detail-Titel nennt den gewählten Eintrag.
- Eine dritte Spalte ist nur für Nebeninformationen erlaubt (z. B. Protokoll, Kontext) und erst ab 1280px. Breakpoints: 2 Spalten (Master und Detail) von 900px bis 1279px, 3 Spalten ab 1280px Viewport-Breite. Darunter steht eine Spalte: Die Liste allein, ein Klick öffnet das Detail als eigene Seite mit Zurück-Schaltfläche.
- Jede Spalte hat einen Titel (`fr-subhead`). Spalten sind Flächen ohne Rahmen, Schatten oder Füllung. Die Trennung erfolgt über Haarlinien.
- Zwischen zwei Spalten liegt ein Trenner: `role="separator"`, `aria-orientation="vertical"`, `aria-valuenow`, `aria-valuemin`, `aria-valuemax`, `tabindex="0"`. Die sichtbare Linie ist 1px `--line`, bei Hover und Fokus `--line-strong`. Fokus mit Crimson-Ring (2px). Cursor `col-resize`.
- Der Ziehbereich ist mindestens 40px breit (`--app-handle-hit`), unsichtbar um die Linie herum. Touch-Ziele gelten wie überall (Regel 8).
- **Horizontal verziehbar:** Jeder Trenner ist immer verziehbar, solange mehr als eine Spalte sichtbar ist. Beim Ziehen ändern sich nur die beiden angrenzenden Spalten, die Summe bleibt gleich. Keine Spalte wird schmaler als `--app-col-min` (280px).
- Tastatur: ← und → verschieben den Trenner um 16px, mit Shift um 64px. Doppelklick auf den Trenner oder Taste Pos1 setzt die Standardbreiten zurück.
- **Breiten merken:** Die Breiten werden als Anteile gespeichert (Summe 1), nicht als Pixel, damit sie bei Fenstergrössenänderung passen. Schlüssel `fr-app-cols:<seite>:<spalten>`, also pro Seite und Spaltenanzahl. Beim Laden gespeicherte Anteile anwenden, sonst Standard. Ungültige oder fehlende Werte fallen auf den Standard zurück.
- Unter 900px gibt es keine Trenner und keine gespeicherten Breiten, alle Spalten stehen untereinander.
- `prefers-reduced-motion` gilt für das Ein- und Ausklappen der Navigation.

Darstellung und Schema: [`../mood.html`](../mood.html), Abschnitt «Layout». Werte: `tokens.json`, Schlüssel `layout.app`.

## Vor dem Abliefern prüfen

- [ ] Keine Cards, keine fremden Farben, keine Serifen.
- [ ] Zahlen in Mono und rechtsbündig in Tabellen.
- [ ] Tabellen sortierbar, Auswahlen als Combobox ab 7 Optionen.
- [ ] Anwendungen: App-Layout mit einklappbarer Navigation, Master/Detail, verziehbaren Spalten und gespeicherten Breiten.
- [ ] Fokus sichtbar, Tastatur getestet, Dark Mode geprüft.
- [ ] Text in Schweizer Rechtschreibung.
