/** * Modul: Layout * Zweck: App-Shell-Layout, Navigation (Bottom Mobile / Sidebar Desktop), Responsive Grid * Abhängigkeiten: tokens.css, reset.css * * Navigation: * Mobile (<1024px): Bottom-Nav-Bar, 5 Haupt-Module * Desktop (1024–1279px): Collapsed Sidebar, nur Icons (56px) * Wide Desktop (>1280px): Expanded Sidebar mit Labels (220px) * * Referenz: Apple HIG Sidebar-Pattern */ /* -------------------------------------------------------- * App-Shell * -------------------------------------------------------- */ .app-shell { display: flex; flex-direction: column; /* -webkit-fill-available als iOS-Fallback: In manchen iOS-WebKit-Versionen * ist 100dvh minimal kleiner als die tatsächliche WKWebView-Höhe, was zu einem * sichtbaren Streifen unten führt. -webkit-fill-available füllt zuverlässig. */ height: -webkit-fill-available; height: 100dvh; } /* -------------------------------------------------------- * Loading-Screen * -------------------------------------------------------- */ .app-loading { display: flex; align-items: center; justify-content: center; min-height: 100dvh; background-color: var(--color-bg); } .app-loading__logo { font-size: var(--text-2xl); font-weight: var(--font-weight-bold); color: var(--color-accent); animation: pulse 1.5s ease-in-out infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } /* -------------------------------------------------------- * Seiten-Übergangs-Animation (direktional) * -------------------------------------------------------- */ /* Basis: unsichtbar vor Animation */ .page-transition { opacity: 0; } /* Nach der In-Animation soll es sichtbar bleiben → direkt auf der Klasse */ .page-transition--in-right, .page-transition--in-left { opacity: 1; transform: none; } /* Keyframes OHNE forwards */ @keyframes page-slide-in-right { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: none; } } @keyframes page-slide-in-left { from { opacity: 0; transform: translateX(-20px); } to { opacity: 1; transform: none; } } @keyframes page-out-left { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateX(-20px); } } @keyframes page-out-right { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateX(20px); } } .page-transition--in-right { animation: page-slide-in-right 0.2s var(--ease-out); } .page-transition--in-left { animation: page-slide-in-left 0.2s var(--ease-out); } .page-transition--out-left { animation: page-out-left 0.12s ease forwards; /* ← out braucht forwards, wird ja eh entfernt */ pointer-events: none; } .page-transition--out-right { animation: page-out-right 0.12s ease forwards; pointer-events: none; } @media (prefers-reduced-motion: reduce) { .page-transition--in-right, .page-transition--in-left { animation: none; opacity: 1; } .page-transition--out-left, .page-transition--out-right { animation: none; } } /* -------------------------------------------------------- * Layout: Mobile (Standard, < 1024px) * -------------------------------------------------------- */ .app-content { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; -webkit-overflow-scrolling: touch; } /* Sidebar auf Mobile verstecken */ .nav-sidebar { display: none; } /* -------------------------------------------------------- * Bottom Navigation (Mobil + Tablet) * * Kompakte Bar: 56px Höhe, Icons 22px, Labels 11px. * Leichter Blur-Effekt für Glassmorphismus. * -------------------------------------------------------- */ .nav-bottom { /* Flex-Kind von .app-shell statt position: fixed - zuverlässiger auf iOS PWA. * position: fixed kann durch will-change: transform in iOS-Compositor-Layern * falsch referenziert werden. Als Flex-Kind am Ende der 100dvh-App-Shell ist * die Position garantiert am Viewport-Bottom. */ position: relative; flex-shrink: 0; width: 100%; background-color: color-mix(in srgb, var(--color-surface) 85%, transparent); border-top: 1px solid var(--color-border-subtle); display: flex; flex-direction: column; z-index: var(--z-nav); backdrop-filter: blur(16px) saturate(180%); -webkit-backdrop-filter: blur(16px) saturate(180%); } /* ── Items-Reihe ── */ .nav-bottom__items { display: flex; height: var(--nav-height-mobile); } /* ── More-Button (Button-Basis, ansonsten wie nav-item) ── */ .nav-item--more { background: none; border: none; cursor: pointer; font-family: inherit; } .nav-item--active { color: var(--active-module-accent, var(--color-accent)); } /* ── More-Backdrop ── */ .more-backdrop { display: none; position: fixed; inset: 0; background-color: rgba(0, 0, 0, 0.4); z-index: calc(var(--z-nav) + 1); backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); } .more-backdrop--visible { display: block; animation: fade-in 0.15s ease; } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } /* ── More-Sheet ── */ .more-sheet { position: fixed; bottom: 0; left: 0; right: 0; background-color: var(--color-surface); border-top: 1px solid var(--color-border-subtle); border-radius: var(--radius-xl) var(--radius-xl) 0 0; padding: var(--space-4) var(--space-4) calc(var(--space-4) + var(--safe-area-inset-bottom)); z-index: calc(var(--z-nav) + 2); display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-3); transform: translateY(100%); transition: transform 0.25s var(--ease-out); } .more-sheet[aria-hidden="false"] { transform: translateY(0); } /* ── More-Item ── */ .more-item { display: flex; flex-direction: column; align-items: center; gap: var(--space-1); padding: var(--space-3) var(--space-2); border-radius: var(--radius-lg); background-color: var(--color-surface-elevated); color: var(--color-text-secondary); text-decoration: none; border: none; cursor: pointer; font-family: inherit; transition: background-color var(--transition-fast), color var(--transition-fast); -webkit-tap-highlight-color: transparent; min-height: var(--target-lg); } .more-item:active { background-color: var(--color-surface-hover); transform: scale(0.96); } .more-item[aria-current="page"] { color: var(--active-module-accent, var(--color-accent)); background-color: color-mix(in srgb, var(--active-module-accent, var(--color-accent)) 12%, transparent); } .more-item__icon { width: var(--space-6); height: var(--space-6); flex-shrink: 0; } .more-item__label { font-size: var(--text-xs); font-weight: var(--font-weight-medium); text-align: center; line-height: 1.2; } /* ── Such-Overlay ── */ .search-overlay { position: fixed; inset: 0; background-color: var(--color-surface); z-index: calc(var(--z-nav) + 3); display: flex; flex-direction: column; transform: translateY(100%); transition: transform 0.25s var(--ease-out); } .search-overlay--visible { transform: translateY(0); } .search-overlay__header { display: flex; align-items: center; gap: var(--space-3); padding: calc(var(--space-4) + var(--safe-area-inset-top)) var(--space-4) var(--space-3); border-bottom: 1px solid var(--color-border-subtle); } .search-overlay__input { flex: 1; height: var(--target-lg); padding: 0 var(--space-3); border-radius: var(--radius-lg); border: 1.5px solid var(--color-border); background-color: var(--color-surface-elevated); color: var(--color-text-primary); font-size: var(--text-base); } .search-overlay__input:focus { outline: none; border-color: var(--color-accent); } .search-overlay__close { display: flex; align-items: center; justify-content: center; width: var(--target-base); height: var(--target-base); border-radius: var(--radius-full); border: none; background: var(--color-surface-elevated); color: var(--color-text-secondary); cursor: pointer; flex-shrink: 0; -webkit-tap-highlight-color: transparent; } .search-overlay__close-icon { width: var(--space-5); height: var(--space-5); } .search-overlay__results { flex: 1; overflow-y: auto; padding: var(--space-4); } .search-overlay__empty { text-align: center; color: var(--color-text-tertiary); margin-top: var(--space-8); } .search-section { margin-bottom: var(--space-5); } .search-section__heading { font-size: var(--text-xs); font-weight: var(--font-weight-semibold); color: var(--color-text-tertiary); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: var(--space-2); } .search-result { display: block; width: 100%; text-align: left; padding: var(--space-3); border-radius: var(--radius-md); border: none; background: var(--color-surface-elevated); color: var(--color-text-primary); font-size: var(--text-sm); cursor: pointer; margin-bottom: var(--space-2); transition: background-color var(--transition-fast); -webkit-tap-highlight-color: transparent; } .search-result:active { background-color: var(--color-surface-hover); } /* ── Nav-Item (Bottom-Bar): Basis-State ── */ .nav-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-0h); padding: var(--space-1) var(--space-1); color: var(--color-text-tertiary); transition: color var(--transition-fast), transform 0.12s ease; -webkit-tap-highlight-color: transparent; min-height: var(--target-lg); min-width: var(--target-lg); text-decoration: none; position: relative; } .nav-item__icon-wrap { position: relative; display: inline-flex; flex-shrink: 0; } .nav-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; padding: 0 var(--space-1); border-radius: var(--radius-full); background-color: var(--color-danger); color: var(--color-text-on-accent); font-size: var(--text-xs); font-weight: var(--font-weight-bold); } .nav-item__icon-wrap .nav-badge { position: absolute; top: calc(-1 * var(--space-1)); right: calc(-1 * var(--space-1)); } .nav-item:active { transform: scale(0.92); transition-duration: 0.06s; } /* ── Nav-Item: Aktiv ── */ .nav-item[aria-current="page"] { color: var(--active-module-accent, var(--color-accent)); } /* Pill-Hintergrund hinter dem Icon (nur Bottom-Nav, nicht Sidebar) */ .nav-bottom .nav-item[aria-current="page"] .nav-item__icon-wrap { background-color: color-mix(in srgb, var(--active-module-accent, var(--color-accent)) 14%, transparent); border-radius: var(--radius-lg); padding: var(--space-1) var(--space-4); margin-bottom: 1px; transition: background-color var(--transition-fast); } .nav-item__icon { width: var(--space-5); height: var(--space-5); flex-shrink: 0; } .nav-item__label { font-size: var(--text-xs); font-weight: var(--font-weight-medium); line-height: 1; } /* -------------------------------------------------------- * Page FAB - Schwebender Erstellen-Button (alle Breakpoints) * * Einheitlicher runder Plus-Button auf allen Unterseiten. * Mobile: über der Bottom-Nav. Desktop: unten rechts im Content. * Toolbar-"Neu"-Buttons werden überall versteckt. * -------------------------------------------------------- */ @keyframes fab-in { from { transform: scale(0.6) translateY(8px); opacity: 0; } to { transform: scale(1) translateY(0); opacity: 1; } } .page-fab { position: fixed; bottom: calc(var(--nav-bottom-height) + 24px + var(--safe-area-inset-bottom)); right: var(--space-4); width: 52px; height: 52px; border-radius: var(--radius-full); background-color: var(--module-accent, var(--color-accent)); color: var(--color-text-on-accent); box-shadow: var(--shadow-lg); display: flex; align-items: center; justify-content: center; cursor: pointer; border: none; z-index: calc(var(--z-nav) - 1); transition: transform var(--transition-base), background-color var(--transition-fast); -webkit-tap-highlight-color: transparent; animation: fab-in 0.35s var(--ease-out) backwards; } .page-fab:hover { background-color: color-mix(in srgb, var(--module-accent, var(--color-accent)) 85%, black); } .page-fab:active { transform: scale(0.92); } .page-fab:focus-visible { outline: 2px solid var(--color-bg); outline-offset: 2px; box-shadow: 0 0 0 4px var(--color-accent); } /* Desktop: FAB Position anpassen (keine Bottom-Nav) und etwas kleiner */ @media (min-width: 1024px) { .page-fab { bottom: var(--space-8); right: var(--space-8); width: var(--target-lg); height: var(--target-lg); } } /* Toolbar-"Neu"-Buttons überall verstecken - FAB übernimmt */ #btn-new-task, #notes-add-btn, #contacts-add-btn, #budget-add, #cal-add { display: none !important; } /* ================================================================ * Sidebar Navigation - Desktop (≥ 1024px) * * Design: Flach, kein Neumorphismus. Dezenter Seitenrand. * Aktiver State: Hintergrund-Highlight + Akzentstreifen links. * Hover: Subtile Hintergrundfarbe, kein Shadow. * ================================================================ */ @media (min-width: 1024px) { .app-shell { flex-direction: row; } .app-content { flex: 1; min-height: 0; padding-bottom: 0; margin-left: var(--sidebar-width); transition: margin-left var(--transition-slow); } .nav-bottom { display: none; } /* ── Sidebar anzeigen ── */ .nav-sidebar { display: flex; position: fixed; top: 0; left: 0; bottom: 0; width: var(--sidebar-width); background: var(--sidebar-bg); border-right: 1px solid var(--color-border-subtle); flex-direction: column; z-index: var(--z-nav); padding: var(--space-4) 0 var(--space-4); transition: width var(--transition-slow); overflow: hidden; } /* ── Logo: Icon-only bei collapsed ── */ .nav-sidebar__logo { display: flex; justify-content: center; align-items: center; padding: var(--space-2) 0 var(--space-4); margin-bottom: var(--space-1); flex-shrink: 0; } /* SVG-Logomark (via DOM API in router.js injiziert) */ .nav-sidebar__logomark { width: var(--target-sm); height: var(--target-sm); border-radius: var(--radius-sm); background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-secondary) 100%); color: var(--color-text-on-accent); display: flex; align-items: center; justify-content: center; flex-shrink: 0; } .nav-sidebar__logomark svg { width: 16px; height: 16px; } /* Logo-Text verstecken im collapsed-Modus */ .nav-sidebar__logo > span { display: none; } /* ── Nav-Items Container ── */ .nav-sidebar__items { display: flex; flex-direction: column; gap: var(--space-0h); padding: 0 var(--space-2); flex: 1; overflow-y: auto; scrollbar-width: none; } .nav-sidebar__items::-webkit-scrollbar { display: none; } /* ── Nav-Item: Sidebar-Basis ── */ .nav-sidebar .nav-item { flex-direction: row; justify-content: center; align-items: center; border-radius: var(--radius-sm); padding: var(--space-2) var(--space-2); gap: 0; min-height: var(--target-lg); font-size: var(--text-sm); color: var(--color-text-secondary); background: transparent; transition: color var(--transition-fast), background-color var(--transition-fast); position: relative; } .nav-sidebar .nav-item__icon { width: 20px; height: 20px; flex-shrink: 0; } .nav-sidebar .nav-item__label { display: none; font-size: var(--text-sm); font-weight: var(--font-weight-medium); white-space: nowrap; overflow: hidden; } /* ── Hover: dezente Hintergrundfarbe ── */ .nav-sidebar .nav-item:hover:not([aria-current="page"]) { color: var(--color-text-primary); background-color: var(--color-surface-3); } /* ── Aktiv: Hintergrund-Highlight + Akzent-Indikator ── */ .nav-sidebar .nav-item[aria-current="page"] { color: var(--active-module-accent, var(--color-accent)); font-weight: var(--font-weight-semibold); background-color: color-mix(in srgb, var(--active-module-accent, var(--color-accent)) 12%, transparent); } /* Akzentstreifen links */ .nav-sidebar .nav-item[aria-current="page"]::before { content: ''; position: absolute; left: 0; top: var(--space-2); bottom: var(--space-2); width: 3px; border-radius: 0 var(--radius-full) var(--radius-full) 0; background: var(--active-module-accent, var(--color-accent)); } /* Active-Press auf Desktop */ .nav-sidebar .nav-item:active { transform: scale(0.97); } /* ── Einstellungen ans Ende ── */ .nav-sidebar__items > a:last-child { margin-top: auto; } } /* ================================================================ * Sidebar Expanded (≥ 1280px) - Labels sichtbar * ================================================================ */ @media (min-width: 1440px) { :root { --sidebar-width: var(--sidebar-width-expanded); } /* Logo: Text-Variante */ .nav-sidebar__logo { justify-content: flex-start; padding: var(--space-2) var(--space-5) var(--space-4); gap: var(--space-3); } .nav-sidebar__logomark { width: 28px; height: 28px; } .nav-sidebar__logo > span { display: inline-block; font-size: var(--text-lg); font-weight: var(--font-weight-bold); letter-spacing: -0.3px; color: var(--color-text-primary); } .nav-sidebar__items { padding: 0 var(--space-2); gap: var(--space-0h); } /* Nav-Items: horizontal mit Label */ .nav-sidebar .nav-item { justify-content: flex-start; padding: var(--space-2) var(--space-3); gap: var(--space-3); } .nav-sidebar .nav-item__icon { width: 18px; height: 18px; } .nav-sidebar .nav-item__label { display: block; font-weight: inherit; } } /* -------------------------------------------------------- * Seiten-Container * -------------------------------------------------------- */ .page { padding: var(--space-4); max-width: var(--content-max-width); margin: 0 auto; } .page__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-5); gap: var(--space-4); } .page__title { font-size: var(--text-xl); font-weight: var(--font-weight-bold); letter-spacing: -0.3px; } @media (min-width: 1024px) { .page { padding: var(--space-8) var(--space-8); } .page__title { font-size: var(--text-2xl); letter-spacing: -0.5px; } } @media (min-width: 1440px) { .page { padding: var(--space-8) var(--space-10); } } /* -------------------------------------------------------- * Cards * * Einheitliches Card-Pattern für alle Module: * - Padding: 16px (mobile), 20px (desktop) * - Radius: 12px * - Shadow: subtle (sm) * - Internes Spacing: 8–12px * - Hover auf Desktop: leichter Lift (1px) * * Varianten: * .card - Basis (kein Padding) * .card--padded - Mit Padding * .card--compact - Enges Padding (12px) * .card--flat - Kein Shadow, nur Border * .card--interactive - Hover-Lift + Cursor * -------------------------------------------------------- */ .card { background-color: var(--color-surface); border-radius: var(--radius-md); box-shadow: var(--shadow-sm); overflow: hidden; } .card--padded { padding: var(--space-4); } .card--compact { padding: var(--space-3); } .card--flat { box-shadow: none; border: 1px solid var(--color-border-subtle); } .card--interactive { cursor: pointer; transition: transform var(--transition-fast), box-shadow var(--transition-fast), background-color var(--transition-fast); } .card--interactive:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); } .card--interactive:active { transform: scale(0.98); background-color: var(--color-surface-3); box-shadow: var(--shadow-sm); } .card--interactive:focus-visible { outline: 2px solid var(--active-module-accent, var(--color-accent)); outline-offset: 2px; } @media (min-width: 1024px) { .card--padded { padding: var(--space-5); } } /* ── Card-Inhalts-Utilities ── */ .card__header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding-bottom: var(--space-3); margin-bottom: var(--space-3); border-bottom: 1px solid var(--color-border-subtle); } .card__title { font-size: var(--text-sm); font-weight: var(--font-weight-semibold); color: var(--color-text-primary); } .card__body { display: flex; flex-direction: column; gap: var(--space-2); } /* -------------------------------------------------------- * Modal-Basis * * Einheitliches Modal-Pattern für alle Module: * - Mobile: Bottom-Sheet (gerundet oben) * - Desktop: Zentriert (max-width variabel) * - Overlay: var(--color-overlay) * - Padding: 16px * - Radius: 16px * -------------------------------------------------------- */ .modal-overlay { position: fixed; inset: 0; background-color: var(--color-overlay); z-index: var(--z-modal); display: flex; align-items: flex-end; justify-content: center; overflow: hidden; animation: modal-overlay-in 0.2s ease forwards; /* iOS Safari: click-Events auf non-interactive divs erfordern cursor:pointer */ cursor: pointer; } @media (min-width: 768px) { .modal-overlay { align-items: center; padding: var(--space-6); } } .modal-panel { background-color: var(--color-surface); width: 100%; max-height: 90dvh; overflow: hidden; display: flex; flex-direction: column; border-radius: var(--radius-lg) var(--radius-lg) 0 0; border: 1px solid var(--color-border-subtle); box-shadow: var(--shadow-lg); animation: modal-slide-up 0.25s var(--ease-out) forwards; /* Cursor vom Overlay zurücksetzen */ cursor: default; } @media (min-width: 768px) { .modal-panel { max-width: 520px; border-radius: var(--radius-lg); animation: modal-scale-in 0.2s var(--ease-out) forwards; } .modal-panel--sm { max-width: 400px; } .modal-panel--lg { max-width: 680px; } } .modal-panel__header { display: flex; align-items: center; justify-content: space-between; padding: var(--space-4); border-bottom: 1px solid var(--color-border-subtle); background-color: var(--color-surface); flex-shrink: 0; } .modal-panel__title { font-size: var(--text-md); font-weight: var(--font-weight-semibold); } .modal-panel__close { width: var(--target-md); height: var(--target-md); display: flex; align-items: center; justify-content: center; border-radius: var(--radius-full); color: var(--color-text-secondary); transition: background-color var(--transition-fast); position: relative; flex-shrink: 0; } .modal-panel__close:hover { background-color: var(--color-surface-3); } .modal-panel__body { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-4); flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; } /* Modal-Content-Grids (z. B. 2 Spalten in Formularen) */ .modal-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-3); align-items: stretch; } .modal-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } .modal-grid > * { min-width: 0; } .modal-grid > .form-group { margin-bottom: 0; } /* Gleich hohe Karten/Boxen in Modal-Grids */ .modal-panel__body .grid { align-items: stretch; } .modal-panel__body .grid > .card { height: 100%; display: flex; flex-direction: column; } @media (max-width: 639px) { .modal-grid--2 { grid-template-columns: 1fr; } } .form-stack { display: flex; flex-direction: column; gap: var(--space-4); } .modal-actions { display: flex; justify-content: flex-end; gap: var(--space-2); } .modal-panel__footer { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-top: 1px solid var(--color-border-subtle); } @keyframes modal-overlay-in { from { opacity: 0; } to { opacity: 1; } } @keyframes modal-slide-up { from { transform: translateY(100%); } to { transform: translateY(0); } } @keyframes modal-scale-in { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } /* ── Bottom Sheet: Handle + Closing Animation (Mobile < 768px) ── */ @media (max-width: 767px) { .modal-panel { /* Extra top padding for the drag handle */ padding-top: calc(var(--space-4) + 20px); position: relative; } .modal-panel::before { content: ''; position: absolute; top: var(--space-3); left: 50%; transform: translateX(-50%); width: 36px; height: var(--space-1); background: var(--color-border); border-radius: var(--radius-full); } .modal-panel--closing { animation: sheet-out 0.2s ease forwards; } } @keyframes sheet-out { from { transform: translateY(0); } to { transform: translateY(100%); } } @media (prefers-reduced-motion: reduce) { .modal-panel { animation: none; } .modal-panel--closing { animation: none; } } /* -------------------------------------------------------- * Buttons * -------------------------------------------------------- */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); padding: var(--space-2) var(--space-4); border-radius: var(--radius-sm); font-size: var(--text-sm); font-weight: var(--font-weight-medium); min-height: var(--target-lg); transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast); cursor: pointer; white-space: nowrap; } @media (min-width: 1024px) { .btn { min-height: var(--target-md); } } .btn:active { transform: scale(0.98); } .btn:focus-visible { outline: 2px solid var(--active-module-accent, var(--color-accent)); outline-offset: 2px; } .btn--primary { background-color: var(--color-btn-primary); color: var(--color-text-on-accent); box-shadow: var(--shadow-sm); } .btn--primary:hover { background-color: var(--color-btn-primary-hover); box-shadow: var(--shadow-md); } .btn--secondary { background-color: transparent; color: var(--color-accent); border: 1.5px solid var(--color-border); } .btn--secondary:hover { background-color: var(--color-accent-light); border-color: var(--color-accent); } .btn--danger { background-color: var(--color-danger); color: var(--color-text-on-accent); } .btn--danger:hover { background-color: var(--color-danger-hover); } .btn--ghost { background-color: transparent; color: var(--color-text-secondary); } .btn--ghost:hover { background-color: var(--color-surface-2); color: var(--color-text-primary); } .btn:disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; } .btn--icon { padding: var(--space-2); min-height: var(--target-lg); min-width: var(--target-lg); border-radius: var(--radius-sm); } @media (min-width: 1024px) { .btn--icon { min-height: var(--target-md); min-width: var(--target-md); } } /* FAB (Floating Action Button) */ .fab { position: fixed; bottom: calc(var(--nav-bottom-height) + var(--safe-area-inset-bottom) + var(--space-4)); right: var(--space-4); width: 52px; height: 52px; border-radius: var(--radius-full); background-color: var(--module-accent, var(--color-btn-primary)); color: var(--color-text-on-accent); box-shadow: var(--shadow-lg); display: flex; align-items: center; justify-content: center; z-index: calc(var(--z-nav) - 1); transition: transform var(--transition-fast), background-color var(--transition-fast), box-shadow var(--transition-fast); animation: fab-in 0.35s var(--ease-out) backwards; } .fab:hover { background-color: color-mix(in srgb, var(--module-accent, var(--color-btn-primary)) 85%, black); transform: scale(1.05); } .fab:active { transform: scale(0.95); } .fab:focus-visible { outline: 2px solid var(--color-bg); outline-offset: 2px; box-shadow: 0 0 0 4px var(--color-accent); } @media (min-width: 1024px) { .fab { bottom: var(--space-8); width: var(--target-lg); height: var(--target-lg); } } /* FAB und page-fab ausblenden wenn virtuelle Tastatur offen (nur Mobile) */ @media (max-width: 1023px) { .keyboard-visible .fab, .keyboard-visible .page-fab { visibility: hidden; pointer-events: none; } } /* -------------------------------------------------------- * Form-Elemente * -------------------------------------------------------- */ .input, .form-input { width: 100%; min-width: 0; box-sizing: border-box; padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); border: 1.5px solid var(--color-border); background-color: var(--color-surface); color: var(--color-text-primary); font-size: var(--text-md); transition: border-color var(--transition-fast), box-shadow var(--transition-fast); min-height: var(--target-lg); } @media (min-width: 1024px) { .input, .form-input { min-height: var(--target-md); font-size: var(--text-base); } } .input:focus, .form-input:focus { outline: none; border-color: var(--color-accent); box-shadow: 0 0 0 3px var(--color-accent-light); } .input::placeholder, .form-input::placeholder { color: var(--color-text-disabled); } .label, .form-label { display: block; font-size: var(--text-sm); font-weight: var(--font-weight-medium); color: var(--color-text-secondary); margin-bottom: var(--space-1); } .form-group { display: flex; flex-direction: column; gap: var(--space-1); margin-bottom: var(--space-4); } /* ── Inline-Validierung ── */ .form-field { display: flex; flex-direction: column; gap: var(--space-1); } .form-field--error .input, .form-field--error .form-input { border-color: var(--color-danger); } .form-field--valid .input, .form-field--valid .form-input { border-color: var(--color-success); } .form-field__error { display: none; font-size: var(--text-sm); color: var(--color-danger); gap: var(--space-1); align-items: center; } .form-field--error .form-field__error { display: flex; } /* -------------------------------------------------------- * Toggle-Switch * Custom iOS-style toggle, ersetzt native Checkboxen. * Usage: * -------------------------------------------------------- */ .toggle { display: inline-flex; align-items: center; cursor: pointer; gap: var(--space-3); } .toggle input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); } .toggle__track { position: relative; width: 44px; height: 26px; background-color: var(--neutral-300); border-radius: var(--radius-full); transition: background-color var(--transition-fast); flex-shrink: 0; } .toggle__track::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; background: var(--color-surface); border-radius: var(--radius-full); box-shadow: var(--shadow-sm); transition: transform var(--transition-fast) var(--ease-out); } .toggle input:checked + .toggle__track { background-color: var(--active-module-accent, var(--color-accent)); } .toggle input:checked + .toggle__track::after { transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); } .toggle input:checked + .toggle__track::after { transform: translateX(18px); } .toggle input:focus-visible + .toggle__track { outline: 2px solid var(--active-module-accent, var(--color-accent)); outline-offset: 2px; } .toggle input:disabled + .toggle__track { opacity: 0.4; cursor: not-allowed; } @media (prefers-reduced-motion: reduce) { .toggle__track, .toggle__track::after { transition: none; } .fab, .page-fab { animation: none; } .list-stagger > * { animation: none; } .swipe-row--hint > :first-child { animation: none; } .btn--loading::after { animation: none; } } /* -------------------------------------------------------- * Skeleton-Loading * -------------------------------------------------------- */ .skeleton { background: linear-gradient( 90deg, var(--color-border-subtle) 25%, var(--color-surface-2) 50%, var(--color-border-subtle) 75% ); background-size: 200% 100%; animation: skeleton-shimmer 1.5s infinite; border-radius: var(--radius-sm); } @keyframes skeleton-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } /* -------------------------------------------------------- * Leer-Zustände (Empty States) * -------------------------------------------------------- */ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-3); padding: var(--space-12) var(--space-6); text-align: center; animation: empty-state-in 0.4s var(--ease-out) both; } @keyframes empty-state-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } } @media (prefers-reduced-motion: reduce) { .empty-state { animation: none; } } .empty-state__icon { width: 56px; height: 56px; color: var(--color-text-disabled); } .empty-state__title { font-size: var(--text-md); font-weight: var(--font-weight-semibold); color: var(--color-text-primary); } .empty-state__description { font-size: var(--text-sm); color: var(--color-text-secondary); max-width: 280px; line-height: var(--line-height-base); } .empty-state--compact { padding: var(--space-4) var(--space-3); gap: var(--space-2); } .empty-state--compact .empty-state__description { font-size: var(--text-sm); } /* -------------------------------------------------------- * Responsive Grid (Utility) * -------------------------------------------------------- */ .grid { display: grid; gap: var(--space-4); grid-template-columns: 1fr; } @media (min-width: 768px) { .grid--2 { grid-template-columns: repeat(2, 1fr); } } @media (min-width: 1024px) { .grid--3 { grid-template-columns: repeat(3, 1fr); } } @media (min-width: 1440px) { .grid--4 { grid-template-columns: repeat(4, 1fr); } } /* -------------------------------------------------------- * Layout-Primitives * * Wiederverwendbare Content-Area-Patterns für Desktop. * Mobile: immer single-column (Stacking). * * .layout-master-detail - Liste links, Detail rechts (Aufgaben, Einkauf) * .layout-content-aside - Hauptinhalt + schmale Seitenleiste (Kalender) * .layout-center - Zentrierter schmaler Content (Settings, Login) * .layout-wide - Volle Breite mit max-width (Dashboard) * -------------------------------------------------------- */ /* ── Master-Detail ── * Mobile: gestapelt (Detail wird programmatisch ein-/ausgeblendet). * Desktop: 2 Spalten - Liste ~40%, Detail ~60%. */ .layout-master-detail { display: flex; flex-direction: column; gap: var(--space-4); max-width: var(--content-max-width); margin: 0 auto; } .layout-master-detail__master { flex: 1; min-width: 0; } .layout-master-detail__detail { flex: 1; min-width: 0; } @media (min-width: 1024px) { .layout-master-detail { flex-direction: row; gap: var(--space-6); } .layout-master-detail__master { flex: 0 0 380px; max-width: 420px; overflow-y: auto; max-height: calc(100dvh - var(--space-16)); position: sticky; top: var(--space-8); } .layout-master-detail__detail { flex: 1; } } @media (min-width: 1440px) { .layout-master-detail__master { flex: 0 0 420px; max-width: 460px; } } /* ── Content + Aside ── * Mobile: gestapelt. * Desktop: Hauptinhalt nimmt Platz ein, Aside ist 280–320px. */ .layout-content-aside { display: flex; flex-direction: column; gap: var(--space-4); max-width: var(--content-max-width); margin: 0 auto; } .layout-content-aside__main { flex: 1; min-width: 0; } .layout-content-aside__aside { min-width: 0; } @media (min-width: 1024px) { .layout-content-aside { flex-direction: row; gap: var(--space-6); } .layout-content-aside__main { flex: 1; } .layout-content-aside__aside { flex: 0 0 280px; overflow-y: auto; max-height: calc(100dvh - var(--space-16)); position: sticky; top: var(--space-8); } } @media (min-width: 1440px) { .layout-content-aside__aside { flex: 0 0 320px; } } /* ── Center ── * Schmaler, zentrierter Content-Bereich (max. 720px). * Ideal für Settings, Formulare, Einzelansichten. */ .layout-center { max-width: 720px; margin: 0 auto; } /* ── Wide ── * Volle verfügbare Breite mit max-width. * Für Dashboard und andere Multi-Column-Ansichten. */ .layout-wide { max-width: var(--content-max-width); margin: 0 auto; } /* ── Prose ── * Text-Content mit optimaler Lesebreite. */ .prose { max-width: 720px; } /* ── Sticky-Header (Modul-Toolbars) ── * Klebt am oberen Rand beim Scrollen. */ .sticky-header { position: sticky; top: 0; z-index: var(--z-sticky); background-color: color-mix(in srgb, var(--color-bg) 90%, transparent); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); padding-bottom: var(--space-3); margin-bottom: var(--space-4); } /* -------------------------------------------------------- * Toast-Benachrichtigungen * -------------------------------------------------------- */ .toast-container { position: fixed; bottom: calc(var(--nav-bottom-height) + var(--safe-area-inset-bottom) + var(--space-4)); left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; gap: var(--space-2); z-index: var(--z-toast); pointer-events: none; width: min(calc(100% - var(--space-8)), 380px); } @media (min-width: 1024px) { .toast-container { bottom: var(--space-6); left: calc(var(--sidebar-width) + (100% - var(--sidebar-width)) / 2); } } .toast { display: flex; align-items: center; gap: var(--space-2); background-color: var(--neutral-800); color: var(--neutral-50); padding: var(--space-3) var(--space-4); border-radius: var(--radius-sm); font-size: var(--text-sm); box-shadow: var(--shadow-lg); pointer-events: auto; animation: toast-in 0.25s var(--ease-out) forwards; } .toast__icon { flex-shrink: 0; width: 16px; height: 16px; } .toast__undo { margin-left: auto; flex-shrink: 0; padding: var(--space-1) var(--space-2); background: transparent; border: 1px solid currentColor; border-radius: var(--radius-sm); color: inherit; font-size: var(--text-xs); font-weight: var(--font-weight-semibold); cursor: pointer; white-space: nowrap; opacity: 0.85; } .toast__undo:hover { opacity: 1; } .toast--success { background-color: var(--color-success); color: var(--toast-success-text); } .toast--danger { background-color: var(--color-danger); color: var(--toast-danger-text); } .toast--warning { background-color: var(--color-warning); color: var(--toast-warning-text); } @keyframes toast-in { from { opacity: 0; transform: translateY(6px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } } .toast--out { animation: toast-out 0.2s ease forwards; } @keyframes toast-out { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.95) translateY(4px); } } /* -------------------------------------------------------- * RRULE-Felder (Wiederholungs-Formular, shared Tasks + Kalender) * -------------------------------------------------------- */ .rrule-fields { margin-top: var(--space-4); border-top: 1px solid var(--color-border-subtle); padding-top: var(--space-4); } .rrule-details { display: flex; flex-direction: column; gap: var(--space-3); margin-top: var(--space-3); padding: var(--space-3); background: var(--color-surface-2); border-radius: var(--radius-sm); } .rrule-row { display: flex; align-items: flex-end; gap: var(--space-3); } .rrule-interval-wrap { display: flex; align-items: center; gap: var(--space-2); } .rrule-interval-unit { font-size: var(--text-sm); color: var(--color-text-secondary); white-space: nowrap; } .rrule-day-grid { display: flex; gap: var(--space-1); flex-wrap: wrap; margin-top: var(--space-1); } .rrule-day { width: var(--target-md); height: var(--target-md); border-radius: var(--radius-sm); border: 1.5px solid var(--color-border); background: transparent; color: var(--color-text-secondary); font-size: var(--text-sm); font-weight: var(--font-weight-medium); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all var(--transition-fast); position: relative; } .rrule-day::before { content: ''; position: absolute; inset: -4px; } .rrule-day:hover { border-color: var(--color-accent); color: var(--color-accent); } .rrule-day--active { background: var(--color-accent); border-color: var(--color-accent); color: var(--color-text-on-accent); } .rrule-day--active:hover { background: var(--color-accent-hover); border-color: var(--color-accent-hover); } /* ── Submit-Feedback Animationen ── */ /* Checkbox-Pop: kanonische Definition - shopping.css + tasks.css verwenden diese */ @keyframes check-pop { 0% { transform: scale(1); } 30% { transform: scale(0.8); } 60% { transform: scale(1.3); } 80% { transform: scale(0.95); } 100% { transform: scale(1); } } @keyframes btn-shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-4px); } 40% { transform: translateX(4px); } 60% { transform: translateX(-4px); } 80% { transform: translateX(4px); } } .btn--shaking { animation: btn-shake 0.3s ease; } /* prefers-reduced-motion: kein Schütteln, nur Farb-Feedback */ .btn--error-static { background-color: var(--color-danger) !important; color: var(--color-text-on-accent) !important; pointer-events: none; } .btn--success { background-color: var(--color-success) !important; color: var(--color-text-on-accent) !important; pointer-events: none; } /* Button loading state: hides text, shows spinner */ @keyframes btn-spin { to { transform: rotate(360deg); } } .btn--loading { position: relative; color: transparent !important; pointer-events: none; } .btn--loading::after { content: ''; position: absolute; top: 50%; left: 50%; width: 14px; height: 14px; margin-top: -7px; margin-left: -7px; border: 2px solid var(--color-glass-hover); border-top-color: var(--color-text-on-accent); border-radius: var(--radius-full); animation: btn-spin 0.55s linear infinite; } /* For non-primary buttons (secondary, ghost) */ .btn--secondary.btn--loading::after, .btn--ghost.btn--loading::after { border-color: var(--color-border); border-top-color: var(--color-accent); } /* -------------------------------------------------------- * Swipe-Wrapper - Gemeinsame Basis (Tasks + Shopping) * Modul-spezifische Styles (.swipe-reveal--edit, .swipe-reveal--delete, * .swipe-row .task-card, .swipe-row .shopping-item) liegen in den Modul-CSS. * -------------------------------------------------------- */ .swipe-row { position: relative; overflow: hidden; border-radius: var(--radius-md); margin-bottom: var(--space-2); /* Verhindert ungewolltes Flackern auf iOS */ -webkit-backface-visibility: hidden; } /* Reveal-Panels hinter der Karte */ .swipe-reveal { position: absolute; top: 0; bottom: 0; width: 50%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-1); font-size: var(--text-xs); font-weight: var(--font-weight-semibold); opacity: 0; pointer-events: none; z-index: 0; transition: opacity 0.05s linear; } /* -------------------------------------------------------- * List Stagger * Füge .list-stagger zum Container hinzu. * Items erscheinen sequenziell beim ersten Rendern. * -------------------------------------------------------- */ @keyframes list-item-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } } .list-stagger > * { animation: list-item-in 0.2s var(--ease-out) both; } .list-stagger > *:nth-child(1) { animation-delay: 0ms; } .list-stagger > *:nth-child(2) { animation-delay: 35ms; } .list-stagger > *:nth-child(3) { animation-delay: 65ms; } .list-stagger > *:nth-child(4) { animation-delay: 90ms; } .list-stagger > *:nth-child(5) { animation-delay: 112ms; } .list-stagger > *:nth-child(6) { animation-delay: 130ms; } .list-stagger > *:nth-child(7) { animation-delay: 145ms; } .list-stagger > *:nth-child(8) { animation-delay: 157ms; } .list-stagger > *:nth-child(9) { animation-delay: 166ms; } .list-stagger > *:nth-child(n+10) { animation-delay: 173ms; } /* -------------------------------------------------------- * Swipe Affordance Hint * Einmalig per JS: .swipe-row--hint auf das erste Element. * Gibt nach Seitenladezeit einen kurzen Nudge-Hinweis. * -------------------------------------------------------- */ @keyframes swipe-nudge { 0% { transform: translateX(0); } 20% { transform: translateX(-18px); } 45% { transform: translateX(0); } 60% { transform: translateX(-9px); } 80% { transform: translateX(0); } 100% { transform: translateX(0); } } .swipe-row--hint > :first-child { animation: swipe-nudge 0.8s var(--ease-out) 1.2s both; } /* Gemeinsam: Erledigt / Abhaken (Swipe nach links) */ .swipe-reveal--done { right: 0; background-color: var(--color-success); color: var(--color-text-on-accent); border-radius: 0 var(--radius-md) var(--radius-md) 0; } /* -------------------------------------------------------- * Print-Styles * -------------------------------------------------------- */ @media print { .nav-sidebar, .nav-bottom, .fab, .toast-container, .modal-overlay, .modal-backdrop { display: none !important; } .app-content { margin-left: 0 !important; padding-bottom: 0 !important; } .app-shell { display: block; } body { background: #ffffff; color: #000000; } .card { box-shadow: none; border: 1px solid #cccccc; break-inside: avoid; } a[href]::after { content: " (" attr(href) ")"; font-size: var(--text-sm); color: var(--color-text-secondary); } .nav-item, a[data-route]::after { content: none; } } /* -------------------------------------------------------- * Skip-Link (sichtbar bei Keyboard-Focus) * -------------------------------------------------------- */ .sr-only:focus-visible { position: fixed; top: 0; left: 0; z-index: 9999; width: auto; height: auto; padding: var(--space-2) var(--space-4); margin: 0; clip: auto; white-space: normal; overflow: visible; background: var(--color-accent); color: var(--color-text-on-accent); font-size: var(--text-sm); border-radius: 0 0 var(--radius-sm) 0; } /* -------------------------------------------------------- * Icon-Größen-Utilities * Größen für Lucide-Icons (). Klassen werden * vom Lucide-Renderer auf das erzeugte übertragen. * -------------------------------------------------------- */ .icon-xs { width: 10px; height: 10px; flex-shrink: 0; } .icon-11 { width: 11px; height: 11px; flex-shrink: 0; } .icon-sm { width: 12px; height: 12px; flex-shrink: 0; } .icon-md { width: 14px; height: 14px; flex-shrink: 0; } .icon-base { width: 16px; height: 16px; flex-shrink: 0; } .icon-lg { width: 18px; height: 18px; flex-shrink: 0; } .icon-xl { width: 22px; height: 22px; flex-shrink: 0; } .icon-2xl { width: 24px; height: 24px; flex-shrink: 0; } /* Icons innerhalb von Buttons sollen keine Pointer-Events fangen */ button i[data-lucide], button svg { pointer-events: none; } /* Kompakter Icon-Button (36×36) für Icons in engen Listenkontexten */ .btn--icon-sm { padding: var(--space-1); min-height: unset; min-width: unset; width: 36px; height: 36px; } /* Textarea: vertikale Größenänderung ist nutzbar */ textarea.input { resize: vertical; } /* -------------------------------------------------------- * Windows High Contrast / Forced Colors * -------------------------------------------------------- */ @media (forced-colors: active) { .btn, .page-fab, .fab { border: 1px solid ButtonText; } .card { border: 1px solid CanvasText; } .modal-panel { border: 2px solid CanvasText; } .nav-item[aria-current="page"] { border-bottom: 2px solid Highlight; } }