diff --git a/TESTING.md b/TESTING.md
index dc24df1..2cf6bac 100644
--- a/TESTING.md
+++ b/TESTING.md
@@ -1525,6 +1525,59 @@ bleibt `node` (schnell); wer ein DOM braucht, schreibt
---
+## Platzhalter und Leerzustände (18.09.2026)
+
+Beim Laden stand ein Kreisel in der Mitte, danach klappte das Layout um. Jetzt
+steht dort ein Platzhalter **in der Form des Kommenden** – Kacheln in der
+Kachelansicht, Zeilen in der Liste –, und nichts springt mehr, wenn die Daten
+ankommen. Die Maße sind aus `FileViews.tsx` abgeschrieben, nicht geschätzt;
+weichen sie ab, springt es wieder.
+
+- [ ] Ordner öffnen → Platzhalter in der Form der gewählten Ansicht 🔎
+- [ ] Beim Umschalten zwischen Kachel und Liste wechselt auch der Platzhalter
+- [ ] Datenankunft ohne Sprung: Die Kacheln stehen dort, wo die Platzhalter
+ standen
+- [ ] Papierkorb, „Für mich freigegeben", Etiketten, Kalender: ebenfalls
+ Platzhalter statt „Loading…"
+- [ ] Betriebssystem auf „Bewegung reduzieren" → kein Pulsieren, nur gedämpfte
+ Flächen
+
+Leerzustände sagen jetzt, **warum** nichts da ist, und bieten den Knopf an,
+der an dieser Stelle weiterhilft:
+
+- [ ] Leerer Ordner → „This folder is empty" mit Knopf „Upload files"
+- [ ] Wurzel ohne Inhalt → „Nothing here yet"
+- [ ] Leerer Papierkorb → **kein** Knopf (ein leerer Papierkorb ist ein guter
+ Zustand, kein Mangel)
+- [ ] Nichts freigegeben → „Nothing shared with you yet"
+- [ ] Etikett ohne Dateien → Hinweis auf „Organise…"
+
+### Der Kreisel stand sechsmal im Quelltext
+
+`@keyframes spin` lag als Inline-`
)
}
diff --git a/frontend/src/components/EmptyState.tsx b/frontend/src/components/EmptyState.tsx
new file mode 100644
index 0000000..03587e8
--- /dev/null
+++ b/frontend/src/components/EmptyState.tsx
@@ -0,0 +1,63 @@
+import type { ReactNode } from 'react'
+
+/**
+ * Was dasteht, wenn nichts dasteht.
+ *
+ * Bisher war das je nach Seite ein grauer Halbsatz („Trash is empty") oder gar
+ * nichts. Beides lässt offen, ob die Liste leer ist oder das Laden
+ * fehlgeschlagen – und beides verschenkt die Stelle, an der jemand am
+ * ehesten Hilfe gebrauchen kann: Er steht vor etwas Leerem und weiß nicht,
+ * wie es sich füllt.
+ *
+ * Deshalb drei Teile, und der dritte ist der wichtige:
+ *
+ * 1. ein Zeichen, damit die Fläche nicht kaputt aussieht
+ * 2. ein Satz, der sagt, **warum** hier nichts ist
+ * 3. der Knopf, der genau hier weiterhilft – wo es einen gibt
+ *
+ * Wo es keinen sinnvollen Knopf gibt (ein leerer Papierkorb ist ein guter
+ * Zustand, kein Mangel), bleibt es bei zweien. Ein Knopf, der nur da ist,
+ * damit einer da ist, führt nirgendwohin.
+ */
+export default function EmptyState({ icon, title, hint, action }: {
+ /** Ein Emoji oder ein SVG. Gedämpft – es soll die Fläche tragen, nicht sie beherrschen. */
+ icon: ReactNode
+ title: string
+ hint?: ReactNode
+ action?: { label: string; onClick: () => void }
+}) {
+ return (
+
+
{icon}
+
+
+ {title}
+
+
+ {hint && (
+
+ {hint}
+
+ )}
+
+ {action && (
+
+ )}
+
+ )
+}
diff --git a/frontend/src/components/Skeleton.tsx b/frontend/src/components/Skeleton.tsx
new file mode 100644
index 0000000..78baa5f
--- /dev/null
+++ b/frontend/src/components/Skeleton.tsx
@@ -0,0 +1,84 @@
+/**
+ * Platzhalter, solange geladen wird.
+ *
+ * Vorher stand dort ein Kreisel in der Mitte. Der sagt „warte", aber er sagt
+ * nicht, **worauf** – und wenn die Daten da sind, klappt das Layout um: Der
+ * Kreisel verschwindet, Kacheln erscheinen, die Seite springt. Ein Platzhalter
+ * in der Form des Kommenden tut beides besser. Er zeigt, was gleich da ist,
+ * und er hält den Platz, sodass nichts springt.
+ *
+ * **Die Maße sind abgeschrieben, nicht geschätzt** – aus FileViews.tsx: Kachel
+ * mit 12er Radius und 100 Pixel Deckfläche, Zeile mit 8/12er Innenabstand und
+ * 8er Radius. Weichen sie ab, springt es beim Wechsel wieder, und der ganze
+ * Zweck wäre dahin.
+ */
+
+/** Ein graues Feld, das leicht pulsiert. */
+function Feld({ w, h, r = 6, mb }: { w: number | string; h: number; r?: number; mb?: number }) {
+ return (
+
+ )
+}
+
+/**
+ * Wie viele Platzhalter.
+ *
+ * Genug, um die sichtbare Fläche zu füllen, und nicht mehr: Ein Schirm voll
+ * grauer Kästen, der sich noch scrollen lässt, verspricht mehr Inhalt als da
+ * ist – und enttäuscht zweimal, wenn der Ordner am Ende drei Dateien hat.
+ */
+const kacheln = 8
+const zeilen = 7
+
+export function SkeletonGrid() {
+ return (
+
+ {Array.from({ length: kacheln }, (_, i) => (
+
+
+
+
+
+ {/* Unterschiedlich lang, sonst sieht es aus wie ein Formular und
+ nicht wie eine Liste von Namen. */}
+
+
+
)}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 1671b72..0370dea 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -155,3 +155,35 @@ body {
@media (prefers-reduced-motion: reduce) {
.aether-toast { animation: none; }
}
+
+/* ── Platzhalter beim Laden ──────────────────────────────────────────────────
+ Ein ruhiges Pulsieren, kein Glanzstreifen: Der wandernde Reflex, den man
+ oft sieht, zieht den Blick genau dorthin, wo noch nichts steht. Hier soll
+ die Fläche zeigen, dass gleich etwas kommt – nicht um Aufmerksamkeit
+ bitten.
+
+ 1,4 s ist bewusst langsam. Schneller wirkt es hektisch, und der Platzhalter
+ steht ja meist nur den Bruchteil einer Sekunde. */
+@keyframes aether-puls {
+ 0%, 100% { opacity: 0.55; }
+ 50% { opacity: 0.9; }
+}
+
+.aether-skeleton {
+ background: var(--color-surface-2);
+ animation: aether-puls 1.4s ease-in-out infinite;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .aether-skeleton { animation: none; opacity: 0.6; }
+}
+
+/* ── Kreisel ─────────────────────────────────────────────────────────────────
+ Stand bis zum 18.09.2026 **sechsmal** als `
)
}
diff --git a/frontend/src/pages/SharePage.tsx b/frontend/src/pages/SharePage.tsx
index 5a66ec1..7dacbfc 100644
--- a/frontend/src/pages/SharePage.tsx
+++ b/frontend/src/pages/SharePage.tsx
@@ -100,7 +100,6 @@ export default function SharePage({ token }: Props) {
) : !info ? (
- )
+/** Die drei leeren Fälle dieser Seite sehen gleich aus; nur der Satz wechselt.
+ * Das Zeichen richtet sich danach, worauf man gerade schaut. */
+function Empty({ text, icon = '🏷' }: { text: string; icon?: string }) {
+ return
}
diff --git a/frontend/src/pages/TrashPage.tsx b/frontend/src/pages/TrashPage.tsx
index eb044bd..b3d4d9b 100644
--- a/frontend/src/pages/TrashPage.tsx
+++ b/frontend/src/pages/TrashPage.tsx
@@ -3,6 +3,8 @@ import { trashApi, filesApi, type TrashItem } from '../api/client'
import { formatBytes } from '../format'
import PageHeader from '../components/PageHeader'
import ConfirmDialog from '../components/ConfirmDialog'
+import { SkeletonRows } from '../components/Skeleton'
+import EmptyState from '../components/EmptyState'
interface Props {
/** Wiederherstellen verändert den belegten Speicher – Balken nachziehen. */
@@ -74,18 +76,16 @@ export default function TrashPage({ onUserChanged }: Props) {
- {loading && (
-
-
-
-
- )}
+ {loading && }
+ {/* Kein Knopf: Ein leerer Papierkorb ist ein guter Zustand, kein
+ Mangel, den man beheben müsste. */}
{!loading && items.length === 0 && (
-