banyaro/backend/static/css/layout.css
rene 1ff66a7083 Sicherheit + Tests + A11y, SW by-v1118
PYDANTIC max_length (38 Routen, ~400 Field-Constraints):
Schützt vor DoS durch Riesen-Payloads (10MB Thread-Titel etc.).
Pragmatische Limits:
- Titel/Name: 200 · Beschreibung/Body: 10000 · Notiz: 5000
- Email: 254 (RFC 5321) · URL: 500 · Slug/Kategorie: 100
- Hund-Name/Rasse: 80 · Hund-Bio: 2000

Top-betroffen: forum.py, diary.py, health.py, dogs.py, expenses.py,
notes.py, auth.py, profile.py. Manuelle len()-Checks in profile,
chat, ki entfernt (jetzt durch Field abgedeckt).

PYTEST COVERAGE (+19 Tests, 37 grün + 1 xfail):
- test_security.py: require_owner (Places GET/PATCH/DELETE mit
  Fremduser → 403), JWT-Blacklist (Logout invalidiert Token),
  Login-Lockout (5 Fehlversuche → 429 + Retry-After Header)
- test_race.py: Invoice-Counter (20 parallele Threads, alle unique),
  Founder-Number (atomare Vergabe, voll bei 100)
- test_validation.py: Forum-Titel 30k Zeichen → 422, Diary-Text
  50k → 422 (verifiziert Pydantic max_length-Sweep)

A11Y (Tap-Targets ≥44×44 + Dark-Mode-Kontrast):
- #header-user-btn 36→44px, .header-back 40→44, .header-menu-btn 40→44
- dog-profile Wrapped-Slider Prev/Next 40→44
- forum-Lightbox Close 40→44
- --c-text-muted Light: #B0A090 (2.37:1 FAIL) → #7F6B58 (4.74:1 PASS)
- --c-text-muted Dark:  #806A58 (3.58:1 FAIL) → #A08878 (5.46:1 PASS)
- Branding-Farben unangetastet
2026-05-27 13:40:30 +02:00

