diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt
index 1a581ffa3c82ca72ff4b4cc86c7c80436fefbc9e..a4ea4730a65a08ac83da746a4f121628e8bf3d9b 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt
@@ -46,6 +46,7 @@ import de.chaosdorf.meteroid.theme.MeteroidTheme
 import de.chaosdorf.meteroid.ui.*
 import de.chaosdorf.meteroid.ui.common.BottomBar
 import de.chaosdorf.meteroid.ui.common.TopBar
+import de.chaosdorf.meteroid.ui.purchase.PurchaseRoute
 import de.chaosdorf.meteroid.viewmodel.NavigationViewModel
 import de.chaosdorf.meteroid.viewmodel.*
 import kotlinx.coroutines.flow.update
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Color.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Color.kt
index 6fdf1e23dfcd9f938ad701cb16cf20984965d3b9..d9ca40027337085c5c775ee77c8a08e9dae75073 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Color.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Color.kt
@@ -26,6 +26,7 @@ package de.chaosdorf.meteroid.theme
 
 import androidx.compose.material3.ColorScheme
 import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Stable
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.graphics.luminance
@@ -92,7 +93,7 @@ val md_theme_dark_surfaceTint = Color(0xFFAFC6FF)
 val md_theme_dark_outlineVariant = Color(0xFF44474F)
 val md_theme_dark_scrim = Color(0xFF000000)
 
