Files
oikos/public/styles/layout.css
T
Ulas 7eb06ed905 fix(modal): replace native prompt() with custom modal dialogs
Native browser prompt() is unreliable on mobile browsers and PWAs,
often requiring multiple clicks to close. Replace all prompt() calls
with custom promptModal() and selectModal() functions that use the
existing modal system with proper focus management and animations.

Affected pages: shopping (create/rename list), tasks (add subtask),
meals (choose shopping list).

Fixes #12
2026-04-04 21:31:50 +02:00

1572 lines
36 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.
/**
* 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 (10241279px): 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;
min-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)
* -------------------------------------------------------- */
@keyframes page-slide-in-right {
from { opacity: 0; transform: translateX(20px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes page-slide-in-left {
from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes page-out-left {
from { opacity: 1; transform: translateX(0); }
to { opacity: 0; transform: translateX(-20px); }
}
@keyframes page-out-right {
from { opacity: 1; transform: translateX(0); }
to { opacity: 0; transform: translateX(20px); }
}
.page-transition--in-right {
animation: page-slide-in-right 0.2s var(--ease-out) forwards;
}
.page-transition--in-left {
animation: page-slide-in-left 0.2s var(--ease-out) forwards;
}
.page-transition--out-left {
animation: page-out-left 0.12s ease forwards;
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;
padding-bottom: calc(var(--nav-bottom-height) + var(--safe-area-inset-bottom));
overflow-y: auto;
}
/* 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 {
position: fixed;
bottom: 0;
left: 0;
right: 0;
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%);
padding-bottom: var(--safe-area-inset-bottom);
}
/* ── Dot-Indikator ── */
.nav-bottom__dots {
display: flex;
justify-content: center;
gap: var(--space-2);
padding: var(--space-1) 0 var(--space-0h);
}
.nav-bottom__dot {
width: var(--space-1);
height: var(--space-1);
border-radius: var(--radius-full);
background-color: var(--color-text-tertiary);
opacity: 0.25;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.nav-bottom__dot--active {
opacity: 0.7;
transform: scale(1.2);
}
/* ── Scroll-Container ── */
.nav-bottom__scroll {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
scrollbar-width: none; /* Firefox */
height: var(--nav-height-mobile);
}
.nav-bottom__scroll::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
/* ── Einzelne Seiten ── */
.nav-bottom__page {
display: flex;
min-width: 100%;
flex-shrink: 0;
scroll-snap-align: start;
}
/* ── Nav-Item (Bottom-Bar): Basis-State ── */
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
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;
}
.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));
}
.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.
* -------------------------------------------------------- */
.page-fab {
position: fixed;
bottom: calc(var(--nav-height-mobile) + 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: #ffffff;
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;
}
.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 #fff;
outline-offset: 2px;
}
/* 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: 48px;
height: 48px;
}
}
/* 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;
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;
}
/* App-Icon als kompaktes Logo */
.nav-sidebar__logo::before {
content: 'O';
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
background: linear-gradient(135deg, var(--color-accent) 0%, #7C5CFC 100%);
color: #ffffff;
font-size: var(--text-sm);
font-weight: var(--font-weight-bold);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* 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: 8px 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: 1280px) {
: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__logo::before {
width: 28px;
height: 28px;
font-size: var(--text-xs);
}
.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: 8px 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: 812px
* - 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);
}
.card--interactive:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.card--interactive:active {
transform: scale(0.99);
}
.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;
animation: modal-overlay-in 0.2s ease forwards;
}
@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-y: auto;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
animation: modal-slide-up 0.25s var(--ease-out) forwards;
}
@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);
position: sticky;
top: 0;
background-color: var(--color-surface);
z-index: 1;
}
.modal-panel__title {
font-size: var(--text-md);
font-weight: var(--font-weight-semibold);
}
.modal-panel__close {
width: 32px;
height: 32px;
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;
}
.modal-panel__close::before {
content: '';
position: absolute;
inset: -8px;
}
.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);
}
.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: 4px;
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: #ffffff;
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: #ffffff;
}
.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-height-mobile) + 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: #ffffff;
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);
}
.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 #fff;
outline-offset: 2px;
}
@media (min-width: 1024px) {
.fab {
bottom: var(--space-8);
width: 48px;
height: 48px;
}
}
/* 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%;
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: <label class="toggle"><input type="checkbox"><span class="toggle__track"></span></label>
* -------------------------------------------------------- */
.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 0.2s ease;
flex-shrink: 0;
}
.toggle__track::after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
background: #fff;
border-radius: var(--radius-full);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
transition: transform 0.2s var(--ease-out);
}
.toggle input:checked + .toggle__track {
background-color: var(--active-module-accent, var(--color-accent));
}
.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;
}
}
/* --------------------------------------------------------
* 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 280320px.
*/
.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--success { background-color: var(--color-success); color: #fff; }
.toast--danger { background-color: var(--color-danger); color: #fff; }
.toast--warning { background-color: var(--color-warning); color: #fff; }
@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: 40px;
height: 40px;
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: #fff;
}
.rrule-day--active:hover {
background: var(--color-accent-hover);
border-color: var(--color-accent-hover);
}
/* ── Submit-Feedback Animationen ── */
@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;
}
.btn--success {
background-color: var(--color-success) !important;
color: #fff !important;
pointer-events: none;
}
/* --------------------------------------------------------
* 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;
}
/* Gemeinsam: Erledigt / Abhaken (Swipe nach links) */
.swipe-reveal--done {
right: 0;
background-color: var(--color-success);
color: #fff;
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: #fff;
color: #000;
}
.card {
box-shadow: none;
border: 1px solid #ddd;
break-inside: avoid;
}
a[href]::after {
content: " (" attr(href) ")";
font-size: 0.85em;
color: #666;
}
.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: #fff;
font-size: var(--text-sm);
border-radius: 0 0 var(--radius-sm) 0;
}