798 lines
22 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
BAN YARO — Layout
App Shell: Mobile Bottom-Nav + Desktop Sidebar
Mobile-first mit einer einzigen Media Query für Desktop
============================================================ */
/* ------------------------------------------------------------
1. APP SHELL
------------------------------------------------------------ */
#app {
display: flex;
flex-direction: column;
min-height: 100dvh;
}
/* Content-Bereich: füllt den Raum zwischen Header und Bottom-Nav */
#page-content {
flex: 1;
min-height: 0; /* iOS-Flex-Bug: ohne das scrollt body statt #page-content */
overflow-y: auto;
overflow-x: hidden;
padding-top: var(--safe-top);
padding-bottom: calc(var(--nav-bottom-height) + var(--safe-bottom) + 16px);
-webkit-overflow-scrolling: touch;
}
/* Mobile only: iOS-Bounce verhindert dass fixed-Nav driftet */
@media (max-width: 767px) {
#page-content {
overscroll-behavior-y: none;
}
}
/* Desktop: Sidebar-Layout — kein Bottom-Nav, natürliche Höhe */
@media (min-width: 768px) {
#app {
flex-direction: row;
}
#page-content {
min-height: unset;
overflow-y: visible; /* nicht als Scroll-Container — Window scrollt */
overflow-x: visible;
padding-bottom: 0;
padding-left: var(--nav-sidebar-width);
}
}
/* ------------------------------------------------------------
2. HEADER (Mobile)
------------------------------------------------------------ */
#app-header {
position: sticky;
top: 0;
z-index: 100;
min-height: calc(var(--header-height) + var(--safe-top));
padding-top: var(--safe-top);
padding-bottom: var(--space-2);
background: var(--c-surface);
border-bottom: 1px solid var(--c-border-light);
display: flex;
align-items: center;
padding-left: var(--space-4);
padding-right: var(--space-4);
gap: var(--space-3);
box-shadow: var(--shadow-xs);
}
.header-title {
font-size: var(--text-lg);
font-weight: var(--weight-bold);
color: var(--c-text);
flex: 1;
}
/* Dog Switcher Container im Header */
#header-dog-switcher {
flex: 1;
display: flex;
align-items: center;
gap: var(--space-2);
min-width: 0;
overflow: visible;
}
.header-back {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: var(--radius-md);
color: var(--c-text-secondary);
cursor: pointer;
transition: background var(--transition-fast);
font-size: 20px;
flex-shrink: 0;
}
.header-back:hover { background: var(--c-surface-2); }
/* Hamburger-Button (nur Mobile) */
.header-menu-btn {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
font-size: 1.3rem;
color: var(--c-text);
cursor: pointer;
border-radius: var(--radius-md);
flex-shrink: 0;
-webkit-tap-highlight-color: transparent;
}
.header-menu-btn:hover { background: var(--c-surface-2); }
/* Backdrop für mobile Sidebar */
.sidebar-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.45);
z-index: 499;
}
.sidebar-backdrop.visible { display: block; }
/* Auf Desktop keinen mobilen Header zeigen */
@media (min-width: 768px) {
#app-header { display: none; }
.header-menu-btn { display: none; }
.sidebar-backdrop { display: none !important; }
}
/* Karte: volle Höhe, kein Scrollen, kein Padding */
#page-map {
height: 100%;
overflow: hidden;
}
#page-map > .page-body {
padding: 0 !important;
overflow: hidden;
height: 100%;
}
/* Gassi-Treffen + Sitting: volle Höhe, internes Scroll */
#page-walks,
#page-sitting,
#page-chat {
height: 100%;
overflow: hidden;
}
#page-walks > .page-body,
#page-sitting > .page-body,
#page-chat > .page-body {
padding: 0 !important;
gap: 0 !important;
overflow: hidden;
height: 100%;
position: relative;
}
/* Routen: volle Höhe damit .rk-layout height:100% auflöst und
das Grid intern scrollt (nicht die gesamte Seite via #page-content) */
#page-routes {
height: 100%;
overflow: hidden;
}
#page-routes > .page-body {
padding: 0 !important;
overflow: hidden;
height: 100%;
}
/* ------------------------------------------------------------
3. BOTTOM NAVIGATION (Mobile)
------------------------------------------------------------ */
#bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 700; /* über Leaflet-Panes (~400) */
/* GPU-Layer erzwingen → iOS Safari fixed-position Stabilität */
transform: translateZ(0);
-webkit-transform: translateZ(0);
will-change: transform;
min-height: calc(var(--nav-bottom-height) + var(--safe-bottom));
padding-top: var(--space-1);
padding-bottom: calc(var(--safe-bottom) + var(--space-1));
background: var(--c-surface);
border-top: 1px solid var(--c-border-light);
display: flex;
align-items: stretch;
box-shadow: 0 -2px 12px rgba(42, 31, 20, 0.08);
transition: border-top-color 0.4s ease;
}
@keyframes nav-alert-pulse {
0%, 100% { box-shadow: 0 -2px 8px rgba(42,31,20,0.06); }
50% { box-shadow: 0 -6px 28px var(--nav-alert-color, rgba(220,38,38,0.65)); }
}
#bottom-nav.alert-poison {
border-top: 4px solid var(--c-danger);
--nav-alert-color: rgba(220, 38, 38, 0.65);
animation: nav-alert-pulse 1.6s ease-in-out infinite;
}
#bottom-nav.alert-lost {
border-top: 4px solid #f59e0b;
--nav-alert-color: rgba(245, 158, 11, 0.65);
animation: nav-alert-pulse 1.6s ease-in-out infinite;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
cursor: pointer;
color: var(--c-icon, var(--c-text-secondary));
transition: color var(--transition-fast);
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
position: relative;
padding: var(--space-2) var(--space-1);
}
.nav-item:hover { color: var(--c-text-secondary); }
.nav-item.active { color: var(--c-primary); }
/* Aktiv-Indikator oben */
.nav-item.active::before {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 32px;
height: 3px;
background: var(--c-primary);
border-radius: 0 0 var(--radius-full) var(--radius-full);
}
.nav-item-icon {
font-size: 22px;
line-height: 1;
position: relative;
filter: saturate(0.35) brightness(0.75);
transition: filter var(--transition-fast);
}
.nav-item.active .nav-item-icon {
filter: sepia(0.7) saturate(1.8) hue-rotate(-8deg) brightness(0.9);
}
/* Ungelesen-Badge auf Nav-Icon */
.nav-badge {
position: absolute;
top: -4px;
right: -6px;
background: var(--c-danger);
color: #fff;
font-size: 9px;
font-weight: var(--weight-bold);
min-width: 16px;
height: 16px;
border-radius: var(--radius-full);
display: flex;
align-items: center;
justify-content: center;
padding: 0 3px;
border: 2px solid var(--c-surface);
pointer-events: none;
}
.nav-item-label {
font-size: clamp(9px, 0.6rem, 11px);
font-weight: var(--weight-semibold);
line-height: 1;
white-space: nowrap;
}
/* Mittlerer + Button: kein Label, größer */
.nav-item-center .nav-item-icon {
width: 50px;
height: 50px;
background: var(--c-primary);
color: #fff;
border-radius: var(--radius-full);
font-size: 26px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--shadow-md);
transition: transform var(--transition-fast),
box-shadow var(--transition-fast);
margin-top: -12px; /* ragt über die Nav hinaus */
}
.nav-item-center:active .nav-item-icon {
transform: scale(0.93);
box-shadow: var(--shadow-sm);
}
.nav-item-center.active::before { display: none; }
.nav-item-center .nav-item-label { display: none; }
/* Desktop: Bottom Nav ausblenden */
@media (min-width: 768px) {
#bottom-nav { display: none; }
}
/* ------------------------------------------------------------
4. SIDEBAR NAVIGATION (Desktop)
------------------------------------------------------------ */
#sidebar {
display: none;
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: var(--nav-sidebar-width);
z-index: 300;
background: var(--c-surface);
border-right: 1px solid var(--c-border-light);
flex-direction: column;
overflow: hidden; /* Sidebar selbst scrollt nicht */
box-shadow: var(--shadow-sm);
}
@media (min-width: 768px) {
#sidebar { display: flex; }
}
/* Mobile Sidebar als Drawer */
@media (max-width: 767px) {
#sidebar {
display: flex !important; /* überschreibt display:none aus Base */
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: var(--nav-sidebar-width);
z-index: 1000; /* klar über Backdrop (999) */
transform: translateX(-100%);
transition: transform 0.28s ease;
background: var(--c-surface);
box-shadow: none;
}
#sidebar.open {
transform: translateX(0);
box-shadow: 4px 0 24px rgba(0,0,0,0.18);
}
.sidebar-backdrop {
z-index: 999; /* unter Sidebar, über Seiteninhalt */
}
}
.sidebar-logo {
padding: calc(var(--space-6) + var(--safe-top)) var(--space-5) var(--space-6);
display: flex;
align-items: center;
gap: var(--space-3);
border-bottom: 1px solid var(--c-border-light);
flex-shrink: 0;
}
.sidebar-logo-img {
width: 36px;
height: 36px;
border-radius: var(--radius-full);
object-fit: cover;
}
.sidebar-logo-text {
font-size: var(--text-lg);
font-weight: var(--weight-bold);
color: var(--c-text);
}
.sidebar-add {
padding: var(--space-4) var(--space-4) var(--space-2);
flex-shrink: 0;
}
.sidebar-nav {
flex: 1;
padding: var(--space-2) var(--space-2) var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-1);
overflow-y: auto;
min-height: 0; /* wichtig: flex-child darf kleiner werden als Inhalt */
/* Firefox */
scrollbar-width: thin;
scrollbar-color: var(--c-primary) var(--c-surface);
}
.sidebar-nav::-webkit-scrollbar { width: 6px; }
.sidebar-nav::-webkit-scrollbar-track { background: var(--c-surface); }
.sidebar-nav::-webkit-scrollbar-thumb {
background: var(--c-primary);
border-radius: 3px;
}
.sidebar-nav::-webkit-scrollbar-thumb:hover { background: var(--c-primary-dark); }
.sidebar-section-label {
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
color: var(--c-text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
padding: var(--space-3) var(--space-3) var(--space-1);
margin-top: var(--space-2);
}
.sidebar-section-toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
background: none;
border: none;
cursor: pointer;
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
color: var(--c-text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
padding: var(--space-3) var(--space-3) var(--space-1);
margin-top: var(--space-2);
border-radius: var(--radius-sm);
transition: color var(--transition-fast);
-webkit-tap-highlight-color: transparent;
}
.sidebar-section-toggle:hover { color: var(--c-text); }
.sidebar-section-toggle .wissen-caret {
transition: transform 0.2s ease;
flex-shrink: 0;
}
.sidebar-section-toggle[aria-expanded="true"] .wissen-caret {
transform: rotate(90deg);
}
.sidebar-section-body {
display: flex;
flex-direction: column;
gap: var(--space-1);
overflow: hidden;
max-height: 0;
opacity: 0;
transition: max-height 0.25s ease, opacity 0.2s ease;
}
.sidebar-section-body.open {
max-height: 300px;
opacity: 1;
}
.sidebar-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3);
border-radius: var(--radius-md);
cursor: pointer;
color: var(--c-text-secondary);
font-size: var(--text-sm);
font-weight: var(--weight-medium);
transition: background var(--transition-fast),
color var(--transition-fast);
-webkit-tap-highlight-color: transparent;
}
.sidebar-item:hover {
background: var(--c-bg);
color: var(--c-text);
}
.sidebar-item.active {
background: var(--c-primary-subtle);
color: var(--c-primary-dark);
font-weight: var(--weight-semibold);
}
/* User-Eintrag bekommt nie den Active-Stil */
.sidebar-item--user.active {
background: transparent;
color: var(--c-text-secondary);
font-weight: var(--weight-medium);
}
.sidebar-item-icon {
font-size: 18px;
width: 24px;
text-align: center;
flex-shrink: 0;
filter: saturate(0.35) brightness(0.75);
transition: filter var(--transition-fast);
}
.sidebar-item.active .sidebar-item-icon {
filter: sepia(0.7) saturate(1.8) hue-rotate(-8deg) brightness(0.9);
}
.sidebar-item-badge {
margin-left: auto;
background: var(--c-danger);
color: #fff;
font-size: var(--text-xs);
font-weight: var(--weight-bold);
min-width: 20px;
height: 20px;
border-radius: var(--radius-full);
display: flex;
align-items: center;
justify-content: center;
padding: 0 var(--space-1);
}
/* sidebar-footer entfernt — Einstellungen/Konto sind jetzt Teil der scrollbaren Nav */
/* ------------------------------------------------------------
5. PAGE WRAPPER (inneres Layout der Seiten)
------------------------------------------------------------ */
.page {
display: none;
flex-direction: column;
min-height: 100%;
}
.page.active { display: flex; }
/* Scrollbarer Seiteninhalt */
.page-body {
flex: 1;
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
/* Volle Breite mit seitlichem Limit für Desktop */
.page-container {
width: 100%;
max-width: 680px;
margin: 0 auto;
}
/* Desktop: Standard-Container auf 860px erweitern (768px1023px) */
@media (min-width: 768px) {
.page-container { max-width: 860px; }
}
/* Desktop-Breite: von app.js nach Page-Init gesetzt */
@media (min-width: 768px) {
.pc-desktop {
max-width: 860px !important;
margin-left: auto !important;
margin-right: auto !important;
}
}
/* Wide-Layout für Karte und ähnliches */
.page-container-wide {
width: 100%;
max-width: 1040px;
margin: 0 auto;
}
/* Karte nimmt vollen Platz ein */
.page-fullscreen {
position: absolute;
inset: 0;
padding: 0;
}
/* ------------------------------------------------------------
6. PULL-TO-REFRESH INDIKATOR
------------------------------------------------------------ */
.ptr-indicator {
display: flex;
justify-content: center;
align-items: center;
height: 48px;
color: var(--c-text-muted);
font-size: var(--text-sm);
gap: var(--space-2);
overflow: hidden;
max-height: 0;
transition: max-height var(--transition-normal);
}
.ptr-indicator.visible { max-height: 48px; }
/* ------------------------------------------------------------
7. RESPONSIVE GRID
------------------------------------------------------------ */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-3);
}
.grid-3 {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: var(--space-3);
}
@media (max-width: 400px) {
.grid-3 { grid-template-columns: 1fr 1fr; }
}
/* ============================================================
Desktop Multi-Column Layouts (min-width: 1024px)
============================================================ */
@media (min-width: 1024px) {
/* Admin: breit + Sidebar-Layout */
#page-admin .page-container { max-width: 1200px; }
#page-admin .adm-shell {
display: flex;
gap: var(--space-4);
align-items: flex-start;
}
#page-admin .adm-tabs {
display: flex !important;
flex-direction: column;
width: 190px;
flex-shrink: 0;
gap: var(--space-1);
padding-bottom: 0;
position: sticky;
top: var(--space-3);
}
#page-admin .adm-tabs .by-tab {
justify-content: flex-start !important;
text-align: left !important;
padding-left: var(--space-3);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#page-admin #adm-content {
flex: 1;
min-width: 0;
}
/* ----------------------------------------------------------
WELCOME: 2-spaltige Feature-Sections, zentrierter Hero
---------------------------------------------------------- */
.welcome-layout { max-width: 920px; }
.welcome-sections {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-4);
align-items: start;
}
.welcome-sections .card { margin-bottom: 0; }
/* ----------------------------------------------------------
WALKS: Liste (links) + Karte (rechts) nebeneinander
---------------------------------------------------------- */
#page-walks .page-container { max-width: 860px; }
.walks-layout {
flex-direction: row;
align-items: stretch;
}
/* View-Toggle auf Desktop nicht nötig */
.walks-view-toggle { display: none; }
/* Beide Panels immer sichtbar */
#walks-list-view,
#walks-map-view {
display: flex !important;
flex-direction: column;
}
#walks-list-view {
width: 340px;
flex-shrink: 0;
border-right: 1px solid var(--c-border-light);
}
#walks-map-view { flex: 1; }
/* Toolbar-Zeile kompakter da Toggle wegfällt */
#page-walks .by-toolbar { padding: var(--space-2) var(--space-4); }
/* ----------------------------------------------------------
CHAT: Split-Pane zentriert mit max-width
---------------------------------------------------------- */
#page-chat .page-body {
display: flex;
flex-direction: column;
align-items: stretch;
max-width: 860px;
margin: 0 auto;
width: 100%;
}
#chat-split { min-height: 600px; }
/* ----------------------------------------------------------
FORUM: Rubriken über volle Breite, Threads darunter
---------------------------------------------------------- */
#page-forum .page-container { max-width: 860px; }
/* Rubriken auf genau 2 Zeilen verteilen, zentriert
— #page-forum nötig für Spezifität > .by-tabs (components.css lädt später) */
#page-forum .forum-category-tabs {
display: grid;
grid-template-columns: repeat(var(--forum-tab-cols, 7), minmax(0, 1fr));
overflow: visible;
gap: var(--space-1);
padding-bottom: var(--space-2);
}
#page-forum .forum-category-tabs .by-tab {
justify-content: center;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Gesundheit: Tabs auf 2 Zeilen */
#page-health .by-tabs {
display: grid;
grid-template-columns: repeat(var(--health-tab-cols, 5), minmax(0, 1fr));
overflow: visible;
gap: var(--space-1);
padding-bottom: var(--space-2);
}
#page-health .by-tabs .by-tab {
justify-content: center;
text-align: center;
white-space: nowrap;
}
/* Admin: Tabs als 2-zeiliges Grid (Mobile/Tablet) */
#page-admin .adm-tabs {
display: grid;
grid-template-columns: repeat(var(--adm-tab-cols, 4), minmax(0, 1fr));
overflow: visible;
gap: var(--space-1);
padding-bottom: var(--space-2);
}
#page-admin .adm-tabs .by-tab {
justify-content: center;
text-align: center;
white-space: nowrap;
}
/* Übungen: Tabs auf 2 Zeilen */
#page-uebungen #ueb-tabs {
display: grid;
grid-template-columns: repeat(var(--ueb-tab-cols, 5), minmax(0, 1fr));
overflow: visible;
gap: var(--space-1);
padding-bottom: var(--space-2);
}
#page-uebungen #ueb-tabs .by-tab {
justify-content: center;
text-align: center;
white-space: nowrap;
}
/* Karte: Legende auf 2 Zeilen
— #page-map nötig für Spezifität > .map-legend (components.css lädt später) */
#page-map .map-legend {
display: grid;
grid-template-columns: repeat(var(--map-legend-cols, 8), minmax(0, 1fr));
overflow: visible;
gap: var(--space-1);
}
#page-map .map-legend-btn {
justify-content: center;
}
/* Suche + Threads volle Breite */
.forum-main-col {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
}
/* ============================================================
Phosphor Icons — SVG Sprite
============================================================ */
.ph-icon {
width: 20px;
height: 20px;
display: inline-block;
vertical-align: middle;
fill: currentColor;
flex-shrink: 0;
}
.nav-item .ph-icon { width: 24px; height: 24px; }
.nav-item-center .ph-icon { width: 22px; height: 22px; }
.header-menu-btn .ph-icon { width: 22px; height: 22px; }