-
+@Stable
 val ColorScheme.secondaryGradient
   get() = ThemeGradient(
     listOf(
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/theme/ThemeGradient.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/ThemeGradient.kt
index 9e61bf409df12428d22a235fa88271659a137918..234f8ba60f4fe334201e0afd28248f87bb597a20 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/theme/ThemeGradient.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/ThemeGradient.kt
@@ -24,12 +24,14 @@
 
 package de.chaosdorf.meteroid.theme
 
+import androidx.compose.runtime.Immutable
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.TileMode
 
-class ThemeGradient(val colors: List<Color>) {
+@Immutable
+data class ThemeGradient(val colors: List<Color>) {
   fun linearGradient(
     start: Offset = Offset.Zero,
     end: Offset = Offset.Infinite,
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PurchaseRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PurchaseRoute.kt
deleted file mode 100644
index b42942f30611b0dd626925b86f9ec9f4c26bd423..0000000000000000000000000000000000000000
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PurchaseRoute.kt
+++ /dev/null
@@ -1,261 +0,0 @@
-/*
- * The MIT License (MIT)
- *
- * Copyright (c) 2013-2025 Chaosdorf e.V.
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-package de.chaosdorf.meteroid.ui
-
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.GridItemSpan
-import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.items
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import coil3.compose.rememberAsyncImagePainter
-import de.chaosdorf.meteroid.model.Drink
-import de.chaosdorf.meteroid.theme.onPrimaryContainerTinted
-import de.chaosdorf.meteroid.theme.secondaryGradient
-import de.chaosdorf.meteroid.ui.common.PriceBadge
-import de.chaosdorf.meteroid.viewmodel.Navigator
-import de.chaosdorf.meteroid.viewmodel.PurchaseViewModel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.update
-import kotlinx.datetime.Clock
-import kotlinx.datetime.TimeZone
-import kotlinx.datetime.toLocalDateTime
-import java.math.BigDecimal
-import java.util.*
-
-@Composable
-fun PurchaseRoute(
-  viewModel: PurchaseViewModel,
-  navigator: Navigator,
-  contentPadding: PaddingValues,
-) {
-  val drinks by viewModel.drinks.collectAsState()
-  val filters by viewModel.filters.collectAsState()
-
-  LazyVerticalGrid(
-    GridCells.Adaptive(104.dp),
-    contentPadding = contentPadding,
-    modifier = Modifier.padding(horizontal = 8.dp),
-  ) {
-    item("filter", span = { GridItemSpan(maxLineSpan) }) {
-      FlowRow(
-        modifier = Modifier.padding(horizontal = 12.dp),
-        horizontalArrangement = Arrangement.spacedBy(8.dp)
-      ) {
-        PurchaseFilterChip(
-          label = "Active",
-          selected = filters.contains(PurchaseViewModel.Filter.Active),
-          onClick = { viewModel.toggleFilter(PurchaseViewModel.Filter.Active) }
-        )
-        PurchaseFilterChip(
-          label = "Coffeine Free",
-          selected = filters.contains(PurchaseViewModel.Filter.CaffeineFree),
-          onClick = { viewModel.toggleFilter(PurchaseViewModel.Filter.CaffeineFree) }
-        )
-      }
-    }
-
-    item("wrapped", span = { GridItemSpan(maxLineSpan) }) {
-      Surface(
-        color = MaterialTheme.colorScheme.primaryContainer,
-        contentColor = MaterialTheme.colorScheme.onPrimaryContainerTinted,
-        shape = RoundedCornerShape(8.dp),
-        modifier = Modifier.padding(vertical = 8.dp, horizontal = 12.dp)
-      ) {
-          Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 8.dp)) {
-            Spacer(Modifier.width(12.dp))
-            Column(verticalArrangement = Arrangement.Center) {
-              val now = Clock.System.now().toLocalDateTime(TimeZone.UTC)
-              Text(
-                "Your ${now.year} Wrapped is here",
-                style = MaterialTheme.typography.bodyLarge,
-              )
-              Text(
-                "Jump into your year in beverages",
-                style = MaterialTheme.typography.bodyMedium,
-              )
-            }
-            Spacer(Modifier.width(8.dp))
-            Button(
-              onClick = {
-                navigator.backStack.update {
-                  it.plus(MeteroidRoute.Wrapped(viewModel.serverId, viewModel.userId))
-                }
-              },
-            ) {
-              Text("Let's go")
-            }
-            Spacer(Modifier.width(4.dp))
-          }
-        }
-    }
-
-    items(
-      drinks,
-      key = { "drink-${it.serverId}-${it.drinkId}" },
-    ) { drink ->
-      PurchaseDrinkTile(drink) { item, count ->
-        viewModel.purchase(item, count, onBack = {})
-      }
-    }
-  }
-}
-
-@Composable
-fun PurchaseFilterChip(
-  label: String,
-  selected: Boolean,
-  onClick: () -> Unit,
-) {
-  FilterChip(
-    label = {
-      Text(label, style = MaterialTheme.typography.labelLarge)
-    },
-    selected = selected,
-    leadingIcon = {
-      if (selected) {
-        Icon(
-          Icons.Default.Check,
-          contentDescription = null,
-          modifier = Modifier.size(18.dp)
-        )
-      }
-    },
-    onClick = onClick,
-    colors = FilterChipDefaults.filterChipColors(
-      selectedContainerColor = MaterialTheme.colorScheme.secondaryContainer
-    )
-  )
-}
-
-@Composable
-fun PurchaseDrinkTile(
-  item: Drink,
-  modifier: Modifier = Modifier,
-  onPurchase: (Drink, Int) -> Unit = { _, _ -> }
-) {
-  var purchaseCount by remember { mutableIntStateOf(0) }
-  val pendingPurchases = purchaseCount != 0
-
-  LaunchedEffect(purchaseCount) {
-    delay(2000L)
-    onPurchase(item, purchaseCount)
-    purchaseCount = 0
-  }
-
-  val thumbPainter = rememberAsyncImagePainter(
-    item.logoUrl
-  )
-  val drinkPainter = rememberAsyncImagePainter(
-    item.originalLogoUrl,
-    error = thumbPainter
-  )
-
-  Column(
-    modifier = modifier
-        .height(IntrinsicSize.Max)
-        .alpha(if (item.active) 1.0f else 0.67f)
-        .clip(RoundedCornerShape(8.dp))
-        .clickable { purchaseCount += 1 }
-        .padding(8.dp)
-  ) {
-    Box(
-      Modifier
-          .aspectRatio(1.0f)
-          .background(MaterialTheme.colorScheme.secondaryGradient.verticalGradient(), CircleShape),
-      contentAlignment = Alignment.Center
-    ) {
-      Image(
-        drinkPainter,
-        contentDescription = null,
-        contentScale = ContentScale.Fit,
-        modifier = Modifier
-            .alpha(if (pendingPurchases) 0.0f else 1.0f)
-            .clip(CircleShape)
-      )
-      PriceBadge(
-        item.price,
-        modifier = Modifier
-            .alpha(if (pendingPurchases) 0.0f else 1.0f)
-            .align(Alignment.BottomEnd)
-            .paddingFromBaseline(bottom = 12.dp)
-      )
-      Text(
-        "×$purchaseCount",
-        fontSize = 36.sp,
-        fontWeight = FontWeight.Light,
-        color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.67f),
-        textAlign = TextAlign.Center,
-        modifier = Modifier.alpha(if (pendingPurchases) 1.0f else 0.0f)
-      )
-    }
-    Spacer(Modifier.height(4.dp))
-    Text(
-      item.name,
-      modifier = Modifier
-          .fillMaxWidth()
-          .padding(horizontal = 8.dp),
-      textAlign = TextAlign.Center,
-      fontWeight = FontWeight.SemiBold,
-      style = MaterialTheme.typography.labelLarge,
-    )
-    Spacer(Modifier.height(4.dp))
-    Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
-      val unitPrice =
-        if (item.volume <= BigDecimal.ZERO) null
-        else item.price / item.volume
-
-      Text(
-        if (unitPrice == null) String.format(Locale.getDefault(), "%.02fl", item.volume)
-        else String.format(Locale.getDefault(), "%.02fl · %.02f€/l", item.volume, item.price / item.volume),
-        modifier = Modifier
-            .fillMaxWidth()
-            .padding(horizontal = 8.dp),
-        textAlign = TextAlign.Center,
-        fontWeight = FontWeight.SemiBold,
-        style = MaterialTheme.typography.labelMedium,
-        color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
-      )
-    }
-  }
-}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseDrinkTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseDrinkTile.kt
new file mode 100644
index 0000000000000000000000000000000000000000..00adcbdce145128ab5570ef8b5f75eb7cb9a7637
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseDrinkTile.kt
@@ -0,0 +1,189 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2025 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.purchase
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.paddingFromBaseline
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import coil3.compose.rememberAsyncImagePainter
+import de.chaosdorf.meteroid.model.Drink
+import de.chaosdorf.meteroid.theme.secondaryGradient
+import de.chaosdorf.meteroid.ui.common.PriceBadge
+import kotlinx.coroutines.delay
+import java.math.BigDecimal
+import java.util.Locale
+
+@Composable
+fun PurchaseDrinkTile(
+    item: Drink,
+    modifier: Modifier = Modifier,
+    onPurchase: (Drink, Int) -> Unit = { _, _ -> }
+) {
+  var purchaseCount by remember { mutableIntStateOf(0) }
+  val pendingPurchases by remember {
+      derivedStateOf {
+          purchaseCount != 0
+      }
+  }
+
+    LaunchedEffect(purchaseCount) {
+        delay(2000L)
+        onPurchase(item, purchaseCount)
+        purchaseCount = 0
+    }
+
+  val thumbPainter = rememberAsyncImagePainter(
+      item.logoUrl
+  )
+  val drinkPainter = rememberAsyncImagePainter(
+      item.originalLogoUrl,
+      error = thumbPainter
+  )
+
+  val contentAlpha = remember(item) {
+      if (item.active) 1.0f else 0.67f
+  }
+
+    Column(
+        modifier = modifier
+            .height(IntrinsicSize.Max)
+            .clip(RoundedCornerShape(8.dp))
+            .clickable { purchaseCount += 1 }
+            .padding(8.dp)
+    ) {
+        Box(
+            Modifier.Companion
+                .aspectRatio(1.0f)
+                .background(
+                    brush = MaterialTheme.colorScheme.secondaryGradient.verticalGradient(),
+                    alpha = contentAlpha,
+                    shape = CircleShape,
+                ),
+            contentAlignment = Alignment.Companion.Center
+        ) {
+            Box(
+                modifier = Modifier.Companion
+                    .graphicsLayer {
+                        clip = true
+                        alpha = if (pendingPurchases) 0.0f else contentAlpha
+                    }
+            ) {
+                Image(
+                    drinkPainter,
+                    contentDescription = null,
+                    contentScale = ContentScale.Companion.Fit,
+                    modifier = Modifier.Companion
+                        .clip(CircleShape)
+                )
+                PriceBadge(
+                    item.price,
+                    modifier = Modifier.Companion
+                        .align(Alignment.Companion.BottomEnd)
+                        .paddingFromBaseline(bottom = 12.dp)
+                )
+            }
+            Text(
+                "×$purchaseCount",
+                fontSize = 36.sp,
+                fontWeight = FontWeight.Companion.Light,
+                color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.67f),
+                textAlign = TextAlign.Companion.Center,
+                modifier = Modifier.Companion
+                    .graphicsLayer {
+                        clip = true
+                        alpha = if (pendingPurchases) contentAlpha else 0.0f
+                    }
+            )
+        }
+        Spacer(Modifier.Companion.height(4.dp))
+        Text(
+            item.name,
+            modifier = Modifier.Companion
+                .fillMaxWidth()
+                .padding(horizontal = 8.dp),
+            textAlign = TextAlign.Companion.Center,
+            fontWeight = FontWeight.Companion.SemiBold,
+            style = MaterialTheme.typography.labelLarge,
+            color = MaterialTheme.colorScheme.onSurface.copy(alpha = contentAlpha)
+        )
+        Spacer(Modifier.Companion.height(4.dp))
+        Row(modifier = Modifier.Companion.align(Alignment.Companion.CenterHorizontally)) {
+            val label = remember(item) {
+                val unitPrice =
+                    if (item.volume <= BigDecimal.ZERO) null
+                    else item.price / item.volume
+
+                if (unitPrice == null) String.Companion.format(Locale.getDefault(), "%.02fl", item.volume)
+                else String.Companion.format(
+                    Locale.getDefault(),
+                    "%.02fl · %.02f€/l",
+                    item.volume,
+                    item.price / item.volume
+                )
+            }
+
+            Text(
+                label,
+                modifier = Modifier.Companion
+                    .fillMaxWidth()
+                    .padding(horizontal = 8.dp),
+                textAlign = TextAlign.Companion.Center,
+                fontWeight = FontWeight.Companion.SemiBold,
+                style = MaterialTheme.typography.labelMedium,
+                color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f * contentAlpha)
+            )
+        }
+    }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterChip.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterChip.kt
new file mode 100644
index 0000000000000000000000000000000000000000..609d77680c62b7b7e0e694330a1d09d2a4ff1e9f
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterChip.kt
@@ -0,0 +1,64 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2025 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.purchase
+
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.FilterChipDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun PurchaseFilterChip(
+  label: String,
+  selected: Boolean,
+  onClick: () -> Unit,
+) {
+    FilterChip(
+        label = {
+            Text(label, style = MaterialTheme.typography.labelLarge)
+        },
+        selected = selected,
+        leadingIcon = {
+            if (selected) {
+                Icon(
+                    Icons.Default.Check,
+                    contentDescription = null,
+                    modifier = Modifier.Companion.size(18.dp)
+                )
+            }
+        },
+        onClick = onClick,
+        colors = FilterChipDefaults.filterChipColors(
+            selectedContainerColor = MaterialTheme.colorScheme.secondaryContainer
+        )
+    )
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterRow.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterRow.kt
new file mode 100644
index 0000000000000000000000000000000000000000..309572a7ebfadb422ec54f0a2447af45dcbd623c
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseFilterRow.kt
@@ -0,0 +1,57 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2025 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.purchase
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import de.chaosdorf.meteroid.viewmodel.PurchaseViewModel
+
+@Preview(showBackground = true)
+@Composable
+fun PurchaseFilterRow(
+    filters: Set<PurchaseViewModel.Filter> = emptySet(),
+    toggleFilter: (filter: PurchaseViewModel.Filter) -> Unit = {},
+) {
+    FlowRow(
+        modifier = Modifier.Companion.padding(horizontal = 12.dp),
+        horizontalArrangement = Arrangement.spacedBy(8.dp)
+    ) {
+        PurchaseFilterChip(
+            label = "Active",
+            selected = filters.contains(PurchaseViewModel.Filter.Active),
+            onClick = { toggleFilter(PurchaseViewModel.Filter.Active) }
+        )
+        PurchaseFilterChip(
+            label = "Coffeine Free",
+            selected = filters.contains(PurchaseViewModel.Filter.CaffeineFree),
+            onClick = { toggleFilter(PurchaseViewModel.Filter.CaffeineFree) }
+        )
+    }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseRoute.kt
new file mode 100644
index 0000000000000000000000000000000000000000..be3cb023240f9914cace462c4d3d90a79a82b894
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/PurchaseRoute.kt
@@ -0,0 +1,79 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2025 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.purchase
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import de.chaosdorf.meteroid.ui.MeteroidRoute
+import de.chaosdorf.meteroid.viewmodel.Navigator
+import de.chaosdorf.meteroid.viewmodel.PurchaseViewModel
+import kotlinx.coroutines.flow.update
+
+@Composable
+fun PurchaseRoute(
+  viewModel: PurchaseViewModel,
+  navigator: Navigator,
+  contentPadding: PaddingValues,
+) {
+  val drinks by viewModel.drinks.collectAsState()
+  val filters by viewModel.filters.collectAsState()
+
+  LazyVerticalGrid(
+    GridCells.Adaptive(104.dp),
+    contentPadding = contentPadding,
+    modifier = Modifier.padding(horizontal = 8.dp),
+  ) {
+    item("filter", span = { GridItemSpan(maxLineSpan) }) {
+      PurchaseFilterRow(filters, viewModel::toggleFilter)
+    }
+
+    item("wrapped", span = { GridItemSpan(maxLineSpan) }) {
+      WrappedBanner(onClick = {
+        navigator.backStack.update {
+          it.plus(MeteroidRoute.Wrapped(viewModel.serverId, viewModel.userId))
+        }
+      })
+    }
+
+    items(
+      drinks,
+      key = { "drink-${it.serverId}-${it.drinkId}" },
+    ) { drink ->
+      PurchaseDrinkTile(drink) { item, count ->
+        viewModel.purchase(item, count, onBack = {})
+      }
+    }
+  }
+}
+
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/WrappedBanner.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/WrappedBanner.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b4784d21dfc16a67b6755dfb6cc35bf858c8ea21
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchase/WrappedBanner.kt
@@ -0,0 +1,83 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2025 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.purchase
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import de.chaosdorf.meteroid.theme.onPrimaryContainerTinted
+import kotlinx.datetime.Clock
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+
+@Preview(showBackground = true)
+@Composable
+fun WrappedBanner(
+  onClick: () -> Unit = {}
+) {
+    Surface(
+        color = MaterialTheme.colorScheme.primaryContainer,
+        contentColor = MaterialTheme.colorScheme.onPrimaryContainerTinted,
+        shape = RoundedCornerShape(8.dp),
+        modifier = Modifier.Companion.padding(vertical = 8.dp, horizontal = 12.dp)
+    ) {
+        Row(
+            horizontalArrangement = Arrangement.SpaceBetween,
+            verticalAlignment = Alignment.Companion.CenterVertically,
+            modifier = Modifier.Companion.padding(vertical = 8.dp)
+        ) {
+            Spacer(Modifier.Companion.width(12.dp))
+            Column(verticalArrangement = Arrangement.Center) {
+                val now = Clock.System.now().toLocalDateTime(TimeZone.Companion.UTC)
+                Text(
+                    "Your ${now.year} Wrapped is here",
+                    style = MaterialTheme.typography.bodyLarge,
+                )
+                Text(
+                    "Jump into your year in beverages",
+                    style = MaterialTheme.typography.bodyMedium,
+                )
+            }
+            Spacer(Modifier.Companion.width(8.dp))
+            Button(onClick = onClick) {
+                Text("Let's go")
+            }
+            Spacer(Modifier.Companion.width(4.dp))
+        }
+    }
+}