diff --git a/PlanTempus.Application/Features/Dashboard/Components/StatCard/Default.cshtml b/PlanTempus.Application/Features/Dashboard/Components/StatCard/Default.cshtml
new file mode 100644
index 0000000..dbbe501
--- /dev/null
+++ b/PlanTempus.Application/Features/Dashboard/Components/StatCard/Default.cshtml
@@ -0,0 +1,16 @@
+@model PlanTempus.Application.Features.Dashboard.Components.StatCardViewModel
+
+
+ @Model.Value
+ @Model.Label
+ @if (Model.HasTrend)
+ {
+
+ @if (!string.IsNullOrEmpty(Model.TrendIcon))
+ {
+
+ }
+ @Model.TrendText
+
+ }
+
diff --git a/PlanTempus.Application/Features/Dashboard/Components/StatCardViewComponent.cs b/PlanTempus.Application/Features/Dashboard/Components/StatCardViewComponent.cs
new file mode 100644
index 0000000..ccac82f
--- /dev/null
+++ b/PlanTempus.Application/Features/Dashboard/Components/StatCardViewComponent.cs
@@ -0,0 +1,90 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace PlanTempus.Application.Features.Dashboard.Components;
+
+///
+/// ViewComponent for rendering a stat card on the dashboard.
+///
+public class StatCardViewComponent : ViewComponent
+{
+ public IViewComponentResult Invoke(string key)
+ {
+ var model = StatCardCatalog.Get(key);
+ return View(model);
+ }
+}
+
+///
+/// ViewModel for the StatCard component.
+///
+public class StatCardViewModel
+{
+ public required string Key { get; init; }
+ public required string Value { get; init; }
+ public required string Label { get; init; }
+ public string? TrendText { get; init; }
+ public string? TrendIcon { get; init; }
+ public string? TrendDirection { get; init; }
+ public string? Variant { get; init; }
+ public bool HasTrend => !string.IsNullOrEmpty(TrendText);
+}
+
+///
+/// Catalog of available stat cards with their data.
+///
+public static class StatCardCatalog
+{
+ private static readonly Dictionary Cards = new()
+ {
+ ["bookings-today"] = new StatCardViewModel
+ {
+ Key = "bookings-today",
+ Value = "12",
+ Label = "Bookinger i dag",
+ TrendText = "4 gennemført, 2 i gang",
+ TrendIcon = "ph-check-circle",
+ TrendDirection = "up",
+ Variant = "highlight"
+ },
+ ["expected-revenue"] = new StatCardViewModel
+ {
+ Key = "expected-revenue",
+ Value = "8.450 kr",
+ Label = "Forventet omsætning",
+ TrendText = "+12% vs. gennemsnit",
+ TrendIcon = "ph-trend-up",
+ TrendDirection = "up",
+ Variant = "success"
+ },
+ ["occupancy-rate"] = new StatCardViewModel
+ {
+ Key = "occupancy-rate",
+ Value = "78%",
+ Label = "Belægningsgrad",
+ TrendText = "God kapacitet",
+ TrendIcon = "ph-trend-up",
+ TrendDirection = "up",
+ Variant = null
+ },
+ ["needs-attention"] = new StatCardViewModel
+ {
+ Key = "needs-attention",
+ Value = "4",
+ Label = "Kræver opmærksomhed",
+ TrendText = null,
+ TrendIcon = null,
+ TrendDirection = null,
+ Variant = "warning"
+ }
+ };
+
+ public static StatCardViewModel Get(string key)
+ {
+ if (Cards.TryGetValue(key, out var card))
+ return card;
+
+ throw new KeyNotFoundException($"StatCard with key '{key}' not found");
+ }
+
+ public static IEnumerable AllKeys => Cards.Keys;
+}
diff --git a/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml b/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml
index 9e3b36c..92c24f0 100644
--- a/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml
+++ b/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml
@@ -1,5 +1,5 @@
@page "/"
-@using PlanTempus.Application.Features.Dashboard.Pages
+@using PlanTempus.Application.Features.Dashboard.Pages
@model PlanTempus.Application.Features.Dashboard.Pages.IndexModel
@{
ViewData["Title"] = "Dashboard";
@@ -8,34 +8,10 @@
-
- 12
- Bookinger i dag
-
-
- 4 gennemført, 2 i gang
-
-
-
- 8.450 kr
- Forventet omsætning
-
-
- +12% vs. gennemsnit
-
-
-
- 78%
- Belægningsgrad
-
-
- God kapacitet
-
-
-
- 4
- Kræver opmærksomhed
-
+ @await Component.InvokeAsync("StatCard", "bookings-today")
+ @await Component.InvokeAsync("StatCard", "expected-revenue")
+ @await Component.InvokeAsync("StatCard", "occupancy-rate")
+ @await Component.InvokeAsync("StatCard", "needs-attention")
diff --git a/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml b/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml
index e8bb2e3..bc1b7d0 100644
--- a/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml
+++ b/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml
@@ -14,12 +14,27 @@
+
@await RenderSectionAsync("Styles", required: false)
-
+
+
+
+
+
+ Du ser en demo af PlanTempus.
+
+
+
+ Opret konto
+
+
+
+
+
@await Component.InvokeAsync("SideMenu")
diff --git a/PlanTempus.Application/wwwroot/css/demo-banner.css b/PlanTempus.Application/wwwroot/css/demo-banner.css
new file mode 100644
index 0000000..39b9179
--- /dev/null
+++ b/PlanTempus.Application/wwwroot/css/demo-banner.css
@@ -0,0 +1,145 @@
+/**
+ * Demo Banner
+ *
+ * Persistent banner shown at the top of pages when in demo mode.
+ * Provides CTA to subscription selection.
+ */
+
+/* ===========================================
+ DEMO BANNER
+ =========================================== */
+swp-demo-banner {
+ /* TODO: Remove display:none when demo mode is ready */
+ display: none;
+ align-items: center;
+ justify-content: center;
+ gap: var(--spacing-4);
+ padding: var(--spacing-3) var(--spacing-6);
+ background: linear-gradient(135deg, var(--color-teal) 0%, #00796b 100%);
+ color: white;
+ font-size: var(--font-size-sm);
+ position: relative;
+ z-index: 1000;
+}
+
+swp-demo-banner-text {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-2);
+}
+
+swp-demo-banner-text i {
+ font-size: 18px;
+ opacity: 0.9;
+}
+
+swp-demo-banner-text span {
+ opacity: 0.95;
+}
+
+swp-demo-banner-text strong {
+ font-weight: var(--font-weight-semibold);
+ opacity: 1;
+}
+
+swp-demo-banner-cta {
+ display: inline-flex;
+}
+
+swp-demo-banner-cta a {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--spacing-2);
+ padding: var(--spacing-2) var(--spacing-4);
+ background: white;
+ color: var(--color-teal);
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-semibold);
+ border-radius: var(--radius-pill);
+ text-decoration: none;
+ transition: all var(--transition-fast);
+ white-space: nowrap;
+}
+
+swp-demo-banner-cta a:hover {
+ background: rgba(255, 255, 255, 0.95);
+ color: #00695c;
+ transform: translateY(-1px);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+swp-demo-banner-cta i {
+ font-size: 16px;
+}
+
+/* Close button (optional) */
+swp-demo-banner-close {
+ position: absolute;
+ right: var(--spacing-4);
+ top: 50%;
+ transform: translateY(-50%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ background: rgba(255, 255, 255, 0.15);
+ border-radius: 50%;
+ color: white;
+ cursor: pointer;
+ opacity: 0.7;
+ transition: all var(--transition-fast);
+}
+
+swp-demo-banner-close:hover {
+ opacity: 1;
+ background: rgba(255, 255, 255, 0.25);
+}
+
+swp-demo-banner-close i {
+ font-size: 16px;
+}
+
+/* ===========================================
+ BODY OFFSET (when banner is visible)
+ =========================================== */
+body.has-demo-banner swp-app-layout {
+ /* Adjust if banner needs to push content down */
+}
+
+/* ===========================================
+ RESPONSIVE
+ =========================================== */
+@media (max-width: 768px) {
+ swp-demo-banner {
+ flex-wrap: wrap;
+ gap: var(--spacing-2);
+ padding: var(--spacing-3) var(--spacing-4);
+ }
+
+ swp-demo-banner-text {
+ flex: 1 1 100%;
+ justify-content: center;
+ text-align: center;
+ }
+
+ swp-demo-banner-cta {
+ flex: 1 1 100%;
+ justify-content: center;
+ }
+
+ swp-demo-banner-cta a {
+ width: 100%;
+ justify-content: center;
+ }
+}
+
+@media (max-width: 480px) {
+ swp-demo-banner-text span {
+ display: none;
+ }
+
+ swp-demo-banner-text strong {
+ display: inline;
+ }
+}