testing gitignore
This commit is contained in:
parent
40f87f2fc0
commit
72e0666f33
4 changed files with 6113 additions and 0 deletions
|
|
@ -0,0 +1,897 @@
|
|||
# AHK-Ehrenamtsmanagement — Entscheidungsprotokoll und bisheriger Aufbau
|
||||
|
||||
> **Stand:** 2026-09-16
|
||||
> **Projekt:** AHK-Ehrenamtsmanagement
|
||||
> **Technischer Name:** `ahk-ehrenamtsmanagement`
|
||||
> **Status:** Greenfield-Neustart; Phase 0 vollständig geplant, Phase 1 vollständig entschieden, Phase 2 begonnen
|
||||
> **Verbindliche Grundlage:** `PROJECT_DESIGN.md`
|
||||
> **Visuelle Referenz:** `PROJECT_DESIGN_VOR_ORT_MOCKUP.png`
|
||||
> **Arbeitsprinzip:** Wenn `PROJECT_DESIGN.md` etwas nicht entscheidet, wird nicht geraten, sondern bewusst gemeinsam entschieden.
|
||||
|
||||
---
|
||||
|
||||
# 1. Grundsatzentscheidungen
|
||||
|
||||
## 1.1 Greenfield statt Legacy-Umbau
|
||||
|
||||
Der bestehende Prototyp wird **nicht migriert**.
|
||||
|
||||
- kein erhaltenswerter Produktivdatenbestand
|
||||
- nur ein einzelner Testnutzer ohne relevante Inhalte
|
||||
- Ziel ist ausdrücklich, den Aufbau der App von Grund auf zu lernen
|
||||
- kein Legacy-Code wird nur deshalb übernommen, weil er bereits existiert
|
||||
- keine Legacy-Datenmigration
|
||||
- keine Kompatibilitätsschicht
|
||||
- keine Datenrettung aus der alten SQLite-Datenbank
|
||||
|
||||
Falls alte Implementierungen später als Referenz dienen, werden sie zuerst gegen `PROJECT_DESIGN.md` geprüft.
|
||||
|
||||
## 1.2 Projektname
|
||||
|
||||
Sichtbarer Produktname:
|
||||
|
||||
```text
|
||||
AHK-Ehrenamtsmanagement
|
||||
```
|
||||
|
||||
Technischer Name:
|
||||
|
||||
```text
|
||||
ahk-ehrenamtsmanagement
|
||||
```
|
||||
|
||||
Der technische Name soll konsistent für Repository, App-Verzeichnis, Stack-Verzeichnis und Container verwendet werden.
|
||||
|
||||
## 1.3 Entwicklungsumgebung
|
||||
|
||||
```text
|
||||
Betriebssystem: EndeavourOS / Arch Linux
|
||||
Shell: Bash
|
||||
Python-Verwaltung: pyenv
|
||||
Virtuelle Umgebungen: pyenv-virtualenv
|
||||
Python-Serie: 3.14
|
||||
```
|
||||
|
||||
Fish ist nicht mehr Standardshell.
|
||||
|
||||
## 1.4 Python-Environment
|
||||
|
||||
Eigenes pyenv-virtualenv:
|
||||
|
||||
```text
|
||||
ahk-ehrenamtsmanagement
|
||||
```
|
||||
|
||||
Projektbindung:
|
||||
|
||||
```bash
|
||||
pyenv local ahk-ehrenamtsmanagement
|
||||
```
|
||||
|
||||
`.python-version` wird versioniert. Das eigentliche Virtualenv liegt außerhalb des Repositories in der pyenv-Verwaltung.
|
||||
|
||||
---
|
||||
|
||||
# 2. Phase 0 — Greenfield-Projektbasis
|
||||
|
||||
## Ziel
|
||||
|
||||
Am Ende von Phase 0 existiert eine minimale, lauffähige und getestete Flask-Anwendung.
|
||||
|
||||
Noch nicht enthalten:
|
||||
|
||||
- Datenbankmodelle
|
||||
- Auth
|
||||
- Organisationen
|
||||
- Teams
|
||||
- Rollen
|
||||
- Templates
|
||||
- Branding
|
||||
- Docker
|
||||
- Fachmodule
|
||||
|
||||
## 2.1 Git-Grundlage
|
||||
|
||||
Initiale Struktur:
|
||||
|
||||
```text
|
||||
ahk-ehrenamtsmanagement/
|
||||
├── PROJECT_DESIGN.md
|
||||
├── PROJECT_DESIGN_VOR_ORT_MOCKUP.png
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
Initialisierung:
|
||||
|
||||
```bash
|
||||
mkdir ahk-ehrenamtsmanagement
|
||||
cd ahk-ehrenamtsmanagement
|
||||
git init
|
||||
```
|
||||
|
||||
Minimale `.gitignore`:
|
||||
|
||||
```gitignore
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Optional local virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# Local application data
|
||||
instance/
|
||||
|
||||
# Environment / secrets
|
||||
.env
|
||||
|
||||
# OS / editor noise
|
||||
.DS_Store
|
||||
```
|
||||
|
||||
Baseline-Commit:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git status
|
||||
git commit -m "Initialize AHK-Ehrenamtsmanagement project"
|
||||
```
|
||||
|
||||
Ein Remote ist in Phase 0 nicht Pflicht.
|
||||
|
||||
## 2.2 Abhängigkeiten
|
||||
|
||||
Runtime und Development werden getrennt.
|
||||
|
||||
`requirements.txt`:
|
||||
|
||||
```text
|
||||
Flask
|
||||
```
|
||||
|
||||
`requirements-dev.txt`:
|
||||
|
||||
```text
|
||||
-r requirements.txt
|
||||
|
||||
pytest
|
||||
```
|
||||
|
||||
Installation:
|
||||
|
||||
```bash
|
||||
python -m pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
Bevorzugte Form:
|
||||
|
||||
```text
|
||||
python -m pip ...
|
||||
```
|
||||
|
||||
## 2.3 Erste Flask-Struktur
|
||||
|
||||
Verbindlich:
|
||||
|
||||
- Flask App Factory
|
||||
- Blueprints
|
||||
- eigener Core-Bereich für `/health`
|
||||
|
||||
Struktur:
|
||||
|
||||
```text
|
||||
app/
|
||||
├── __init__.py
|
||||
└── core/
|
||||
├── __init__.py
|
||||
└── health/
|
||||
├── __init__.py
|
||||
└── routes.py
|
||||
```
|
||||
|
||||
`app/core/health/__init__.py`:
|
||||
|
||||
```python
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint("health", __name__)
|
||||
|
||||
from app.core.health import routes
|
||||
```
|
||||
|
||||
`app/core/health/routes.py`:
|
||||
|
||||
```python
|
||||
from app.core.health import bp
|
||||
|
||||
@bp.get("/health")
|
||||
def health():
|
||||
return {"status": "ok"}, 200
|
||||
```
|
||||
|
||||
Initiale `app/__init__.py`:
|
||||
|
||||
```python
|
||||
from flask import Flask
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
|
||||
from app.core.health import bp as health_bp
|
||||
app.register_blueprint(health_bp)
|
||||
|
||||
return app
|
||||
```
|
||||
|
||||
Start:
|
||||
|
||||
```bash
|
||||
flask --app 'app:create_app' run --debug
|
||||
```
|
||||
|
||||
Healthcheck:
|
||||
|
||||
```bash
|
||||
curl -i http://127.0.0.1:5000/health
|
||||
```
|
||||
|
||||
Erwartung:
|
||||
|
||||
```json
|
||||
{"status": "ok"}
|
||||
```
|
||||
|
||||
mit HTTP 200.
|
||||
|
||||
## 2.4 Erste Testbasis
|
||||
|
||||
`tests/test_health.py`:
|
||||
|
||||
```python
|
||||
from app import create_app
|
||||
|
||||
def test_health_endpoint():
|
||||
app = create_app()
|
||||
client = app.test_client()
|
||||
|
||||
response = client.get("/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.get_json() == {"status": "ok"}
|
||||
```
|
||||
|
||||
Ausführen:
|
||||
|
||||
```bash
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 3. Phase 1 — Technisches App-Fundament
|
||||
|
||||
## Reihenfolge
|
||||
|
||||
```text
|
||||
1.1 Architektur-Gerüst
|
||||
1.2 Konfigurationssystem
|
||||
1.3 Flask Extensions
|
||||
1.4 SQLAlchemy + SQLite
|
||||
1.5 Flask-Migrate / Alembic
|
||||
1.6 Teststruktur / Fixtures
|
||||
1.7 Foundation-Tests
|
||||
1.8 Checkpoint
|
||||
```
|
||||
|
||||
## 3.1 Architektur-Gerüst
|
||||
|
||||
Die vollständige Ordnerstruktur wird als Gerüst angelegt.
|
||||
|
||||
Wichtig:
|
||||
- nur Ordner/Pakete vorbereiten
|
||||
- nicht vorsorglich überall `models.py`, `routes.py`, `services.py`, `permissions.py` oder `forms.py` anlegen
|
||||
- solche Dateien entstehen erst bei echter Implementierung
|
||||
|
||||
```text
|
||||
app/
|
||||
├── __init__.py
|
||||
├── core/
|
||||
│ ├── __init__.py
|
||||
│ ├── health/
|
||||
│ ├── auth/
|
||||
│ ├── accounts/
|
||||
│ ├── organizations/
|
||||
│ ├── teams/
|
||||
│ ├── memberships/
|
||||
│ ├── invitations/
|
||||
│ ├── permissions/
|
||||
│ ├── branding/
|
||||
│ ├── storage/
|
||||
│ ├── notifications/
|
||||
│ ├── dashboard/
|
||||
│ ├── profiles/
|
||||
│ └── audit/
|
||||
├── modules/
|
||||
│ ├── __init__.py
|
||||
│ ├── scheduling/
|
||||
│ ├── documents/
|
||||
│ └── chat/
|
||||
├── templates/
|
||||
│ ├── core/
|
||||
│ └── components/
|
||||
└── static/
|
||||
├── css/
|
||||
├── js/
|
||||
└── avatars/
|
||||
|
||||
tests/
|
||||
instance/
|
||||
```
|
||||
|
||||
Git speichert keine leeren Ordner. Keine künstliche `.gitkeep`-Orgie.
|
||||
|
||||
## 3.2 `config.py`
|
||||
|
||||
Liegt im **Projektroot**.
|
||||
|
||||
Zunächst:
|
||||
|
||||
```python
|
||||
class Config:
|
||||
pass
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
DEBUG = True
|
||||
|
||||
class TestingConfig(Config):
|
||||
TESTING = True
|
||||
```
|
||||
|
||||
Noch nicht vorsorglich anlegen:
|
||||
- `ProductionConfig`
|
||||
- `SECRET_KEY`
|
||||
- SMTP
|
||||
- Uploadlimits
|
||||
- Auth-Zeiten
|
||||
- Polling-Intervalle
|
||||
|
||||
## 3.3 App-Factory-Interface
|
||||
|
||||
Festgelegt:
|
||||
|
||||
```python
|
||||
def create_app(
|
||||
config_class=DevelopmentConfig,
|
||||
config_overrides=None,
|
||||
):
|
||||
...
|
||||
```
|
||||
|
||||
Reihenfolge:
|
||||
|
||||
```text
|
||||
Factory starten
|
||||
→ Config-Klasse laden
|
||||
→ optionale Overrides
|
||||
→ Extensions initialisieren
|
||||
→ Blueprints registrieren
|
||||
→ später Module registrieren
|
||||
```
|
||||
|
||||
Sinngemäß:
|
||||
|
||||
```python
|
||||
from flask import Flask
|
||||
from config import DevelopmentConfig
|
||||
|
||||
def create_app(
|
||||
config_class=DevelopmentConfig,
|
||||
config_overrides=None,
|
||||
):
|
||||
app = Flask(__name__)
|
||||
|
||||
app.config.from_object(config_class)
|
||||
|
||||
if config_overrides:
|
||||
app.config.from_mapping(config_overrides)
|
||||
|
||||
return app
|
||||
```
|
||||
|
||||
## 3.4 Zentrale Flask-Extensions
|
||||
|
||||
Datei:
|
||||
|
||||
```text
|
||||
app/extensions.py
|
||||
```
|
||||
|
||||
Nur Extension-Instanzen, keine Models, keine Businesslogik, keine Konfiguration.
|
||||
|
||||
Zunächst:
|
||||
|
||||
```python
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
```
|
||||
|
||||
Später:
|
||||
|
||||
```python
|
||||
from flask_migrate import Migrate
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
```
|
||||
|
||||
In der Factory:
|
||||
|
||||
```python
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
```
|
||||
|
||||
## 3.5 SQLite-Strategie
|
||||
|
||||
Development:
|
||||
|
||||
```text
|
||||
instance/app.db
|
||||
```
|
||||
|
||||
Sinngemäß:
|
||||
|
||||
```python
|
||||
SQLALCHEMY_DATABASE_URI = "sqlite:///app.db"
|
||||
```
|
||||
|
||||
Tests:
|
||||
- pro Test eigene temporäre echte SQLite-Datei
|
||||
- keine In-Memory-Datenbank
|
||||
- keine gemeinsam genutzte Testdatenbank
|
||||
- nie `instance/app.db`
|
||||
|
||||
```text
|
||||
DevelopmentConfig → instance/app.db
|
||||
TestingConfig → temporäre SQLite-Datei pro Test
|
||||
Production später → /app/instance/app.db
|
||||
```
|
||||
|
||||
## 3.6 Flask-Migrate / Alembic
|
||||
|
||||
Runtime-Abhängigkeiten:
|
||||
|
||||
```text
|
||||
Flask
|
||||
Flask-SQLAlchemy
|
||||
Flask-Migrate
|
||||
```
|
||||
|
||||
Initialisierung bereits in Phase 1:
|
||||
|
||||
```bash
|
||||
flask --app 'app:create_app' db init
|
||||
```
|
||||
|
||||
Ergebnis:
|
||||
|
||||
```text
|
||||
migrations/
|
||||
├── alembic.ini
|
||||
├── env.py
|
||||
├── script.py.mako
|
||||
└── versions/
|
||||
```
|
||||
|
||||
Festgelegt:
|
||||
- `migrations/` wird versioniert
|
||||
- keine künstliche leere erste Migration
|
||||
- erste echte Migration erst mit erstem Domain-Model in Phase 2
|
||||
- `db.create_all()` ist keine Produktionsstrategie
|
||||
|
||||
## 3.7 pytest-Fixtures
|
||||
|
||||
```text
|
||||
tests/
|
||||
├── conftest.py
|
||||
└── test_health.py
|
||||
```
|
||||
|
||||
In Phase 1 nur:
|
||||
- `app`
|
||||
- `client`
|
||||
- temporäre SQLite-Testdatenbank
|
||||
|
||||
Noch keine Domain-Fixtures.
|
||||
|
||||
## 3.8 Foundation-Tests
|
||||
|
||||
Mindestens:
|
||||
|
||||
1. `/health` liefert 200 und `{"status": "ok"}`
|
||||
2. `TestingConfig` ist aktiv
|
||||
3. `config_overrides` überschreibt Config-Werte
|
||||
4. SQLAlchemy erreicht die temporäre SQLite-DB (`SELECT 1`)
|
||||
5. Tests verwenden niemals `instance/app.db`
|
||||
|
||||
## 3.9 Phase-1-Checkpoint
|
||||
|
||||
Phase 1 ist fertig, wenn:
|
||||
|
||||
- Architektur-Gerüst vorhanden
|
||||
- Root-`config.py` vorhanden
|
||||
- Factory-Interface umgesetzt
|
||||
- `app/extensions.py` vorhanden
|
||||
- SQLAlchemy integriert
|
||||
- Development-SQLite auf `instance/app.db`
|
||||
- Tests je eigene temporäre SQLite-Datei
|
||||
- Flask-Migrate/Alembic initialisiert
|
||||
- keine künstliche Migration erzeugt
|
||||
- `/health` funktioniert
|
||||
- alle Foundation-Tests grün
|
||||
- keine Secrets oder lokalen DB-Daten versioniert
|
||||
- sauber committed
|
||||
|
||||
Beispiel:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git status
|
||||
git commit -m "Establish application foundation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 4. Phase 2 — Plattform-Core / Datenmodell
|
||||
|
||||
## Reihenfolge
|
||||
|
||||
```text
|
||||
2.1 Organization
|
||||
2.2 Account
|
||||
2.3 BrandProfile
|
||||
2.4 Team
|
||||
2.5 OrganizationMembership
|
||||
2.6 TeamMembership
|
||||
2.7 Permission-Grundlagen
|
||||
2.8 TeamInvitation
|
||||
2.9 OrganizationAdminInvitation
|
||||
2.10 TeamModule
|
||||
2.11 AuditLog
|
||||
2.12 Core-Integritätstests
|
||||
2.13 Checkpoint
|
||||
```
|
||||
|
||||
`BrandProfile` wird bewusst **vor `Team`** gebaut, damit `Team.brand_profile_id` von Anfang an auf ein reales Modell zeigt.
|
||||
|
||||
---
|
||||
|
||||
# 5. Globale Model-Konventionen
|
||||
|
||||
## 5.1 Primary Keys
|
||||
|
||||
Normale Domain-Models verwenden:
|
||||
|
||||
```text
|
||||
Integer Primary Keys
|
||||
```
|
||||
|
||||
Opaque/kryptographische Werte werden dort verwendet, wo sie tatsächlich nötig sind, z. B. Auth-Tokens oder Storage Keys.
|
||||
|
||||
## 5.2 Statusfelder
|
||||
|
||||
Begrenzte Statusfelder:
|
||||
- Python-Enums
|
||||
- klare Stringwerte in der Datenbank
|
||||
|
||||
Beispiele:
|
||||
|
||||
```text
|
||||
active
|
||||
archived
|
||||
disabled
|
||||
```
|
||||
|
||||
## 5.3 Zeitstempel
|
||||
|
||||
Global:
|
||||
- intern UTC
|
||||
- Python-Seite timezone-aware
|
||||
- lokale Zeitzone erst bei Darstellung
|
||||
- keine lokale Serverzeit als Domain-Wahrheit
|
||||
|
||||
---
|
||||
|
||||
# 6. `Organization`
|
||||
|
||||
Beschlossen:
|
||||
|
||||
```text
|
||||
Organization
|
||||
├── id Integer PK
|
||||
├── name required
|
||||
├── slug required, unique, stabil
|
||||
├── status active | archived
|
||||
└── created_at UTC timestamp
|
||||
```
|
||||
|
||||
## Slug
|
||||
|
||||
Ergänzt, weil Organisations-URLs Slugs verwenden:
|
||||
|
||||
```text
|
||||
Aidshilfe Köln e.V.
|
||||
→ aidshilfe-koeln
|
||||
```
|
||||
|
||||
URL:
|
||||
|
||||
```text
|
||||
/org/aidshilfe-koeln/manage/
|
||||
```
|
||||
|
||||
Der Slug ist eine **stabile technische Kennung** und folgt Namensänderungen nicht automatisch.
|
||||
|
||||
## Betriebsrealität
|
||||
|
||||
Die Architektur behält `Organization`, aber der reale Betrieb ist auf genau eine Organisation ausgerichtet:
|
||||
|
||||
```text
|
||||
Aidshilfe Köln e.V.
|
||||
```
|
||||
|
||||
Es ist nicht geplant, die App anderen Aidshilfen oder externen Organisationen bereitzustellen.
|
||||
|
||||
---
|
||||
|
||||
# 7. `Account`
|
||||
|
||||
Beschlossen:
|
||||
|
||||
```text
|
||||
Account
|
||||
├── id Integer PK
|
||||
├── email required, unique, normalized
|
||||
├── display_name optional
|
||||
├── status
|
||||
│ ├── active
|
||||
│ └── disabled
|
||||
├── created_at UTC
|
||||
└── last_login_at optional, UTC
|
||||
```
|
||||
|
||||
## E-Mail-Normalisierung
|
||||
|
||||
Beispiel:
|
||||
|
||||
```text
|
||||
" Patsy@Example.DE "
|
||||
→ "patsy@example.de"
|
||||
```
|
||||
|
||||
Regeln:
|
||||
- Whitespace entfernen
|
||||
- Groß-/Kleinschreibung für Account-Identität nicht unterscheiden
|
||||
- gleiche Normalisierung vor Suche und Speicherung
|
||||
- zentrale Normalisierungsfunktion
|
||||
|
||||
## Status
|
||||
|
||||
Neue verifizierte/eingeladene Accounts starten als:
|
||||
|
||||
```text
|
||||
active
|
||||
```
|
||||
|
||||
Kein zusätzlicher `pending`-Accountstatus.
|
||||
|
||||
## `display_name`
|
||||
|
||||
Optional, weil Account vor abgeschlossenem Onboarding existieren kann.
|
||||
|
||||
## `last_login_at`
|
||||
|
||||
Initial `NULL`.
|
||||
|
||||
Nur nach erfolgreicher Authentifizierung aktualisieren, nicht bei jedem Seitenaufruf oder bei normaler Nutzung einer gültigen Device Session.
|
||||
|
||||
## `disabled`
|
||||
|
||||
Wirkt sofort:
|
||||
|
||||
```text
|
||||
Account → disabled
|
||||
→ alle aktiven DeviceSessions widerrufen
|
||||
→ bestehende Sitzungen verlieren Zugriff
|
||||
→ neue Logins nicht zugelassen
|
||||
```
|
||||
|
||||
Bei späterer Reaktivierung:
|
||||
- alte Sessions bleiben widerrufen
|
||||
- neue Authentifizierung nötig
|
||||
- neue Device Session wird erzeugt
|
||||
|
||||
---
|
||||
|
||||
# 8. `BrandProfile`
|
||||
|
||||
Aktueller Stand:
|
||||
|
||||
```text
|
||||
BrandProfile
|
||||
├── id
|
||||
├── organization_id
|
||||
├── name
|
||||
├── logo_storage_key optional
|
||||
├── primary_color
|
||||
├── primary_strong
|
||||
├── accent_color
|
||||
├── accent_strong
|
||||
├── heading_font
|
||||
└── body_font
|
||||
```
|
||||
|
||||
## 8.1 Name
|
||||
|
||||
Da die App real nur für eine Organisation betrieben wird:
|
||||
|
||||
```text
|
||||
name
|
||||
```
|
||||
|
||||
wird global eindeutig gehalten.
|
||||
|
||||
`organization_id` bleibt trotzdem Teil des Architekturmodells.
|
||||
|
||||
## 8.2 Logo
|
||||
|
||||
Logo ist optional.
|
||||
|
||||
Gespeichert wird:
|
||||
|
||||
```text
|
||||
logo_storage_key
|
||||
```
|
||||
|
||||
Kein Dateipfad, kein Originaldateiname.
|
||||
|
||||
Der zentrale Storage-Service löst den opaque Key später auf.
|
||||
|
||||
## 8.3 Farben
|
||||
|
||||
Validierte und normalisierte HEX-Strings:
|
||||
|
||||
```text
|
||||
#RRGGBB
|
||||
```
|
||||
|
||||
Beispiele:
|
||||
|
||||
```text
|
||||
#00858A
|
||||
#005F61
|
||||
#52E0CF
|
||||
#2596BE
|
||||
```
|
||||
|
||||
Nicht als freie Brandwerte:
|
||||
- `rgb(...)`
|
||||
- `rgba(...)`
|
||||
- `hsl(...)`
|
||||
- `#RRGGBBAA`
|
||||
- beliebige CSS-Ausdrücke
|
||||
|
||||
Semantische Ableitungen und Transparenzen entstehen im zentralen Designsystem.
|
||||
|
||||
## 8.4 Fonts
|
||||
|
||||
Font-Auswahl über Code-Registry.
|
||||
|
||||
Die Datenbank speichert nur stabile Keys:
|
||||
|
||||
```text
|
||||
heading_font = "big-shoulders-display"
|
||||
body_font = "noto-sans"
|
||||
```
|
||||
|
||||
Keine freien Fontnamen, URLs oder CSS-Fragmente.
|
||||
|
||||
## 8.5 Font-Auslieferung
|
||||
|
||||
Fonts werden lokal mit der Anwendung ausgeliefert.
|
||||
|
||||
Keine Pflichtabhängigkeit von Google Fonts, CDN oder anderen externen Font-Diensten.
|
||||
|
||||
Vor Nutzung einer Schrift wird geprüft, ob die Lizenz lokales Webhosting erlaubt.
|
||||
|
||||
## 8.6 Offene Entscheidung
|
||||
|
||||
Noch nicht entschieden:
|
||||
|
||||
```text
|
||||
created_at
|
||||
updated_at
|
||||
```
|
||||
|
||||
für `BrandProfile`.
|
||||
|
||||
Variante A:
|
||||
- beide ergänzen
|
||||
|
||||
Variante B:
|
||||
- keine Zeitstempel
|
||||
- Änderungsnachvollziehbarkeit nur über `AuditLog`
|
||||
|
||||
Dies ist der aktuelle nächste offene Punkt.
|
||||
|
||||
---
|
||||
|
||||
# 9. Noch nicht im Detail entschieden
|
||||
|
||||
- `Team`
|
||||
- `OrganizationMembership`
|
||||
- `TeamMembership`
|
||||
- Permission-Helper
|
||||
- Einladungsmodelle
|
||||
- `TeamModule`
|
||||
- `AuditLog`
|
||||
- `AuthChallenge`
|
||||
- `DeviceSession`
|
||||
- Passwordless-Flow
|
||||
- BrandProfile-Seed für Vor-Ort
|
||||
- konkrete Font-Registry-Implementierung
|
||||
- Core Storage
|
||||
- UI-Komponenten
|
||||
- Dashboard
|
||||
- Scheduling
|
||||
- Documents
|
||||
- Chat
|
||||
- Deployment
|
||||
- SMTP
|
||||
- Docker
|
||||
- Caddy
|
||||
- Backup/Restore
|
||||
|
||||
Alle diese Punkte bleiben an `PROJECT_DESIGN.md` gebunden und werden schrittweise entschieden.
|
||||
|
||||
---
|
||||
|
||||
# 10. Arbeitsmethode
|
||||
|
||||
Für jede Phase:
|
||||
|
||||
```text
|
||||
1. PROJECT_DESIGN.md lesen
|
||||
2. bereits Festgelegtes übernehmen
|
||||
3. echte offene Entscheidungen identifizieren
|
||||
4. gemeinsam entscheiden
|
||||
5. erst danach Tasks ableiten
|
||||
6. implementieren
|
||||
7. testen
|
||||
8. Checkpoint / Commit
|
||||
```
|
||||
|
||||
Regel:
|
||||
|
||||
> `TASKS.md` darf keine neue Architektur erfinden.
|
||||
|
||||
---
|
||||
|
||||
# 11. Aktueller nächster Schritt
|
||||
|
||||
Aktueller Stand:
|
||||
|
||||
```text
|
||||
Phase 2.3 — BrandProfile
|
||||
```
|
||||
|
||||
Nächste Entscheidung:
|
||||
|
||||
```text
|
||||
Soll BrandProfile created_at und updated_at erhalten?
|
||||
```
|
||||
|
||||
Danach wird `BrandProfile` abgeschlossen und Phase 2.4 `Team` begonnen.
|
||||
503
notes/index.html
Normal file
503
notes/index.html
Normal file
|
|
@ -0,0 +1,503 @@
|
|||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>AHK Ehrenamtsmanagement – HTML Mockup</title>
|
||||
<meta name="description" content="Statischer klickbarer UI-Mockup für AHK Ehrenamtsmanagement">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="mockup.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
</head>
|
||||
<body class="mockup-app">
|
||||
<div class="app-shell">
|
||||
<aside class="mock-sidebar" aria-label="Hauptnavigation">
|
||||
<div class="brand-lockup">
|
||||
<div class="brand-mark" aria-hidden="true">AHK</div>
|
||||
<div>
|
||||
<strong>EHRENAMT</strong>
|
||||
<span>Vor-Ort-Arbeit</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="team-switcher" data-page="team" type="button">
|
||||
<span class="material-icons" aria-hidden="true">groups</span>
|
||||
<span><small>Aktuelles Team</small><strong>Vor-Ort-Arbeit</strong></span>
|
||||
<span class="material-icons" aria-hidden="true">expand_more</span>
|
||||
</button>
|
||||
|
||||
<nav class="desktop-nav" aria-label="App-Bereiche">
|
||||
<p class="nav-label">Mein Bereich</p>
|
||||
<button class="nav-item is-active" data-page="start"><span class="material-icons">home</span>Start</button>
|
||||
<button class="nav-item" data-page="schedule"><span class="material-icons">calendar_month</span>Einsatzplan</button>
|
||||
<button class="nav-item" data-page="chat"><span class="material-icons">forum</span>Teamchat <span class="nav-count">3</span></button>
|
||||
<button class="nav-item" data-page="documents"><span class="material-icons">folder</span>Dokumente</button>
|
||||
<button class="nav-item" data-page="team"><span class="material-icons">groups</span>Team</button>
|
||||
<button class="nav-item" data-page="profile"><span class="material-icons">person</span>Profil</button>
|
||||
|
||||
<p class="nav-label">Verwaltung</p>
|
||||
<button class="nav-item" data-page="team-manage"><span class="material-icons">manage_accounts</span>Team verwalten</button>
|
||||
<button class="nav-item" data-page="org-manage"><span class="material-icons">domain</span>Organisation</button>
|
||||
<button class="nav-item" data-page="audit"><span class="material-icons">history</span>Audit Log</button>
|
||||
</nav>
|
||||
|
||||
<div class="mock-sidebar-footer">
|
||||
<button class="nav-item nav-item--subtle" data-page="login"><span class="material-icons">present_to_all</span>Präsentation ab Login</button>
|
||||
<div class="mini-account">
|
||||
<div class="avatar avatar--small">P</div>
|
||||
<div><strong>Patsy</strong><span>Team-Verwaltung</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<header class="mobile-header">
|
||||
<div class="brand-lockup brand-lockup--mobile">
|
||||
<div class="brand-mark" aria-hidden="true">AHK</div>
|
||||
<div><strong>EHRENAMT</strong><span>Vor-Ort-Arbeit</span></div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="icon-button" data-page="notifications" aria-label="Benachrichtigungen">
|
||||
<span class="material-icons">notifications</span><span class="notification-dot">2</span>
|
||||
</button>
|
||||
<button class="avatar avatar--button" data-page="profile" aria-label="Profil öffnen">P</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="app-main" id="main-content">
|
||||
<div class="desktop-topbar">
|
||||
<div class="route-chip" id="routeChip">/team/vor-ort-arbeit/</div>
|
||||
<div class="desktop-actions">
|
||||
<button class="icon-button icon-button--light" data-page="notifications" aria-label="Benachrichtigungen">
|
||||
<span class="material-icons">notifications</span><span class="notification-dot">2</span>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" data-page="profile"><span class="material-icons">person</span>Patsy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LOGIN -->
|
||||
<section class="screen screen--auth" data-screen="login" hidden>
|
||||
<div class="auth-panel">
|
||||
<div class="auth-brand">
|
||||
<div class="brand-mark brand-mark--large">AHK</div>
|
||||
<p class="kicker">Ehrenamtsmanagement</p>
|
||||
<h1>Willkommen zurück</h1>
|
||||
<p>Mit deiner E-Mail-Adresse erhältst du einen einmaligen Code oder einen direkten Anmeldelink.</p>
|
||||
</div>
|
||||
<form class="card auth-card" onsubmit="return false;">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="loginEmail">E-Mail-Adresse</label>
|
||||
<input class="form-control" id="loginEmail" type="email" value="patsy@example.org" autocomplete="email">
|
||||
<span class="form-hint">Wir senden den Code nur an deine hinterlegte Login-Adresse.</span>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-lg btn-full" data-page="verify" type="button">Code anfordern</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="auth-footnote">Mockup: Es werden keine Daten versendet.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- VERIFY -->
|
||||
<section class="screen screen--auth" data-screen="verify" hidden>
|
||||
<div class="auth-panel">
|
||||
<button class="text-back" data-page="login"><span class="material-icons">arrow_back</span>Zurück</button>
|
||||
<div class="auth-brand">
|
||||
<p class="kicker">Fast geschafft</p>
|
||||
<h1>Code eingeben</h1>
|
||||
<p>Wir haben einen sechsstelligen Code an <strong>patsy@example.org</strong> geschickt.</p>
|
||||
</div>
|
||||
<form class="card auth-card" onsubmit="return false;">
|
||||
<div class="card-body">
|
||||
<div class="code-inputs" aria-label="Einmal-Code">
|
||||
<input value="2" aria-label="Ziffer 1"><input value="4" aria-label="Ziffer 2"><input value="8" aria-label="Ziffer 3"><input value="1" aria-label="Ziffer 4"><input value="7" aria-label="Ziffer 5"><input value="3" aria-label="Ziffer 6">
|
||||
</div>
|
||||
<button class="btn btn-primary btn-lg btn-full" data-page="onboarding-name" type="button">Weiter</button>
|
||||
<button class="btn btn-ghost btn-full" type="button">Neuen Code senden</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ONBOARDING NAME -->
|
||||
<section class="screen screen--auth" data-screen="onboarding-name" hidden>
|
||||
<div class="auth-panel">
|
||||
<div class="auth-brand">
|
||||
<p class="kicker">Dein Profil</p>
|
||||
<h1>Wie sollen wir dich nennen?</h1>
|
||||
<p>Dieser Name ist für dein Team sichtbar. Deine Login-E-Mail bleibt für normale Teammitglieder verborgen.</p>
|
||||
</div>
|
||||
<div class="card auth-card">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="displayName">Anzeigename</label>
|
||||
<input class="form-control" id="displayName" value="Patsy">
|
||||
</div>
|
||||
<div class="avatar-picker" aria-label="Avatar auswählen">
|
||||
<button class="avatar-choice is-selected"><span class="avatar avatar--a">A</span><span>Avatar A</span></button>
|
||||
<button class="avatar-choice"><span class="avatar avatar--b">B</span><span>Avatar B</span></button>
|
||||
<button class="avatar-choice"><span class="avatar avatar--c">C</span><span>Avatar C</span></button>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-lg btn-full" data-page="team-select" type="button">Weiter zur Teamauswahl</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TEAM SELECT -->
|
||||
<section class="screen screen--auth" data-screen="team-select" hidden>
|
||||
<div class="auth-panel auth-panel--wide">
|
||||
<div class="auth-brand">
|
||||
<p class="kicker">Deine Teams</p>
|
||||
<h1>Wo möchtest du starten?</h1>
|
||||
<p>Du kannst später jederzeit zwischen deinen Teams wechseln.</p>
|
||||
</div>
|
||||
<div class="team-choice-grid">
|
||||
<button class="team-choice is-selected" data-page="start">
|
||||
<span class="team-choice__icon material-icons">diversity_3</span>
|
||||
<span><strong>Vor-Ort-Arbeit</strong><small>18 Mitglieder · 2 offene Einsätze</small></span>
|
||||
<span class="material-icons">arrow_forward</span>
|
||||
</button>
|
||||
<button class="team-choice" data-page="start">
|
||||
<span class="team-choice__icon material-icons">local_cafe</span>
|
||||
<span><strong>Café Bach</strong><small>12 Mitglieder · keine offenen Aufgaben</small></span>
|
||||
<span class="material-icons">arrow_forward</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- START -->
|
||||
<section class="screen" data-screen="start">
|
||||
<header class="page-header member-hero">
|
||||
<p class="eyebrow">Montag, 17. August</p>
|
||||
<h1>Guten Morgen, Patsy</h1>
|
||||
<p class="page-subtitle">Hier ist, was heute für dich und dein Team wichtig ist.</p>
|
||||
</header>
|
||||
|
||||
<article class="urgent-card" data-page="schedule">
|
||||
<div class="urgent-card__icon"><span class="material-icons">priority_high</span></div>
|
||||
<div>
|
||||
<span class="card-label card-label--urgent">Dringend · heute</span>
|
||||
<h2>Springer:in für heute gesucht</h2>
|
||||
<p>18:00–21:30 · Café Bach · Noch 1 Position offen</p>
|
||||
</div>
|
||||
<span class="material-icons urgent-card__arrow">arrow_forward</span>
|
||||
</article>
|
||||
|
||||
<div class="section-heading">
|
||||
<div><span class="card-label card-label--upcoming">Nächste Einsätze</span><h2>Dein Plan</h2></div>
|
||||
<button class="text-action" data-page="schedule">Alle ansehen</button>
|
||||
</div>
|
||||
|
||||
<div class="shift-stack">
|
||||
<article class="shift-card">
|
||||
<div class="date-tile"><strong>21</strong><span>AUG</span></div>
|
||||
<div class="shift-card__content">
|
||||
<div class="shift-card__title-row"><h3>Freitagabend</h3><span class="badge status-badge-ui status-ok"><span class="material-icons">check_circle</span>Eingeteilt</span></div>
|
||||
<p>18:30–21:30 · Café Bach</p>
|
||||
<span class="position-pill">Theke</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="shift-card">
|
||||
<div class="date-tile"><strong>28</strong><span>AUG</span></div>
|
||||
<div class="shift-card__content">
|
||||
<div class="shift-card__title-row"><h3>Checkpoint-Abend</h3><span class="badge status-badge-ui status-info"><span class="material-icons">schedule</span>Verfügbar</span></div>
|
||||
<p>17:30–21:00 · Pipinstraße</p>
|
||||
<span class="position-pill">Hauptposition</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<button class="dashboard-tile" data-page="schedule">
|
||||
<span class="tile-icon tile-icon--teal material-icons">event_available</span>
|
||||
<span><small>Action</small><strong>4 Termine offen</strong><em>Verfügbarkeit angeben</em></span>
|
||||
<span class="material-icons">chevron_right</span>
|
||||
</button>
|
||||
<button class="dashboard-tile" data-page="chat">
|
||||
<span class="tile-icon tile-icon--blue material-icons">forum</span>
|
||||
<span><small>Info</small><strong>3 neue Nachrichten</strong><em>im Team-Channel</em></span>
|
||||
<span class="material-icons">chevron_right</span>
|
||||
</button>
|
||||
<button class="dashboard-tile" data-page="documents">
|
||||
<span class="tile-icon material-icons">description</span>
|
||||
<span><small>Info</small><strong>1 neues Dokument</strong><em>Safer-Sex-Leitfaden</em></span>
|
||||
<span class="material-icons">chevron_right</span>
|
||||
</button>
|
||||
<button class="dashboard-tile" data-page="team">
|
||||
<span class="tile-icon tile-icon--yellow material-icons">groups</span>
|
||||
<span><small>Team</small><strong>18 Mitglieder</strong><em>Kontakte & Zuständigkeiten</em></span>
|
||||
<span class="material-icons">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TEAM -->
|
||||
<section class="screen" data-screen="team" hidden>
|
||||
<header class="page-header with-team-switch">
|
||||
<div><p class="kicker">Vor-Ort-Arbeit</p><h1>Unser Team</h1><p class="page-subtitle">Menschen, Kontakte und aktuelle Team-Infos.</p></div>
|
||||
<button class="btn btn-ghost btn-sm"><span class="material-icons">swap_horiz</span>Team wechseln</button>
|
||||
</header>
|
||||
|
||||
<div class="team-highlight card">
|
||||
<div class="card-body">
|
||||
<span class="card-label card-label--info">Ankündigung</span>
|
||||
<h2>Teamtreffen am 26. August</h2>
|
||||
<p>Wir besprechen die September-Planung und sammeln Feedback zur neuen Einsatzstruktur.</p>
|
||||
<span class="meta-line"><span class="material-icons">campaign</span>von Alex · vor 2 Stunden</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading"><div><span class="card-label">Mitglieder</span><h2>18 Personen</h2></div><button class="text-action" data-page="members">Verzeichnis öffnen</button></div>
|
||||
<div class="member-preview-row">
|
||||
<div class="avatar avatar--a">A</div><div class="avatar avatar--b">B</div><div class="avatar avatar--c">C</div><div class="avatar">P</div><div class="avatar avatar--muted">+14</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading"><div><span class="card-label">Aktuell</span><h2>Im Team</h2></div></div>
|
||||
<div class="card-list">
|
||||
<button class="list-row" data-page="members"><span class="material-icons list-row__icon">contacts</span><span><strong>Mitgliederverzeichnis</strong><small>Kontaktdaten, Team-Verwaltungen, Zuständigkeiten</small></span><span class="material-icons">chevron_right</span></button>
|
||||
<button class="list-row" data-page="documents"><span class="material-icons list-row__icon">folder</span><span><strong>Dokumente</strong><small>12 aktuelle Dokumente · 4 im Archiv</small></span><span class="material-icons">chevron_right</span></button>
|
||||
<button class="list-row" data-page="chat"><span class="material-icons list-row__icon">forum</span><span><strong>Teamkommunikation</strong><small>2 aktive Channels</small></span><span class="nav-count">3</span></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- MEMBERS -->
|
||||
<section class="screen" data-screen="members" hidden>
|
||||
<header class="page-header">
|
||||
<p class="kicker">Vor-Ort-Arbeit</p><h1>Mitglieder</h1><p class="page-subtitle">Kontaktdaten sind nur sichtbar, wenn sie freiwillig geteilt wurden.</p>
|
||||
</header>
|
||||
<div class="search-row"><div class="search-box"><span class="material-icons">search</span><input placeholder="Mitglied suchen …"></div><span class="badge badge-blue">18 aktiv</span></div>
|
||||
<div class="member-list">
|
||||
<article class="member-card">
|
||||
<div class="avatar avatar--a">A</div><div class="member-card__main"><div><h3>Alex</h3><span class="badge badge-teal">Team-Verwaltung</span></div><p>Erreichbar: Signal · ab 17 Uhr</p><a href="#">+49 170 1234567</a></div><button class="icon-button icon-button--light" aria-label="Kontakt"><span class="material-icons">chat</span></button>
|
||||
</article>
|
||||
<article class="member-card">
|
||||
<div class="avatar avatar--b">B</div><div class="member-card__main"><div><h3>Sam</h3></div><p>Erreichbar: SMS · bitte keine Anrufe</p><a href="#">+49 171 9876543</a></div><button class="icon-button icon-button--light" aria-label="Kontakt"><span class="material-icons">sms</span></button>
|
||||
</article>
|
||||
<article class="member-card">
|
||||
<div class="avatar avatar--c">C</div><div class="member-card__main"><div><h3>Jo</h3></div><p class="muted-copy">Keine Kontaktdaten freigegeben</p></div><span class="material-icons muted-icon">lock</span>
|
||||
</article>
|
||||
<article class="member-card member-card--admin-extra">
|
||||
<div class="avatar">M</div><div class="member-card__main"><div><h3>Mehmet</h3><span class="badge badge-muted">Eingeladen</span></div><p>Login: mehmet@example.org · Einladung offen</p><small>Beitritt: noch nicht erfolgt</small></div><button class="btn btn-ghost btn-sm">Details</button>
|
||||
</article>
|
||||
</div>
|
||||
<div class="context-note"><span class="material-icons">visibility</span><span><strong>Verwaltungsansicht:</strong> Die letzte Zeile zeigt exemplarisch Zusatzdaten, die nur Team-Verwaltungen sehen.</span></div>
|
||||
</section>
|
||||
|
||||
<!-- SCHEDULE -->
|
||||
<section class="screen" data-screen="schedule" hidden>
|
||||
<header class="page-header">
|
||||
<p class="kicker">Einsatzplanung</p><h1>September 2026</h1><p class="page-subtitle">Verfügbarkeit offen bis 23. August · Veröffentlichung geplant für 25. August.</p>
|
||||
</header>
|
||||
|
||||
<div class="period-progress card">
|
||||
<div class="card-body">
|
||||
<div class="period-progress__top"><span class="badge status-badge-ui status-action"><span class="material-icons">edit_calendar</span>Verfügbarkeit offen</span><strong>3 von 7 beantwortet</strong></div>
|
||||
<div class="progress-track"><span style="width:43%"></span></div>
|
||||
<p>Bitte gib für jeden Termin an, ob du verfügbar bist.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="availability-list">
|
||||
<article class="availability-card">
|
||||
<div class="date-tile"><strong>04</strong><span>SEP</span></div>
|
||||
<div class="availability-card__main"><h3>Checkpoint-Abend</h3><p>17:30–21:00 · Pipinstraße</p><div class="need-row"><span>Haupt: 2</span><span>Springer: 1</span></div></div>
|
||||
<div class="availability-actions"><button class="answer-btn answer-btn--yes is-selected"><span class="material-icons">check</span>Ja</button><button class="answer-btn answer-btn--no"><span class="material-icons">close</span>Nein</button></div>
|
||||
</article>
|
||||
<article class="availability-card availability-card--urgent">
|
||||
<div class="date-tile"><strong>18</strong><span>SEP</span></div>
|
||||
<div class="availability-card__main"><div class="title-with-badge"><h3>Freitagabend</h3><span class="badge badge-coral">Offen</span></div><p>18:00–21:30 · Café Bach</p><div class="need-row"><span>Haupt: 2/2</span><span>Springer: 0/1</span></div></div>
|
||||
<div class="availability-actions"><button class="answer-btn answer-btn--yes"><span class="material-icons">check</span>Ja</button><button class="answer-btn answer-btn--no"><span class="material-icons">close</span>Nein</button></div>
|
||||
</article>
|
||||
<article class="availability-card">
|
||||
<div class="date-tile"><strong>25</strong><span>SEP</span></div>
|
||||
<div class="availability-card__main"><h3>Checkpoint-Abend</h3><p>17:30–21:00 · Pipinstraße</p><div class="need-row"><span>Haupt: 2</span><span>Springer: 1</span></div></div>
|
||||
<div class="availability-actions"><button class="answer-btn answer-btn--yes"><span class="material-icons">check</span>Ja</button><button class="answer-btn answer-btn--no is-selected"><span class="material-icons">close</span>Nein</button></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- DOCUMENTS -->
|
||||
<section class="screen" data-screen="documents" hidden>
|
||||
<header class="page-header page-header--actions">
|
||||
<div><p class="kicker">Teamwissen</p><h1>Dokumente</h1><p class="page-subtitle">Freigegebene Materialien für Vor-Ort-Arbeit.</p></div>
|
||||
<button class="btn btn-primary"><span class="material-icons">upload</span>Hochladen</button>
|
||||
</header>
|
||||
<div class="segmented" role="tablist" aria-label="Dokumentenstatus"><button class="is-active">Aktuell <span>12</span></button><button>Archiv <span>4</span></button></div>
|
||||
<div class="document-list">
|
||||
<article class="document-card">
|
||||
<div class="file-icon file-icon--pdf">PDF</div><div><h3>Safer-Sex-Leitfaden 2026</h3><p>Aktuelle Hinweise für Beratung und Vor-Ort-Arbeit.</p><span>PDF · 1,8 MB · aktualisiert 12.08.2026</span></div><button class="icon-button icon-button--light"><span class="material-icons">download</span></button>
|
||||
</article>
|
||||
<article class="document-card">
|
||||
<div class="file-thumb"><span class="material-icons">image</span></div><div><h3>Aufbauplan Infostand</h3><p>Foto mit markierter Positionierung der Materialien.</p><span>JPG · 840 KB · aktualisiert 02.08.2026</span></div><button class="icon-button icon-button--light"><span class="material-icons">visibility</span></button>
|
||||
</article>
|
||||
<article class="document-card document-card--pending">
|
||||
<div class="file-icon">DOCX</div><div><div class="title-with-badge"><h3>Checkliste Materialkiste</h3><span class="badge badge-yellow">Prüfung läuft</span></div><p>Nur für dich und Team-Verwaltung sichtbar.</p><span>DOCX · 95 KB · von dir hochgeladen</span></div><button class="btn btn-ghost btn-sm">Zurückziehen</button>
|
||||
</article>
|
||||
</div>
|
||||
<div class="context-note"><span class="material-icons">photo_camera</span><span>Bitte nur Fotos hochladen, für die die erforderlichen Rechte geklärt sind.</span></div>
|
||||
</section>
|
||||
|
||||
<!-- CHAT -->
|
||||
<section class="screen screen--chat" data-screen="chat" hidden>
|
||||
<header class="page-header"><p class="kicker">Teamkommunikation</p><h1>Teamchat</h1><p class="page-subtitle">Keine Dateien, keine Embeds – nur klare Teamkommunikation.</p></header>
|
||||
<div class="channel-layout">
|
||||
<aside class="channel-list">
|
||||
<button class="channel-item"><span class="material-icons">campaign</span><span><strong>Ankündigungen</strong><small>nur Team-Verwaltung schreibt</small></span><span class="nav-count">1</span></button>
|
||||
<button class="channel-item is-active"><span class="material-icons">tag</span><span><strong>Team</strong><small>3 ungelesen</small></span><span class="nav-count">3</span></button>
|
||||
<button class="channel-item"><span class="material-icons">tag</span><span><strong>Veranstaltungen</strong><small>keine neuen Nachrichten</small></span></button>
|
||||
</aside>
|
||||
<section class="chat-pane">
|
||||
<div class="chat-pane__header"><div><strong># Team</strong><span>18 Mitglieder</span></div><button class="icon-button icon-button--light"><span class="material-icons">info</span></button></div>
|
||||
<div class="message-stream">
|
||||
<div class="chat-day"><span>Heute</span></div>
|
||||
<article class="message-bubble message-bubble--other"><div class="message-author"><span class="avatar avatar--mini avatar--a">A</span><strong>Alex</strong><time>09:12</time></div><p>Kann jemand am Freitag die Materialkiste schon um 17:30 mitbringen?</p><div class="message-state">✓✓ gelesen von 8/18</div></article>
|
||||
<article class="message-bubble message-bubble--own"><div class="message-author"><strong>Du</strong><time>09:18</time></div><p>Ja, ich kann sie auf dem Weg mitnehmen.</p><div class="message-state message-state--read">✓✓ gelesen</div></article>
|
||||
<article class="message-bubble message-bubble--other message-bubble--reply"><div class="reply-reference">Antwort auf deine Nachricht</div><div class="message-author"><span class="avatar avatar--mini avatar--b">B</span><strong>Sam</strong><time>09:22</time></div><p>Perfekt, danke! Dann schreibe ich das so in die Vorbereitung.</p><div class="message-state">✓ gespeichert</div></article>
|
||||
</div>
|
||||
<form class="chat-composer" onsubmit="return false;"><textarea aria-label="Nachricht" placeholder="Nachricht an #Team"></textarea><div><span class="composer-hint">https-Link möglich · max. 1 externer Link</span><button class="send-button" aria-label="Senden"><span class="material-icons">send</span></button></div></form>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PROFILE -->
|
||||
<section class="screen" data-screen="profile" hidden>
|
||||
<header class="page-header"><p class="kicker">Mein Konto</p><h1>Profil</h1><p class="page-subtitle">So sehen dich deine Teamkolleg:innen.</p></header>
|
||||
<div class="profile-grid">
|
||||
<section class="card profile-card">
|
||||
<div class="card-body">
|
||||
<div class="profile-identity"><div class="avatar avatar--large">P</div><div><h2>Patsy</h2><span>Avatar A</span></div></div>
|
||||
<div class="form-group"><label>Anzeigename</label><input class="form-control" value="Patsy"></div>
|
||||
<div class="avatar-picker avatar-picker--compact"><button class="avatar-choice is-selected"><span class="avatar avatar--a">A</span><span>A</span></button><button class="avatar-choice"><span class="avatar avatar--b">B</span><span>B</span></button><button class="avatar-choice"><span class="avatar avatar--c">C</span><span>C</span></button></div>
|
||||
<button class="btn btn-primary">Änderungen speichern</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card">
|
||||
<div class="card-header"><h2>Kontaktdaten im Team</h2><span class="badge badge-muted">Standard: aus</span></div>
|
||||
<div class="card-body">
|
||||
<label class="toggle-row"><span><strong>Kontaktdaten teilen</strong><small>Nur Mitglieder deines Teams sehen diese Angaben.</small></span><input type="checkbox"><span class="toggle-ui"></span></label>
|
||||
<div class="form-group"><label>Telefon</label><input class="form-control" value="+49 170 1234567"></div>
|
||||
<div class="form-group"><label>Erreichbar per</label><div class="choice-pills"><button class="is-selected">Signal</button><button>SMS</button><button>Anruf</button></div></div>
|
||||
<div class="form-group"><label>Kontakt-Notiz</label><textarea class="form-control">Am besten ab 17 Uhr.</textarea></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card profile-card--wide">
|
||||
<div class="card-header"><h2>Angemeldete Geräte</h2></div>
|
||||
<div class="device-row"><span class="material-icons">computer</span><div><strong>Heimdall-home · Brave</strong><small>Dieses Gerät · zuletzt aktiv gerade eben</small></div><span class="badge badge-teal">Aktuell</span></div>
|
||||
<div class="device-row"><span class="material-icons">smartphone</span><div><strong>Galaxy · Chrome</strong><small>Zuletzt aktiv gestern, 22:14</small></div><button class="btn btn-ghost btn-sm">Abmelden</button></div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- NOTIFICATIONS -->
|
||||
<section class="screen" data-screen="notifications" hidden>
|
||||
<header class="page-header"><p class="kicker">Glocke</p><h1>Benachrichtigungen</h1><p class="page-subtitle">Nur Ereignisse, die deine Aufmerksamkeit brauchen – dauerhafte Zustände bleiben im Dashboard.</p></header>
|
||||
<div class="notification-list">
|
||||
<article class="notification-card notification-card--urgent"><span class="material-icons">priority_high</span><div><span class="card-label card-label--urgent">Dringend</span><h3>Offener Dienst in weniger als 24 Stunden</h3><p>Heute, 18:00 · Springer:in noch unbesetzt.</p><time>vor 12 Minuten</time></div></article>
|
||||
<article class="notification-card notification-card--important"><span class="material-icons">event_available</span><div><span class="card-label card-label--action">Wichtig</span><h3>Deine Einteilung wurde geändert</h3><p>Am 21.08. bist du jetzt für „Theke“ eingeteilt.</p><time>vor 2 Stunden</time></div></article>
|
||||
<article class="notification-card"><span class="material-icons">reply</span><div><span class="card-label card-label--info">Antwort</span><h3>Sam hat dir im Teamchat geantwortet</h3><p>„Perfekt, danke! Dann schreibe ich das so …“</p><time>heute, 09:22</time></div></article>
|
||||
<article class="notification-card"><span class="material-icons">campaign</span><div><span class="card-label card-label--info">Ankündigung</span><h3>Teamtreffen am 26. August</h3><p>Neue Ankündigung von Alex.</p><time>gestern</time></div></article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TEAM MANAGE -->
|
||||
<section class="screen" data-screen="team-manage" hidden>
|
||||
<header class="page-header"><p class="kicker">Team-Verwaltung</p><h1>Vor-Ort-Arbeit</h1><p class="page-subtitle">Operative Aufgaben im Team – ohne Organisationsverwaltung.</p></header>
|
||||
<div class="admin-kpi-grid">
|
||||
<button class="admin-kpi admin-kpi--urgent"><span class="material-icons">warning</span><strong>2</strong><small>unterbesetzte Einsätze</small></button>
|
||||
<button class="admin-kpi"><span class="material-icons">event_busy</span><strong>6</strong><small>Rückmeldungen fehlen</small></button>
|
||||
<button class="admin-kpi"><span class="material-icons">fact_check</span><strong>2</strong><small>Dokumente warten</small></button>
|
||||
<button class="admin-kpi"><span class="material-icons">mail</span><strong>3</strong><small>offene Einladungen</small></button>
|
||||
</div>
|
||||
<section class="card management-section">
|
||||
<div class="card-header"><h2>Unterbesetzte Einsätze</h2><span class="badge badge-coral">Handlungsbedarf</span></div>
|
||||
<div class="management-row"><div><strong>Heute · Freitagabend</strong><span>18:00–21:30 · Springer: 0/1</span></div><button class="btn btn-coral btn-sm">Offenen Platz besetzen</button></div>
|
||||
<div class="management-row"><div><strong>04.09. · Checkpoint-Abend</strong><span>17:30–21:00 · Haupt: 1/2</span></div><button class="btn btn-ghost btn-sm">Planung öffnen</button></div>
|
||||
</section>
|
||||
<section class="card management-section">
|
||||
<div class="card-header"><h2>Fairness-Hilfe</h2><span class="badge badge-blue">Vorschlag</span></div>
|
||||
<div class="fairness-table">
|
||||
<div class="fairness-head"><span>Person</span><span>Einsätze</span><span>Letzter Einsatz</span></div>
|
||||
<div><strong>Sam</strong><span>2</span><span>18.07.</span></div><div><strong>Jo</strong><span>3</span><span>01.08.</span></div><div><strong>Patsy</strong><span>5</span><span>14.08.</span></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- ORG MANAGE -->
|
||||
<section class="screen" data-screen="org-manage" hidden>
|
||||
<header class="page-header"><p class="kicker">Organisationsverwaltung</p><h1>Aidshilfe Köln</h1><p class="page-subtitle">Struktur, Rechte, Brand Profiles und Module – keine teaminternen Fachinhalte.</p></header>
|
||||
<div class="context-note context-note--strong"><span class="material-icons">shield</span><span>Diese Ebene zeigt bewusst <strong>keinen Teamchat, keine Einsatzdetails und keine Teamdokumente</strong>.</span></div>
|
||||
<div class="org-grid">
|
||||
<button class="org-card"><span class="material-icons">groups</span><strong>7 Teams</strong><small>Teams anlegen, archivieren und verwalten</small><span class="material-icons">arrow_forward</span></button>
|
||||
<button class="org-card"><span class="material-icons">admin_panel_settings</span><strong>Team-Verwaltungen</strong><small>Rollen pro Team vergeben</small><span class="material-icons">arrow_forward</span></button>
|
||||
<button class="org-card"><span class="material-icons">palette</span><strong>Brand Profiles</strong><small>Farben, Typografie und Team-Identität</small><span class="material-icons">arrow_forward</span></button>
|
||||
<button class="org-card"><span class="material-icons">extension</span><strong>Module</strong><small>Scheduling, Chat und Dokumente aktivieren</small><span class="material-icons">arrow_forward</span></button>
|
||||
<button class="org-card"><span class="material-icons">verified_user</span><strong>Org-Admins</strong><small>Organisationsrechte verwalten</small><span class="material-icons">arrow_forward</span></button>
|
||||
<button class="org-card" data-page="audit"><span class="material-icons">history</span><strong>Audit Log</strong><small>Administrative Änderungen nachvollziehen</small><span class="material-icons">arrow_forward</span></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- AUDIT -->
|
||||
<section class="screen" data-screen="audit" hidden>
|
||||
<header class="page-header"><p class="kicker">Organisation</p><h1>Audit Log</h1><p class="page-subtitle">Nur relevante administrative Änderungen. Chat und Verfügbarkeiten werden hier nicht protokolliert.</p></header>
|
||||
<div class="filter-bar card"><div class="search-box"><span class="material-icons">search</span><input placeholder="Audit Log durchsuchen …"></div><select class="form-control"><option>Alle Ereignisse</option><option>Teams</option><option>Rechte</option><option>Module</option><option>Dokumente</option></select></div>
|
||||
<div class="audit-list card">
|
||||
<div class="audit-row"><span class="audit-icon"><span class="material-icons">manage_accounts</span></span><div><strong>Team-Verwaltung ernannt</strong><p>Alex wurde für „Vor-Ort-Arbeit“ als Team-Verwaltung ernannt.</p><span>Patsy · 17.08.2026, 10:42</span></div></div>
|
||||
<div class="audit-row"><span class="audit-icon"><span class="material-icons">extension</span></span><div><strong>Modul aktiviert</strong><p>„Dokumente“ wurde für Team „Café Bach“ aktiviert.</p><span>Org-Admin · 16.08.2026, 14:08</span></div></div>
|
||||
<div class="audit-row"><span class="audit-icon audit-icon--danger"><span class="material-icons">delete_forever</span></span><div><strong>Dokument dauerhaft gelöscht</strong><p>„Alte Kontaktdaten.xlsx“ wurde dauerhaft entfernt.</p><span>Alex · 15.08.2026, 18:31</span></div></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<nav class="bottom-nav" aria-label="Mobile Navigation">
|
||||
<button class="bottom-nav__item is-active" data-page="start"><span class="material-icons">home</span><span>Start</span></button>
|
||||
<button class="bottom-nav__item" data-page="schedule"><span class="material-icons">calendar_month</span><span>Plan</span></button>
|
||||
<button class="bottom-nav__item" data-page="chat"><span class="material-icons">forum</span><span>Chat</span><i>3</i></button>
|
||||
<button class="bottom-nav__item" data-page="documents"><span class="material-icons">folder</span><span>Dateien</span></button>
|
||||
<button class="bottom-nav__item" data-page="team"><span class="material-icons">groups</span><span>Team</span></button>
|
||||
<button class="bottom-nav__item" data-page="profile"><span class="material-icons">person</span><span>Profil</span></button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const screens = [...document.querySelectorAll('[data-screen]')];
|
||||
const pageButtons = [...document.querySelectorAll('[data-page]')];
|
||||
const routeChip = document.getElementById('routeChip');
|
||||
|
||||
const routes = {
|
||||
login: '/auth/login/', verify: '/auth/verify/', 'onboarding-name': '/onboarding/profile/', 'team-select': '/onboarding/team/',
|
||||
start: '/team/vor-ort-arbeit/', team: '/team/vor-ort-arbeit/team/', members: '/team/vor-ort-arbeit/members/',
|
||||
schedule: '/team/vor-ort-arbeit/scheduling/', documents: '/team/vor-ort-arbeit/documents/', chat: '/team/vor-ort-arbeit/chat/',
|
||||
profile: '/profile/', notifications: '/notifications/', 'team-manage': '/team/vor-ort-arbeit/manage/', 'org-manage': '/org/aidshilfe-koeln/manage/', audit: '/org/aidshilfe-koeln/manage/audit/'
|
||||
};
|
||||
|
||||
function showPage(page, pushHash = true) {
|
||||
const target = screens.find(screen => screen.dataset.screen === page) || screens.find(screen => screen.dataset.screen === 'start');
|
||||
screens.forEach(screen => screen.hidden = screen !== target);
|
||||
document.querySelectorAll('.nav-item, .bottom-nav__item').forEach(button => button.classList.toggle('is-active', button.dataset.page === page));
|
||||
routeChip.textContent = routes[page] || routes.start;
|
||||
if (pushHash) history.replaceState(null, '', '#' + page);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
pageButtons.forEach(button => button.addEventListener('click', () => showPage(button.dataset.page)));
|
||||
|
||||
document.querySelectorAll('.answer-btn').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
button.parentElement.querySelectorAll('.answer-btn').forEach(b => b.classList.remove('is-selected'));
|
||||
button.classList.add('is-selected');
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.avatar-choice').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
button.closest('.avatar-picker').querySelectorAll('.avatar-choice').forEach(b => b.classList.remove('is-selected'));
|
||||
button.classList.add('is-selected');
|
||||
});
|
||||
});
|
||||
|
||||
const initial = location.hash.replace('#','');
|
||||
if (initial && screens.some(s => s.dataset.screen === initial)) showPage(initial, false);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
400
notes/mockup.css
Normal file
400
notes/mockup.css
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
/*
|
||||
* AHK Ehrenamtsmanagement — Präsentations-Mockup
|
||||
* Ergänzt die unveränderte style.css um die mobile Mitglieder-App.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--brand-ink: #07182E;
|
||||
--brand-deep: #061126;
|
||||
--brand-teal: #00858A;
|
||||
--brand-teal-strong: #005F61;
|
||||
--brand-aqua: #52E0CF;
|
||||
--brand-blue: #2596BE;
|
||||
--brand-coral: #F04A67;
|
||||
--brand-yellow: #F6E74A;
|
||||
--brand-mist: #F4FAFC;
|
||||
--brand-paper: #FFFFFF;
|
||||
}
|
||||
|
||||
body.mockup-app {
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
color: var(--brand-ink);
|
||||
background: var(--brand-deep);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
button { min-height: 44px; }
|
||||
button, a { -webkit-tap-highlight-color: transparent; }
|
||||
|
||||
.app-shell { min-height: 100vh; background: var(--brand-mist); }
|
||||
|
||||
.mock-sidebar {
|
||||
position: fixed;
|
||||
inset: 0 auto 0 0;
|
||||
z-index: 40;
|
||||
width: 260px;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
background:
|
||||
radial-gradient(circle at 14% 8%, rgba(82,224,207,.25), transparent 20rem),
|
||||
linear-gradient(165deg, var(--brand-teal-strong), var(--brand-deep) 78%);
|
||||
border-right: 1px solid rgba(255,255,255,.1);
|
||||
}
|
||||
|
||||
.brand-lockup { display: flex; align-items: center; gap: .75rem; padding: 1.1rem 1.1rem .9rem; }
|
||||
.brand-lockup strong { display: block; font-family: var(--font-display); font-size: 1.35rem; letter-spacing: .04em; line-height: .85; }
|
||||
.brand-lockup span { display: block; margin-top: .18rem; color: rgba(255,255,255,.7); font-size: .7rem; }
|
||||
.brand-mark { display: grid; width: 42px; height: 42px; place-items: center; border: 2px solid var(--brand-aqua); border-radius: 12px 4px 12px 4px; color: var(--brand-yellow); font-family: var(--font-display); font-size: 1.05rem; font-weight: 900; letter-spacing: .03em; transform: rotate(-2deg); }
|
||||
.brand-mark--large { width: 62px; height: 62px; margin: 0 auto 1.2rem; font-size: 1.4rem; }
|
||||
|
||||
.team-switcher { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .65rem; width: calc(100% - 1.4rem); margin: .35rem .7rem .7rem; padding: .72rem .8rem; border: 1px solid rgba(255,255,255,.13); border-radius: .75rem; color: #fff; background: rgba(255,255,255,.06); text-align: left; cursor: pointer; }
|
||||
.team-switcher small, .team-switcher strong { display: block; }
|
||||
.team-switcher small { color: rgba(255,255,255,.52); font-size: .63rem; text-transform: uppercase; letter-spacing: .07em; }
|
||||
.team-switcher strong { font-size: .82rem; }
|
||||
|
||||
.desktop-nav { display: flex; flex: 1; flex-direction: column; gap: 2px; overflow-y: auto; padding: .2rem .65rem 1rem; }
|
||||
.nav-label { padding: 1rem .7rem .35rem; color: rgba(255,255,255,.36); font-size: .62rem; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; }
|
||||
.nav-item { display: flex; align-items: center; gap: .68rem; width: 100%; min-height: 46px; padding: .62rem .7rem; border: 0; border-radius: .65rem; color: rgba(255,255,255,.76); background: transparent; text-align: left; cursor: pointer; font-weight: 650; }
|
||||
.nav-item .material-icons { color: rgba(255,255,255,.44); font-size: 1.15rem; }
|
||||
.nav-item:hover { color: #fff; background: rgba(255,255,255,.07); }
|
||||
.nav-item.is-active { color: #fff; background: rgba(82,224,207,.16); box-shadow: inset 3px 0 0 var(--brand-aqua); }
|
||||
.nav-item.is-active .material-icons { color: var(--brand-yellow); }
|
||||
.nav-count { display: inline-grid; min-width: 1.3rem; height: 1.3rem; margin-left: auto; padding: 0 .3rem; place-items: center; border-radius: 999px; color: #fff; background: var(--brand-coral); font-size: .65rem; font-style: normal; font-weight: 800; }
|
||||
.nav-item--subtle { margin-bottom: .6rem; color: rgba(255,255,255,.55); font-size: .78rem; }
|
||||
.mock-sidebar-footer { padding: .7rem; border-top: 1px solid rgba(255,255,255,.1); }
|
||||
.mini-account { display: flex; align-items: center; gap: .65rem; padding: .7rem .55rem; }
|
||||
.mini-account strong, .mini-account span { display: block; }
|
||||
.mini-account strong { font-size: .8rem; }
|
||||
.mini-account span { color: var(--brand-aqua); font-size: .65rem; }
|
||||
|
||||
.mobile-header { position: sticky; top: 0; z-index: 30; display: flex; min-height: 66px; align-items: center; justify-content: space-between; padding: 0 .8rem; color: #fff; background: linear-gradient(135deg, var(--brand-teal-strong), var(--brand-deep)); box-shadow: 0 3px 18px rgba(6,17,38,.2); }
|
||||
.brand-lockup--mobile { padding: 0; }
|
||||
.brand-lockup--mobile .brand-mark { width: 35px; height: 35px; border-radius: 10px 3px 10px 3px; font-size: .84rem; }
|
||||
.brand-lockup--mobile strong { font-size: 1.1rem; }
|
||||
.brand-lockup--mobile span { font-size: .62rem; }
|
||||
.header-actions { display: flex; align-items: center; gap: .35rem; }
|
||||
.icon-button { position: relative; display: inline-grid; width: 44px; height: 44px; place-items: center; padding: 0; border: 0; border-radius: 50%; color: #fff; background: transparent; cursor: pointer; }
|
||||
.icon-button:hover { background: rgba(255,255,255,.08); }
|
||||
.icon-button--light { color: var(--brand-ink); background: #fff; border: 1px solid rgba(7,24,46,.12); }
|
||||
.icon-button--light:hover { color: var(--brand-teal-strong); background: #eef8f9; }
|
||||
.notification-dot { position: absolute; top: 7px; right: 6px; display: grid; min-width: 17px; height: 17px; padding: 0 4px; place-items: center; border: 2px solid var(--brand-deep); border-radius: 999px; color: #fff; background: var(--brand-coral); font-size: .58rem; font-weight: 800; }
|
||||
.notification-dot::after { content: '2'; }
|
||||
|
||||
.avatar { display: grid; width: 42px; height: 42px; flex: 0 0 42px; place-items: center; border: 2px solid #fff; border-radius: 50%; color: #fff; background: linear-gradient(135deg, var(--brand-teal), var(--brand-blue)); font-weight: 800; box-shadow: 0 2px 7px rgba(7,24,46,.15); }
|
||||
.avatar--button { width: 38px; height: 38px; min-height: 38px; border: 2px solid rgba(255,255,255,.65); cursor: pointer; }
|
||||
.avatar--small { width: 34px; height: 34px; flex-basis: 34px; }
|
||||
.avatar--mini { width: 26px; height: 26px; flex-basis: 26px; border-width: 1px; font-size: .7rem; }
|
||||
.avatar--large { width: 72px; height: 72px; flex-basis: 72px; font-size: 1.35rem; }
|
||||
.avatar--a { background: linear-gradient(135deg, #00858A, #005F61); }
|
||||
.avatar--b { background: linear-gradient(135deg, #2596BE, #5264d9); }
|
||||
.avatar--c { background: linear-gradient(135deg, #F04A67, #b744c4); }
|
||||
.avatar--muted { color: var(--brand-ink); background: #dceff2; border-color: #fff; }
|
||||
|
||||
main.app-main {
|
||||
margin: 0;
|
||||
min-height: calc(100vh - 66px);
|
||||
padding: 1.2rem .9rem 6.4rem;
|
||||
color: var(--brand-ink);
|
||||
background: var(--brand-mist);
|
||||
}
|
||||
.desktop-topbar { display: none; }
|
||||
.screen { width: min(100%, 1040px); margin: 0 auto; animation: page-in .18s ease-out; }
|
||||
@keyframes page-in { from { opacity: .5; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
||||
|
||||
.page-header { margin-bottom: 1.35rem; }
|
||||
.page-header h1, .member-hero h1 { margin: 0 0 .4rem; color: var(--brand-ink); font-family: var(--font-display); font-size: clamp(2.25rem, 9vw, 3.6rem); font-weight: 700; line-height: .88; letter-spacing: -.015em; text-transform: uppercase; }
|
||||
.page-subtitle { max-width: 48rem; color: #425e70 !important; font-size: .9rem; }
|
||||
.eyebrow { margin-bottom: .42rem; color: var(--brand-teal-strong); font-size: .72rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
|
||||
.kicker { color: var(--brand-teal) !important; }
|
||||
.with-team-switch, .page-header--actions { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 1rem; }
|
||||
|
||||
.card { border-color: rgba(7,24,46,.12); background: #fff; box-shadow: 0 5px 18px rgba(7,24,46,.06); }
|
||||
.card-header { background: #edf7f8; border-color: rgba(7,24,46,.1); }
|
||||
.card-header h2, .card-header h3 { color: var(--brand-ink); }
|
||||
.card-body { color: #30495c; }
|
||||
.btn-primary { color: #fff !important; background: #087b7c !important; border-color: #087b7c !important; }
|
||||
.btn-primary:hover { background: #05696a !important; }
|
||||
.btn-ghost { color: var(--brand-ink); border-color: rgba(7,24,46,.18); background: #fff; }
|
||||
.btn-ghost:hover { color: var(--brand-ink); border-color: var(--brand-teal); background: #edf7f8; }
|
||||
|
||||
.card-label { display: block; margin-bottom: .35rem; color: #607786; font-size: .64rem; font-weight: 900; letter-spacing: .1em; text-transform: uppercase; }
|
||||
.card-label--urgent { color: #b62542; }
|
||||
.card-label--action { color: #8b5a00; }
|
||||
.card-label--upcoming, .card-label--info { color: #087b7c; }
|
||||
|
||||
.urgent-card { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .85rem; margin-bottom: 1.8rem; padding: 1rem; border: 1px solid rgba(240,74,103,.3); border-left: 5px solid var(--brand-coral); border-radius: 1rem; background: linear-gradient(110deg, #fff3f5, #fff); box-shadow: 0 6px 18px rgba(240,74,103,.09); cursor: pointer; }
|
||||
.urgent-card__icon { display: grid; width: 42px; height: 42px; place-items: center; border-radius: .75rem; color: #fff; background: var(--brand-coral); }
|
||||
.urgent-card h2 { margin: 0 0 .15rem; font-size: 1rem; }
|
||||
.urgent-card p { color: #4a6474; font-size: .78rem; }
|
||||
.urgent-card__arrow { color: var(--brand-coral); }
|
||||
|
||||
.section-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; margin: 1.7rem 0 .8rem; }
|
||||
.section-heading h2 { margin: 0; color: var(--brand-ink); font-size: 1.1rem; }
|
||||
.text-action, .text-back { display: inline-flex; align-items: center; gap: .3rem; min-height: 44px; padding: 0; border: 0; color: #087b7c; background: transparent; cursor: pointer; font-size: .78rem; font-weight: 800; }
|
||||
|
||||
.shift-stack { display: grid; gap: .7rem; }
|
||||
.shift-card { display: grid; grid-template-columns: auto 1fr; gap: .85rem; padding: .85rem; border: 1px solid rgba(7,24,46,.11); border-radius: .9rem; background: #fff; box-shadow: 0 3px 12px rgba(7,24,46,.05); }
|
||||
.date-tile { display: grid; width: 52px; height: 58px; place-items: center; align-content: center; border-radius: .7rem; color: #fff; background: var(--brand-teal-strong); }
|
||||
.date-tile strong { font-family: var(--font-display); font-size: 1.55rem; line-height: .9; }
|
||||
.date-tile span { margin-top: .2rem; color: var(--brand-aqua); font-size: .57rem; font-weight: 900; letter-spacing: .1em; }
|
||||
.shift-card__content { min-width: 0; }
|
||||
.shift-card__title-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .35rem; }
|
||||
.shift-card h3 { color: var(--brand-ink); font-size: .92rem; }
|
||||
.shift-card p { margin: .1rem 0 .4rem; color: #5c7280; font-size: .76rem; }
|
||||
.position-pill { display: inline-flex; padding: .18rem .5rem; border-radius: 999px; color: #075f60; background: #e2f5f3; font-size: .67rem; font-weight: 800; }
|
||||
.status-badge-ui { display: inline-flex; align-items: center; gap: .25rem; border: 1px solid transparent; }
|
||||
.status-badge-ui .material-icons { font-size: .9rem; }
|
||||
.status-ok { color: #0a654f; background: #e5f6ef; border-color: #b9e8d4; }
|
||||
.status-info { color: #075c70; background: #e4f4f8; border-color: #b7dfe8; }
|
||||
.status-action { color: #6b5100; background: #fff5c9; border-color: #ead66b; }
|
||||
|
||||
.dashboard-grid { display: grid; gap: .7rem; margin-top: 1.3rem; }
|
||||
.dashboard-tile { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .8rem; width: 100%; padding: .9rem; border: 1px solid rgba(7,24,46,.1); border-radius: .85rem; color: var(--brand-ink); background: #fff; text-align: left; cursor: pointer; box-shadow: 0 3px 10px rgba(7,24,46,.04); }
|
||||
.dashboard-tile > span:nth-child(2) { display: grid; }
|
||||
.dashboard-tile small { color: #6b7e89; font-size: .6rem; font-weight: 900; letter-spacing: .08em; text-transform: uppercase; }
|
||||
.dashboard-tile strong { margin-top: .1rem; font-size: .88rem; }
|
||||
.dashboard-tile em { color: #667c89; font-size: .72rem; font-style: normal; }
|
||||
.tile-icon { display: grid; width: 42px; height: 42px; place-items: center; border-radius: .72rem; color: #075f60; background: #e1f3f2; }
|
||||
.tile-icon--blue { color: #176b8a; background: #e1f0f5; }
|
||||
.tile-icon--yellow { color: #725600; background: #fff8c9; }
|
||||
|
||||
.team-highlight h2 { margin: .15rem 0 .35rem; color: var(--brand-ink); font-size: 1.1rem; }
|
||||
.team-highlight p { color: #425e70; }
|
||||
.meta-line { display: inline-flex; align-items: center; gap: .35rem; margin-top: .7rem; color: #637b89; font-size: .72rem; }
|
||||
.meta-line .material-icons { font-size: 1rem; }
|
||||
.member-preview-row { display: flex; padding-left: .3rem; }
|
||||
.member-preview-row .avatar { margin-left: -8px; }
|
||||
.member-preview-row .avatar:first-child { margin-left: 0; }
|
||||
.card-list { overflow: hidden; border: 1px solid rgba(7,24,46,.11); border-radius: 1rem; background: #fff; }
|
||||
.list-row { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .8rem; width: 100%; padding: .9rem 1rem; border: 0; border-bottom: 1px solid rgba(7,24,46,.09); color: var(--brand-ink); background: #fff; text-align: left; cursor: pointer; }
|
||||
.list-row:last-child { border-bottom: 0; }
|
||||
.list-row span:nth-child(2) { display: grid; }
|
||||
.list-row strong { font-size: .84rem; }
|
||||
.list-row small { color: #687f8c; font-size: .7rem; }
|
||||
.list-row__icon { color: #087b7c; }
|
||||
|
||||
.search-row { display: flex; align-items: center; justify-content: space-between; gap: .7rem; margin-bottom: 1rem; }
|
||||
.search-box { display: flex; flex: 1; align-items: center; gap: .55rem; min-height: 44px; padding: 0 .75rem; border: 1px solid rgba(7,24,46,.15); border-radius: .7rem; background: #fff; }
|
||||
.search-box .material-icons { color: #6b7f8c; font-size: 1.1rem; }
|
||||
.search-box input { width: 100%; border: 0; outline: 0; color: var(--brand-ink); background: transparent; }
|
||||
.member-list { display: grid; gap: .65rem; }
|
||||
.member-card { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .8rem; padding: .9rem; border: 1px solid rgba(7,24,46,.11); border-radius: .85rem; background: #fff; }
|
||||
.member-card__main { min-width: 0; }
|
||||
.member-card__main > div { display: flex; flex-wrap: wrap; align-items: center; gap: .45rem; }
|
||||
.member-card h3 { color: var(--brand-ink); font-size: .92rem; }
|
||||
.member-card p { margin-top: .12rem; color: #536d7d; font-size: .73rem; }
|
||||
.member-card a { display: inline-block; margin-top: .2rem; color: #087b7c; font-size: .74rem; font-weight: 800; }
|
||||
.member-card small { color: #6a7d88; font-size: .67rem; }
|
||||
.muted-copy, .muted-icon { color: #81909a !important; }
|
||||
.member-card--admin-extra { border-style: dashed; background: #f9fcfd; }
|
||||
.context-note { display: flex; align-items: flex-start; gap: .6rem; margin-top: 1rem; padding: .8rem .9rem; border-left: 4px solid var(--brand-blue); border-radius: .55rem; color: #3a596b; background: #eaf4f7; font-size: .75rem; line-height: 1.5; }
|
||||
.context-note .material-icons { color: #176b8a; font-size: 1.05rem; }
|
||||
.context-note--strong { margin: 0 0 1.2rem; border-left-color: var(--brand-teal); background: #e7f5f4; }
|
||||
|
||||
.period-progress { margin-bottom: 1rem; }
|
||||
.period-progress__top { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .6rem; }
|
||||
.period-progress__top strong { color: var(--brand-ink); font-size: .8rem; }
|
||||
.progress-track { height: 8px; margin: .8rem 0 .45rem; overflow: hidden; border-radius: 999px; background: #dcebed; }
|
||||
.progress-track span { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--brand-teal), var(--brand-blue)); }
|
||||
.period-progress p { color: #687e8b; font-size: .72rem; }
|
||||
.availability-list { display: grid; gap: .7rem; }
|
||||
.availability-card { display: grid; grid-template-columns: auto 1fr; gap: .8rem; padding: .85rem; border: 1px solid rgba(7,24,46,.11); border-radius: .9rem; background: #fff; }
|
||||
.availability-card--urgent { border-left: 4px solid var(--brand-coral); }
|
||||
.availability-card__main h3 { color: var(--brand-ink); font-size: .9rem; }
|
||||
.availability-card__main p { color: #607886; font-size: .72rem; }
|
||||
.need-row { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .4rem; }
|
||||
.need-row span { padding: .18rem .45rem; border-radius: .4rem; color: #496777; background: #edf5f6; font-size: .65rem; font-weight: 700; }
|
||||
.availability-actions { grid-column: 1 / -1; display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; }
|
||||
.answer-btn { display: inline-flex; align-items: center; justify-content: center; gap: .3rem; min-height: 44px; border: 1px solid rgba(7,24,46,.15); border-radius: .6rem; color: #526b7b; background: #fff; cursor: pointer; font-weight: 800; }
|
||||
.answer-btn .material-icons { font-size: 1rem; }
|
||||
.answer-btn--yes.is-selected { color: #075f60; border-color: #62bdb0; background: #ddf5f1; }
|
||||
.answer-btn--no.is-selected { color: #a82440; border-color: #ef9bad; background: #fff0f3; }
|
||||
.title-with-badge { display: flex; flex-wrap: wrap; align-items: center; gap: .45rem; }
|
||||
|
||||
.segmented { display: inline-grid; grid-template-columns: 1fr 1fr; width: 100%; margin-bottom: 1rem; padding: 4px; border-radius: .75rem; background: #dcebed; }
|
||||
.segmented button { min-height: 40px; border: 0; border-radius: .55rem; color: #5b7482; background: transparent; font-weight: 800; cursor: pointer; }
|
||||
.segmented button.is-active { color: var(--brand-ink); background: #fff; box-shadow: 0 2px 8px rgba(7,24,46,.08); }
|
||||
.segmented span { display: inline-grid; min-width: 20px; height: 20px; margin-left: .25rem; place-items: center; border-radius: 999px; background: #e7f1f3; font-size: .64rem; }
|
||||
.document-list { display: grid; gap: .7rem; }
|
||||
.document-card { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .8rem; padding: .9rem; border: 1px solid rgba(7,24,46,.11); border-radius: .9rem; background: #fff; }
|
||||
.document-card h3 { color: var(--brand-ink); font-size: .88rem; }
|
||||
.document-card p { margin: .12rem 0 .3rem; color: #506b7b; font-size: .72rem; }
|
||||
.document-card > div:nth-child(2) > span:last-child { color: #74858f; font-size: .65rem; }
|
||||
.file-icon, .file-thumb { display: grid; width: 48px; height: 56px; place-items: center; border-radius: .55rem; color: #176b8a; background: #e2f0f5; font-size: .62rem; font-weight: 900; }
|
||||
.file-icon--pdf { color: #ad2946; background: #fbe8ec; }
|
||||
.file-thumb { color: #075f60; background: linear-gradient(145deg, #dff5f1, #c6e9e8); }
|
||||
.document-card--pending { border-style: dashed; background: #fffdf2; }
|
||||
|
||||
.channel-layout { display: grid; gap: .7rem; }
|
||||
.channel-list { display: flex; gap: .45rem; overflow-x: auto; padding-bottom: .2rem; }
|
||||
.channel-item { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .5rem; min-width: 180px; min-height: 58px; padding: .6rem .7rem; border: 1px solid rgba(7,24,46,.11); border-radius: .7rem; color: var(--brand-ink); background: #fff; text-align: left; cursor: pointer; }
|
||||
.channel-item.is-active { border-color: #61bcb7; background: #e6f5f3; }
|
||||
.channel-item .material-icons { color: #087b7c; font-size: 1.1rem; }
|
||||
.channel-item span:nth-child(2) { display: grid; }
|
||||
.channel-item strong { font-size: .78rem; }
|
||||
.channel-item small { color: #667c89; font-size: .62rem; }
|
||||
.chat-pane { display: flex; min-height: 560px; flex-direction: column; overflow: hidden; border: 1px solid rgba(7,24,46,.11); border-radius: 1rem; background: #fff; box-shadow: 0 5px 18px rgba(7,24,46,.05); }
|
||||
.chat-pane__header { display: flex; min-height: 62px; align-items: center; justify-content: space-between; padding: .7rem .9rem; border-bottom: 1px solid rgba(7,24,46,.1); background: #edf7f8; }
|
||||
.chat-pane__header strong, .chat-pane__header span { display: block; }
|
||||
.chat-pane__header strong { color: var(--brand-ink); }
|
||||
.chat-pane__header span { color: #6c818c; font-size: .66rem; }
|
||||
.message-stream { display: flex; flex: 1; flex-direction: column; gap: .85rem; padding: .9rem; overflow-y: auto; background: linear-gradient(#f8fcfd, #fff); }
|
||||
.chat-day { display: flex; align-items: center; gap: .5rem; color: #87959e; font-size: .62rem; font-weight: 800; text-transform: uppercase; }
|
||||
.chat-day::before, .chat-day::after { content: ''; flex: 1; height: 1px; background: #dce9ec; }
|
||||
.message-bubble { max-width: 88%; align-self: flex-start; padding: .7rem .8rem; border: 1px solid #dce7ea; border-radius: .9rem .9rem .9rem .2rem; background: #fff; box-shadow: 0 2px 6px rgba(7,24,46,.04); }
|
||||
.message-bubble--own { align-self: flex-end; border-color: #9ddbd4; border-radius: .9rem .9rem .2rem .9rem; background: #def3f0; }
|
||||
.message-bubble--reply { border-left: 4px solid var(--brand-blue); }
|
||||
.message-author { display: flex; align-items: center; gap: .4rem; margin-bottom: .25rem; }
|
||||
.message-author strong { color: var(--brand-ink); font-size: .72rem; }
|
||||
.message-author time { margin-left: auto; color: #7e9099; font-size: .6rem; }
|
||||
.message-bubble p { color: #304d5e; font-size: .78rem; line-height: 1.48; }
|
||||
.message-state { margin-top: .32rem; color: #86949b; font-size: .58rem; text-align: right; }
|
||||
.message-state--read { color: #087b7c; font-weight: 800; }
|
||||
.reply-reference { margin: -.15rem 0 .45rem; padding: .3rem .45rem; border-left: 3px solid var(--brand-blue); color: #48687a; background: #edf5f8; font-size: .62rem; }
|
||||
.chat-composer { padding: .7rem; border-top: 1px solid rgba(7,24,46,.1); background: #edf7f8; }
|
||||
.chat-composer textarea { width: 100%; min-height: 76px; padding: .65rem; border: 1px solid rgba(7,24,46,.15); border-radius: .65rem; color: var(--brand-ink); background: #fff; resize: none; }
|
||||
.chat-composer > div { display: flex; align-items: center; justify-content: space-between; gap: .5rem; margin-top: .4rem; }
|
||||
.composer-hint { color: #73858f; font-size: .58rem; }
|
||||
.send-button { display: grid; width: 44px; height: 44px; place-items: center; border: 0; border-radius: .65rem; color: #fff; background: #087b7c; cursor: pointer; }
|
||||
|
||||
.profile-grid { display: grid; gap: .9rem; }
|
||||
.profile-identity { display: flex; align-items: center; gap: .9rem; margin-bottom: 1rem; }
|
||||
.profile-identity h2 { color: var(--brand-ink); font-size: 1.15rem; }
|
||||
.profile-identity span { color: #6f828d; font-size: .72rem; }
|
||||
.avatar-picker { display: grid; grid-template-columns: repeat(3, 1fr); gap: .55rem; margin: 1rem 0; }
|
||||
.avatar-choice { display: grid; justify-items: center; gap: .3rem; min-height: 84px; padding: .55rem; border: 1px solid rgba(7,24,46,.14); border-radius: .65rem; color: #526b7b; background: #fff; cursor: pointer; }
|
||||
.avatar-choice.is-selected { border-color: #087b7c; box-shadow: inset 0 0 0 1px #087b7c; background: #eef9f8; }
|
||||
.avatar-choice span:last-child { font-size: .65rem; font-weight: 800; }
|
||||
.avatar-picker--compact { grid-template-columns: repeat(3, minmax(70px, 110px)); }
|
||||
.toggle-row { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: .8rem; margin-bottom: 1rem; cursor: pointer; }
|
||||
.toggle-row strong, .toggle-row small { display: block; }
|
||||
.toggle-row strong { color: var(--brand-ink); font-size: .84rem; }
|
||||
.toggle-row small { color: #6d818d; font-size: .68rem; }
|
||||
.toggle-row input { position: absolute; opacity: 0; }
|
||||
.toggle-ui { position: relative; width: 46px; height: 26px; border-radius: 999px; background: #c8d6db; transition: .18s ease; }
|
||||
.toggle-ui::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: #fff; box-shadow: 0 2px 5px rgba(7,24,46,.2); transition: .18s ease; }
|
||||
.toggle-row input:checked + .toggle-ui { background: #087b7c; }
|
||||
.toggle-row input:checked + .toggle-ui::after { transform: translateX(20px); }
|
||||
.choice-pills { display: flex; flex-wrap: wrap; gap: .4rem; }
|
||||
.choice-pills button { min-height: 38px; padding: .35rem .65rem; border: 1px solid rgba(7,24,46,.14); border-radius: 999px; color: #566f7e; background: #fff; cursor: pointer; }
|
||||
.choice-pills button.is-selected { color: #075f60; border-color: #6dbfb8; background: #def3ef; }
|
||||
.device-row { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .7rem; padding: .85rem 1rem; border-top: 1px solid rgba(7,24,46,.09); }
|
||||
.device-row:first-of-type { border-top: 0; }
|
||||
.device-row > .material-icons { color: #087b7c; }
|
||||
.device-row strong, .device-row small { display: block; }
|
||||
.device-row strong { color: var(--brand-ink); font-size: .78rem; }
|
||||
.device-row small { color: #71858f; font-size: .65rem; }
|
||||
|
||||
.notification-list { display: grid; gap: .7rem; }
|
||||
.notification-card { display: grid; grid-template-columns: auto 1fr; gap: .75rem; padding: .9rem; border: 1px solid rgba(7,24,46,.1); border-radius: .85rem; background: #fff; }
|
||||
.notification-card > .material-icons { display: grid; width: 40px; height: 40px; place-items: center; border-radius: .65rem; color: #176b8a; background: #e3f1f5; }
|
||||
.notification-card--urgent { border-left: 4px solid var(--brand-coral); }
|
||||
.notification-card--urgent > .material-icons { color: #b62542; background: #ffe7ec; }
|
||||
.notification-card--important { border-left: 4px solid #d7ad32; }
|
||||
.notification-card--important > .material-icons { color: #805d00; background: #fff5cc; }
|
||||
.notification-card h3 { color: var(--brand-ink); font-size: .84rem; }
|
||||
.notification-card p { margin: .12rem 0 .3rem; color: #536d7d; font-size: .72rem; }
|
||||
.notification-card time { color: #86959d; font-size: .62rem; }
|
||||
|
||||
.admin-kpi-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: .7rem; margin-bottom: 1.2rem; }
|
||||
.admin-kpi { display: grid; min-height: 122px; align-content: center; justify-items: center; gap: .15rem; padding: .8rem; border: 1px solid rgba(7,24,46,.11); border-top: 4px solid #087b7c; border-radius: .85rem; color: var(--brand-ink); background: #fff; cursor: pointer; }
|
||||
.admin-kpi .material-icons { color: #087b7c; }
|
||||
.admin-kpi strong { font-family: var(--font-display); font-size: 2.2rem; line-height: 1; }
|
||||
.admin-kpi small { color: #667d8a; text-align: center; }
|
||||
.admin-kpi--urgent { border-top-color: var(--brand-coral); }
|
||||
.admin-kpi--urgent .material-icons, .admin-kpi--urgent strong { color: #b62542; }
|
||||
.management-section { margin-bottom: 1rem; }
|
||||
.management-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .8rem; padding: .9rem 1rem; border-top: 1px solid rgba(7,24,46,.09); }
|
||||
.management-row:first-of-type { border-top: 0; }
|
||||
.management-row strong, .management-row span { display: block; }
|
||||
.management-row strong { color: var(--brand-ink); font-size: .8rem; }
|
||||
.management-row span { color: #617987; font-size: .68rem; }
|
||||
.fairness-table { display: grid; }
|
||||
.fairness-table > div { display: grid; grid-template-columns: 1fr 70px 110px; gap: .6rem; padding: .75rem 1rem; border-top: 1px solid rgba(7,24,46,.08); color: #526d7d; font-size: .72rem; }
|
||||
.fairness-table .fairness-head { color: #71848e; background: #f6fafb; font-size: .62rem; font-weight: 900; text-transform: uppercase; }
|
||||
.fairness-table strong { color: var(--brand-ink); }
|
||||
|
||||
.org-grid { display: grid; gap: .75rem; }
|
||||
.org-card { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .8rem; padding: 1rem; border: 1px solid rgba(7,24,46,.11); border-radius: .9rem; color: var(--brand-ink); background: #fff; text-align: left; cursor: pointer; }
|
||||
.org-card > .material-icons:first-child { display: grid; width: 44px; height: 44px; place-items: center; border-radius: .7rem; color: #075f60; background: #e3f4f2; }
|
||||
.org-card strong, .org-card small { display: block; }
|
||||
.org-card strong { font-size: .86rem; }
|
||||
.org-card small { color: #687f8c; font-size: .68rem; }
|
||||
.org-card > .material-icons:last-child { color: #80919a; }
|
||||
|
||||
.filter-bar { display: grid; gap: .7rem; margin-bottom: 1rem; padding: .8rem; }
|
||||
.audit-list { overflow: hidden; }
|
||||
.audit-row { display: grid; grid-template-columns: auto 1fr; gap: .75rem; padding: .95rem 1rem; border-top: 1px solid rgba(7,24,46,.09); }
|
||||
.audit-row:first-child { border-top: 0; }
|
||||
.audit-icon { display: grid; width: 40px; height: 40px; place-items: center; border-radius: .65rem; color: #075f60; background: #e2f3f1; }
|
||||
.audit-icon--danger { color: #ad2946; background: #fde8ed; }
|
||||
.audit-row strong { color: var(--brand-ink); font-size: .8rem; }
|
||||
.audit-row p { margin: .15rem 0 .3rem; color: #4b6777; font-size: .72rem; }
|
||||
.audit-row div > span { color: #84949c; font-size: .62rem; }
|
||||
|
||||
.screen--auth { min-height: calc(100vh - 9rem); display: grid; place-items: center; }
|
||||
.auth-panel { width: min(100%, 430px); margin: 0 auto; }
|
||||
.auth-panel--wide { width: min(100%, 700px); }
|
||||
.auth-brand { margin-bottom: 1rem; text-align: center; }
|
||||
.auth-brand h1 { margin: 0 0 .6rem; color: var(--brand-ink); font-family: var(--font-display); font-size: clamp(2.5rem, 10vw, 4rem); line-height: .88; text-transform: uppercase; }
|
||||
.auth-brand p { color: #526e7e; }
|
||||
.auth-card { width: 100%; }
|
||||
.auth-footnote { margin-top: .8rem; color: #71848f; font-size: .68rem; text-align: center; }
|
||||
.code-inputs { display: grid; grid-template-columns: repeat(6, 1fr); gap: .35rem; margin-bottom: 1rem; }
|
||||
.code-inputs input { width: 100%; min-width: 0; height: 52px; border: 1px solid rgba(7,24,46,.16); border-radius: .55rem; color: var(--brand-ink); background: #f8fcfd; text-align: center; font-size: 1.2rem; font-weight: 800; }
|
||||
.auth-card .btn + .btn { margin-top: .55rem; }
|
||||
.team-choice-grid { display: grid; gap: .7rem; }
|
||||
.team-choice { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .8rem; padding: 1rem; border: 1px solid rgba(7,24,46,.12); border-radius: .9rem; color: var(--brand-ink); background: #fff; text-align: left; cursor: pointer; }
|
||||
.team-choice.is-selected { border-color: #6cbfb8; box-shadow: inset 4px 0 0 #087b7c; }
|
||||
.team-choice__icon { display: grid; width: 46px; height: 46px; place-items: center; border-radius: .7rem; color: #075f60; background: #e0f3f1; }
|
||||
.team-choice strong, .team-choice small { display: block; }
|
||||
.team-choice small { color: #6b808b; font-size: .68rem; }
|
||||
|
||||
.bottom-nav { position: fixed; inset: auto 0 0 0; z-index: 35; display: grid; grid-template-columns: repeat(6, 1fr); min-height: 66px; padding: 4px max(2px, env(safe-area-inset-right)) max(4px, env(safe-area-inset-bottom)) max(2px, env(safe-area-inset-left)); border-top: 1px solid rgba(255,255,255,.12); background: rgba(6,17,38,.97); box-shadow: 0 -5px 20px rgba(6,17,38,.18); backdrop-filter: blur(14px); }
|
||||
.bottom-nav__item { position: relative; display: grid; min-width: 0; min-height: 56px; place-items: center; align-content: center; gap: 1px; padding: 4px 1px; border: 0; border-radius: .55rem; color: rgba(255,255,255,.58); background: transparent; cursor: pointer; font-size: .56rem; font-weight: 750; }
|
||||
.bottom-nav__item .material-icons { font-size: 1.25rem; }
|
||||
.bottom-nav__item.is-active { color: var(--brand-aqua); background: rgba(82,224,207,.1); }
|
||||
.bottom-nav__item i { position: absolute; top: 2px; right: calc(50% - 18px); display: grid; min-width: 16px; height: 16px; place-items: center; border-radius: 999px; color: #fff; background: var(--brand-coral); font-size: .55rem; font-style: normal; font-weight: 900; }
|
||||
|
||||
@media (min-width: 680px) {
|
||||
main.app-main { padding: 1.7rem 1.4rem 7rem; }
|
||||
.dashboard-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.availability-card { grid-template-columns: auto 1fr auto; align-items: center; }
|
||||
.availability-actions { grid-column: auto; display: flex; }
|
||||
.answer-btn { min-width: 68px; }
|
||||
.profile-grid { grid-template-columns: 1fr 1fr; }
|
||||
.profile-card--wide { grid-column: 1 / -1; }
|
||||
.admin-kpi-grid { grid-template-columns: repeat(4, 1fr); }
|
||||
.org-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.filter-bar { grid-template-columns: 1fr 220px; align-items: center; }
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.mock-sidebar { display: flex; }
|
||||
.mobile-header, .bottom-nav { display: none; }
|
||||
main.app-main { margin-left: 260px; min-height: 100vh; padding: 5.7rem 2rem 4rem; }
|
||||
.desktop-topbar { position: fixed; top: 0; right: 0; left: 260px; z-index: 25; display: flex; min-height: 64px; align-items: center; justify-content: space-between; padding: 0 2rem; border-bottom: 1px solid rgba(7,24,46,.08); background: rgba(244,250,252,.92); backdrop-filter: blur(12px); }
|
||||
.route-chip { padding: .35rem .6rem; border: 1px solid rgba(7,24,46,.08); border-radius: .45rem; color: #657c88; background: #fff; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .72rem; }
|
||||
.desktop-actions { display: flex; align-items: center; gap: .5rem; }
|
||||
.screen { margin: 0; }
|
||||
.member-hero { padding-top: .4rem; }
|
||||
.shift-stack { grid-template-columns: repeat(2, 1fr); }
|
||||
.channel-layout { grid-template-columns: 250px minmax(0, 1fr); align-items: start; }
|
||||
.channel-list { display: grid; overflow: visible; }
|
||||
.channel-item { min-width: 0; }
|
||||
.org-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
main.app-main { padding-right: 3rem; padding-left: 3rem; }
|
||||
.dashboard-grid { grid-template-columns: repeat(4, 1fr); }
|
||||
.dashboard-tile { grid-template-columns: auto 1fr; }
|
||||
.dashboard-tile > .material-icons:last-child { display: none; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.screen { animation: none; }
|
||||
* { scroll-behavior: auto !important; }
|
||||
}
|
||||
4313
notes/style(.css
Normal file
4313
notes/style(.css
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue