{#each bottomItems as item}
diff --git a/apps/publisher-dashboard/src/lib/utils/format-date.ts b/apps/publisher-dashboard/src/lib/utils/format-date.ts
new file mode 100644
index 0000000..fe0b0af
--- /dev/null
+++ b/apps/publisher-dashboard/src/lib/utils/format-date.ts
@@ -0,0 +1,31 @@
+/**
+ * Date formatting utilities for consistent display across the app
+ */
+
+/**
+ * Format a date for display in tables and lists
+ * Example: "Jan 15, 2024"
+ */
+export function formatDate(date: string | Date): string {
+ const d = typeof date === "string" ? new Date(date) : date;
+ return d.toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ });
+}
+
+/**
+ * Format a date with time for detailed views
+ * Example: "Jan 15, 2024, 3:30 PM"
+ */
+export function formatDateTime(date: string | Date): string {
+ const d = typeof date === "string" ? new Date(date) : date;
+ return d.toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ });
+}
diff --git a/apps/publisher-dashboard/src/routes/admin/+layout.svelte b/apps/publisher-dashboard/src/routes/admin/+layout.svelte
new file mode 100644
index 0000000..0194281
--- /dev/null
+++ b/apps/publisher-dashboard/src/routes/admin/+layout.svelte
@@ -0,0 +1,51 @@
+
+
+{@render children()}
diff --git a/apps/publisher-dashboard/src/routes/admin/+page.svelte b/apps/publisher-dashboard/src/routes/admin/+page.svelte
new file mode 100644
index 0000000..5772f80
--- /dev/null
+++ b/apps/publisher-dashboard/src/routes/admin/+page.svelte
@@ -0,0 +1,112 @@
+
+
+
+ Admin Dashboard | Publisher Dashboard
+
+
+
+
+
+
+ Admin
+
+
+ {#if isLoading}
+
+
+
+
Loading admin data...
+
+ {:else if hasError}
+
+
+
+
+ {hasError instanceof Error ? hasError.message : "Failed to load admin data"}
+
+
+ {:else}
+
+
+
+
+
+
+ Quick Actions
+
+
+
+
+
+
+
+ {/if}
+
+
diff --git a/apps/publisher-dashboard/src/routes/admin/orgs/+page.svelte b/apps/publisher-dashboard/src/routes/admin/orgs/+page.svelte
new file mode 100644
index 0000000..916ca58
--- /dev/null
+++ b/apps/publisher-dashboard/src/routes/admin/orgs/+page.svelte
@@ -0,0 +1,215 @@
+
+
+
+ Organizations | Admin | Publisher Dashboard
+
+
+
+
+ {#if orgsQuery.isPending}
+
+
+
+
Loading organizations...
+
+ {:else if orgsQuery.error}
+
+
+
+
+ {orgsQuery.error instanceof Error ? orgsQuery.error.message : "Failed to load organizations"}
+
+
+ {:else if orgsQuery.data}
+
+
+
+ Organizations ({orgsQuery.data.length})
+
+
+
+
+ {#if orgsQuery.data.length === 0}
+
+
+
+
+
+
+ No organizations yet
+
+ Create your first organization to get started.
+
+
+
+
+ {:else}
+
+
+
+
+
+ All Organizations
+
+
+
+
+
+
+ Slug
+ Display Name
+ Created At
+ Actions
+
+
+
+ {#each orgsQuery.data as org (org.id)}
+
+ {org.slug}
+ {org.displayName}
+
+ {formatDate(org.createdAt)}
+
+
+
+
+
+
+
+
+ {/each}
+
+
+
+
+ {/if}
+
+
+
+ {/if}
+
+
+
+
+
confirmDialogOpen = false}
+/>
diff --git a/apps/publisher-dashboard/src/routes/admin/orgs/[slug]/+page.svelte b/apps/publisher-dashboard/src/routes/admin/orgs/[slug]/+page.svelte
new file mode 100644
index 0000000..6e3bb0f
--- /dev/null
+++ b/apps/publisher-dashboard/src/routes/admin/orgs/[slug]/+page.svelte
@@ -0,0 +1,474 @@
+
+
+
+
+ {orgQuery.data?.displayName ?? "Organization"} | Admin | Publisher Dashboard
+
+
+
+
+ {#if orgQuery.isPending}
+
+
+
Loading organization...
+
+ {:else if orgQuery.error}
+
+ {:else if orgQuery.data}
+ {@const org = orgQuery.data}
+
+
+
+
+ Back to organizations
+
+
+
+
+
+
+ {#if org.logoUrl}
+

+ {:else}
+
+
+
+ {/if}
+
+
{org.displayName}
+
+ Slug: {org.slug}
+
+
+ Created {formatDate(org.createdAt)}
+
+
+
+
+
+
+
+
+
+ Settings
+
+ Update the organization's display name and logo.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sites ({sitesQuery.data?.length ?? 0})
+
+
+ Manage the sites associated with this organization.
+
+
+
+ {#if sitesQuery.isPending}
+
+
+
+ {:else if sitesQuery.error}
+
+
+
+ {sitesQuery.error instanceof Error
+ ? sitesQuery.error.message
+ : "Failed to load sites"}
+
+
+ {:else if sitesQuery.data && sitesQuery.data.length > 0}
+
+
+
+ Domain
+ Actions
+
+
+
+ {#each sitesQuery.data as site (site.id)}
+
+ {site.domain}
+
+
+
+
+ {/each}
+
+
+ {:else}
+ No sites configured yet.
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+ Danger Zone
+
+
+ Irreversible actions that permanently affect this organization.
+
+
+
+
+
+
+ Deleting this organization will permanently remove all members,
+ invitations, and sites. This action cannot be undone.
+
+
+
+
+
+
+ {/if}
+
+
+
+ {
+ confirmDialogOpen = false;
+ pendingAction = null;
+ }}
+/>
diff --git a/apps/publisher-dashboard/src/routes/admin/orgs/new/+page.svelte b/apps/publisher-dashboard/src/routes/admin/orgs/new/+page.svelte
new file mode 100644
index 0000000..41dc247
--- /dev/null
+++ b/apps/publisher-dashboard/src/routes/admin/orgs/new/+page.svelte
@@ -0,0 +1,160 @@
+
+
+
+ New Organization | Admin | Publisher Dashboard
+
+
+
+
+
+
+
+ Back to organizations
+
+
+
+
+
+ New Organization
+
+ Create a new organization. The owner will receive access automatically.
+
+
+
+
+
+
+
+
diff --git a/apps/publisher-dashboard/src/routes/admin/users/+page.svelte b/apps/publisher-dashboard/src/routes/admin/users/+page.svelte
new file mode 100644
index 0000000..eb02f1f
--- /dev/null
+++ b/apps/publisher-dashboard/src/routes/admin/users/+page.svelte
@@ -0,0 +1,113 @@
+
+
+
+ Users | Admin | Publisher Dashboard
+
+
+
+ {#if usersQuery.isPending}
+
+ {:else if usersQuery.error}
+
+
+
+ {usersQuery.error instanceof Error ? usersQuery.error.message : "Failed to load users"}
+
+
+ {:else if usersQuery.data}
+
+
+
+
+
+ Users ({usersQuery.data.length})
+
+
+
+ {#if usersQuery.data.length > 0}
+
+
+
+ Email
+ Display Name
+ Email Verified
+ Superuser
+ Actions
+
+
+
+ {#each usersQuery.data as user (user.id)}
+
+ {user.email}
+
+ {user.displayName ?? "-"}
+
+
+ {#if user.emailVerified}
+
+ {:else}
+
+ {/if}
+
+
+ {#if user.isSuperuser}
+
+ {/if}
+
+
+
+
+
+ {/each}
+
+
+ {:else}
+ No users found
+ {/if}
+
+
+
+ {/if}
+
diff --git a/apps/publisher-dashboard/src/routes/admin/users/[email]/+page.svelte b/apps/publisher-dashboard/src/routes/admin/users/[email]/+page.svelte
new file mode 100644
index 0000000..218b551
--- /dev/null
+++ b/apps/publisher-dashboard/src/routes/admin/users/[email]/+page.svelte
@@ -0,0 +1,302 @@
+
+
+
+ {userDetailsQuery.data?.displayName ?? email} | Users | Admin
+
+
+
+
+
+
+ {#if userDetailsQuery.isPending}
+
+ {:else if userDetailsQuery.error}
+
+
+
+ {userDetailsQuery.error instanceof Error
+ ? userDetailsQuery.error.message
+ : "Failed to load user"}
+
+
+ {:else if userDetailsQuery.data}
+ {@const user = userDetailsQuery.data}
+
+
+
+
+
+
+ {getInitials(user.displayName, user.email)}
+
+
+
+
+ {user.displayName ?? user.email}
+
+ {#if user.isSuperuser}
+
+ {/if}
+
+
+
+ {user.email}
+
+
+
+
+
+
+
+
+
+
+
+ Profile Information
+
+ Read-only user profile details
+
+
+
+
+
- Email
+ - {user.email}
+
+
+
- Display Name
+ - {user.displayName ?? "-"}
+
+
+
- Full Name
+ - {user.fullName ?? "-"}
+
+
+
- Phone Number
+ - {user.phoneNumber ?? "-"}
+
+
+
- Email Verified
+ -
+ {#if user.emailVerified}
+
+ Yes
+ {:else}
+
+ No
+ {/if}
+
+
+
+
+
+
+
+
+
+ Permissions
+ Manage user access levels
+
+
+ {#if isViewingSelf}
+
+
+
+ You cannot modify your own superuser status. Another superuser must make this
+ change.
+
+
+ {:else}
+
+ {/if}
+
+ {#if !isViewingSelf}
+
+
+
+ {/if}
+
+
+
+ {#if !user.emailVerified}
+
+
+ Actions
+ Administrative actions for this user
+
+
+
+
+
+ {/if}
+
+ {/if}
+
diff --git a/docs/initial-app.md b/docs/initial-app.md
index 29931f9..d816db9 100644
--- a/docs/initial-app.md
+++ b/docs/initial-app.md
@@ -2363,14 +2363,24 @@ _Depends on: J1-J6, C3_
- Reusable components: `$lib/components/org/role-badge.svelte`, `confirm-dialog.svelte`
- Sidebar updated with "Organizations" nav item
-#### Workstream M: Admin Pages (Frontend)
+#### Workstream M: Admin Pages (Frontend) ✅
_Depends on: K1-K5, C3_
_Can run parallel to L_
-- [ ] **M1**: Create `/admin` dashboard page
-- [ ] **M2**: Create `/admin/orgs` pages (list, new, details)
-- [ ] **M3**: Create `/admin/users` pages (list, details)
+- [x] **M1**: Create `/admin` dashboard page
+- [x] **M2**: Create `/admin/orgs` pages (list, new, details)
+- [x] **M3**: Create `/admin/users` pages (list, details)
+
+**Implementation notes:**
+- Admin layout at `/routes/admin/+layout.svelte` provides superuser access control
+- Redirects non-superusers to `/dashboard` with toast error
+- Admin dashboard shows org/user counts with quick action links
+- Org management: list all orgs, create new with owner email, view/edit details, manage sites
+- User management: list all users, view details, toggle superuser status, confirm email
+- Sidebar shows admin link (shield icon) only for superusers
+- Reusable component: `$lib/components/admin/superuser-badge.svelte`
+- All destructive actions use ConfirmDialog
---
diff --git a/docs/test-plans/admin.md b/docs/test-plans/admin.md
new file mode 100644
index 0000000..3710234
--- /dev/null
+++ b/docs/test-plans/admin.md
@@ -0,0 +1,317 @@
+# Test Plan: Admin Dashboard (Workstream M)
+
+## Overview
+
+Manual UI test plan for superuser-only admin management pages:
+- `/admin` - Admin dashboard
+- `/admin/orgs` - Organization list
+- `/admin/orgs/new` - Create organization
+- `/admin/orgs/[slug]` - Organization details
+- `/admin/users` - User list
+- `/admin/users/[email]` - User details
+
+## Prerequisites
+
+- Dev server running: `bun run --cwd apps/publisher-dashboard dev`
+- Test accounts:
+ - Superuser account (has `is_superuser = true`)
+ - Regular user account (not a superuser)
+- At least one organization with sites
+- At least one user who is not a superuser
+
+---
+
+## 1. Access Control
+
+### 1.1 Superuser Access
+- [ ] Superuser visiting `/admin` sees admin dashboard
+- [ ] Superuser can access all admin sub-pages
+
+### 1.2 Non-Superuser Access
+- [ ] Regular user visiting `/admin` gets redirected to `/dashboard`
+- [ ] Toast error message: "Access denied. Superuser privileges required."
+- [ ] Regular user visiting `/admin/orgs` gets redirected
+- [ ] Regular user visiting `/admin/users` gets redirected
+
+### 1.3 Unauthenticated Access
+- [ ] Unauthenticated user visiting `/admin` redirects to `/auth/login`
+- [ ] After login as superuser, returns to `/admin`
+
+---
+
+## 2. Admin Dashboard (`/admin`)
+
+### 2.1 Display
+- [ ] Page title is "Admin Dashboard"
+- [ ] Red "Admin" badge visible at top
+- [ ] Summary cards display:
+ - Organizations card with correct count
+ - Users card with correct count
+- [ ] Cards are clickable and navigate to respective list pages
+
+### 2.2 Quick Actions
+- [ ] "New Organization" button visible
+- [ ] Button navigates to `/admin/orgs/new`
+
+### 2.3 Loading States
+- [ ] Loading spinner shows while fetching data
+- [ ] Error state displays if API fails
+
+---
+
+## 3. Organization List (`/admin/orgs`)
+
+### 3.1 Display
+- [ ] Page title is "Organizations"
+- [ ] Header shows "Organizations (count)" with correct count
+- [ ] "New Organization" button visible in header
+- [ ] Table displays all organizations (not just user's orgs)
+
+### 3.2 Table Content
+- [ ] Slug column displays org slug
+- [ ] Display Name column shows org name
+- [ ] Created At column shows formatted date
+- [ ] Actions column has View and Delete buttons
+
+### 3.3 View Action
+- [ ] View button navigates to `/admin/orgs/[slug]`
+
+### 3.4 Delete Action
+- [ ] Delete button opens confirmation dialog
+- [ ] Dialog shows org name and warning message
+- [ ] Cancel button closes dialog without action
+- [ ] Confirm button deletes organization
+- [ ] Success toast: "Organization deleted"
+- [ ] Org disappears from list after deletion
+- [ ] Error toast on failure
+
+### 3.5 Empty State
+- [ ] Shows appropriate message when no organizations exist
+
+---
+
+## 4. Create Organization (`/admin/orgs/new`)
+
+### 4.1 Display
+- [ ] Page title is "New Organization"
+- [ ] Back link "Back to organizations" works
+
+### 4.2 Form Fields
+- [ ] Slug input: accepts lowercase alphanumeric and hyphens
+- [ ] Slug input: auto-converts uppercase to lowercase
+- [ ] Slug input: strips invalid characters
+- [ ] Display Name input: accepts any text
+- [ ] Owner Email input: validates email format
+
+### 4.3 Form Validation
+- [ ] Submit button disabled when fields are empty
+- [ ] Submit button enabled when all fields filled
+- [ ] Form submits on button click
+
+### 4.4 Submit Flow
+- [ ] Loading state on submit button
+- [ ] Success toast: "Organization created"
+- [ ] Redirects to `/admin/orgs` on success
+- [ ] Error toast on failure (e.g., slug already exists)
+
+---
+
+## 5. Organization Details (`/admin/orgs/[slug]`)
+
+### 5.1 Header Section
+- [ ] Org logo displays if set, placeholder icon otherwise
+- [ ] Display name shown prominently
+- [ ] Slug displayed
+- [ ] Created date shown
+
+### 5.2 Settings Card
+- [ ] Display name input pre-filled with current value
+- [ ] Logo URL input pre-filled if set
+- [ ] Save button disabled when no changes
+- [ ] Save button enabled when form is dirty
+- [ ] Success toast on save: "Organization updated"
+- [ ] Changes reflected after save
+
+### 5.3 Sites Card
+- [ ] Title shows "Sites (count)"
+- [ ] Table shows all sites for the org
+- [ ] Each site has domain and Remove button
+
+### 5.4 Add Site
+- [ ] Domain input visible
+- [ ] Add button visible
+- [ ] Adding valid domain shows success toast
+- [ ] New site appears in list
+- [ ] Error toast on invalid/duplicate domain
+
+### 5.5 Remove Site
+- [ ] Remove button opens confirmation dialog
+- [ ] Dialog shows domain being removed
+- [ ] Confirm removes site from list
+- [ ] Success toast on removal
+
+### 5.6 Danger Zone
+- [ ] Card has red border styling
+- [ ] Warning text about permanent deletion
+- [ ] Delete button opens confirmation dialog
+- [ ] Confirm deletes org and redirects to `/admin/orgs`
+- [ ] Success toast on deletion
+
+### 5.7 Navigation
+- [ ] Back link works
+- [ ] 404 error for non-existent org slug
+
+---
+
+## 6. User List (`/admin/users`)
+
+### 6.1 Display
+- [ ] Page title is "Users"
+- [ ] Header shows "Users (count)" with correct count
+- [ ] Table displays all users in system
+
+### 6.2 Table Content
+- [ ] Email column displays user email
+- [ ] Display Name column shows name (or "-" if not set)
+- [ ] Email Verified column shows checkmark or X icon
+- [ ] Superuser column shows SuperuserBadge for superusers
+- [ ] Actions column has View button
+
+### 6.3 View Action
+- [ ] View button navigates to `/admin/users/[email]`
+- [ ] Email is URL-encoded in the link
+
+### 6.4 Empty State
+- [ ] Shows appropriate message when no users exist
+
+---
+
+## 7. User Details (`/admin/users/[email]`)
+
+### 7.1 Header Section
+- [ ] Avatar with initials displays
+- [ ] Display name shown (or "Unknown" if not set)
+- [ ] Email shown below name
+- [ ] SuperuserBadge shown if user is superuser
+
+### 7.2 Profile Info Card
+- [ ] Email displayed (read-only)
+- [ ] Display Name displayed
+- [ ] Full Name displayed
+- [ ] Phone Number displayed
+- [ ] Email Verified status (Yes/No)
+
+### 7.3 Permissions Card
+- [ ] Superuser checkbox visible
+- [ ] Checkbox reflects current status
+- [ ] Save button disabled when no changes
+- [ ] Save button enabled when checkbox changed
+
+### 7.4 Toggle Superuser
+- [ ] Can grant superuser to regular user
+- [ ] Can revoke superuser from superuser (if not self)
+- [ ] Success toast on save
+- [ ] Cannot demote self (checkbox disabled when viewing own profile)
+- [ ] Warning shown when viewing own profile
+
+### 7.5 Actions Card
+- [ ] "Confirm Email" button visible only if email not verified
+- [ ] Hidden if email already verified
+- [ ] Button confirms email on click
+- [ ] Success toast: "Email confirmed"
+- [ ] Button disappears after confirmation
+
+### 7.6 Navigation
+- [ ] Back link works
+- [ ] 404 error for non-existent user email
+
+---
+
+## 8. Sidebar Navigation
+
+### 8.1 Admin Link
+- [ ] Shield icon visible for superusers
+- [ ] Hidden for regular users
+- [ ] Tooltip shows "Admin" on hover
+- [ ] Clicking navigates to `/admin`
+- [ ] Active state (red tint) when on `/admin` routes
+
+---
+
+## 9. Cross-Cutting Concerns
+
+### 9.1 Loading States
+- [ ] All pages show loading spinner during data fetch
+- [ ] Buttons show loading state during operations
+
+### 9.2 Error Handling
+- [ ] API errors display user-friendly messages
+- [ ] Toast notifications for action results
+- [ ] Error states don't crash the app
+
+### 9.3 Responsive Design
+- [ ] Pages render correctly on mobile viewport
+- [ ] Tables scroll horizontally on small screens
+- [ ] Forms stack vertically on mobile
+
+### 9.4 Query Invalidation
+- [ ] After org create: org list refreshes
+- [ ] After org delete: org list refreshes
+- [ ] After org update: org details refresh
+- [ ] After add site: sites list refreshes
+- [ ] After remove site: sites list refreshes
+- [ ] After user update: user details refresh
+- [ ] After confirm email: user details refresh
+
+---
+
+## 10. Edge Cases
+
+### 10.1 Self-Demotion Prevention
+- [ ] Cannot remove own superuser status
+- [ ] Warning message explains why
+
+### 10.2 Special Characters in Email
+- [ ] User with `+` in email can be viewed
+- [ ] User with `.` in email can be viewed
+- [ ] Email properly URL-encoded/decoded
+
+### 10.3 Long Content
+- [ ] Long org names truncate or wrap properly
+- [ ] Long email addresses don't break layout
+- [ ] Long URLs in logo field don't break layout
+
+### 10.4 Empty States
+- [ ] Org with no sites shows "No sites" message
+- [ ] Empty org list shows appropriate message
+- [ ] Empty user list shows appropriate message
+
+---
+
+## Test Matrix: Admin vs Non-Admin
+
+| Feature | Superuser | Regular User |
+|---------|-----------|--------------|
+| View admin dashboard | Yes | Redirected |
+| View org list | Yes | Redirected |
+| Create organization | Yes | Redirected |
+| View org details | Yes | Redirected |
+| Edit org settings | Yes | Redirected |
+| Manage org sites | Yes | Redirected |
+| Delete organization | Yes | Redirected |
+| View user list | Yes | Redirected |
+| View user details | Yes | Redirected |
+| Toggle superuser | Yes (not self) | Redirected |
+| Confirm user email | Yes | Redirected |
+| See admin link in sidebar | Yes | No |
+
+---
+
+## Regression Checklist
+
+After any changes to admin pages, verify:
+- [ ] Access control still redirects non-superusers
+- [ ] All CRUD operations function
+- [ ] Error states still display
+- [ ] Navigation works end-to-end
+- [ ] Sidebar admin link visibility correct