diff --git a/src/routes/(app)/crew/+layout.server.ts b/src/routes/(app)/crew/+layout.server.ts
new file mode 100644
index 00000000..f24f897e
--- /dev/null
+++ b/src/routes/(app)/crew/+layout.server.ts
@@ -0,0 +1,14 @@
+import { redirect } from '@sveltejs/kit'
+import type { LayoutServerLoad } from './$types'
+
+export const load: LayoutServerLoad = async ({ locals, url }) => {
+ // Check authentication first
+ if (!locals.session.isAuthenticated) {
+ throw redirect(302, '/auth/login')
+ }
+
+ return {
+ user: locals.session.user,
+ account: locals.session.account
+ }
+}
diff --git a/src/routes/(app)/crew/+layout.svelte b/src/routes/(app)/crew/+layout.svelte
new file mode 100644
index 00000000..42f5e75c
--- /dev/null
+++ b/src/routes/(app)/crew/+layout.svelte
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+ {#if crewQuery.isLoading}
+
+ {:else if crewQuery.isError || !crewStore.isInCrew}
+
+
+
+
+ Crews let you team up with other players, track Guild War scores, and share strategies.
+
+
+
+ (createModalOpen = true)}>
+ Create a Crew
+
+
+
+
+ {#if invitationsQuery.data && invitationsQuery.data.length > 0}
+
+ {/if}
+
+ {:else}
+
+
+
+ {#snippet actions()}
+ {#if crewStore.isOfficer}
+ Settings
+ {/if}
+ {/snippet}
+
+
+
+
+
+
+
+ {#if eventsQuery.isLoading}
+
+ {:else if eventsQuery.data && eventsQuery.data.length > 0}
+
+ {:else}
+
No events yet
+ {/if}
+
+ {/if}
+
+
+
+
+
+ {#if crewQuery.isLoading}
+
+ {:else}
+
+ {/if}
+
+
+
diff --git a/src/routes/(app)/crew/join/+page.svelte b/src/routes/(app)/crew/join/+page.svelte
new file mode 100644
index 00000000..7e75a93a
--- /dev/null
+++ b/src/routes/(app)/crew/join/+page.svelte
@@ -0,0 +1,308 @@
+
+
+
+ {#if invitationsQuery.isLoading}
+
+
Loading invitations...
+
+ {:else if invitationsQuery.isError}
+
+
Failed to load invitations
+
+ {:else if !invitationsQuery.data || invitationsQuery.data.length === 0}
+
+
You don't have any pending invitations.
+
+ Ask a crew captain or vice captain to send you an invitation.
+
+
goto('/crew')}>
+ Go Back
+
+
+ {:else}
+
+ {#each invitationsQuery.data as invitation}
+ {@const expired = isExpired(invitation.expiresAt)}
+ {@const highlighted = invitation.id === selectedInvitationId}
+ {@const crew = invitation.crew}
+ {@const invitedBy = invitation.invitedBy}
+
+ {#if crew && invitedBy}
+
+
+
+ {#if 'description' in crew && crew.description}
+
{crew.description}
+ {/if}
+
+
+
+ Invited by {invitedBy.username}
+
+
+ {formatDate(invitation.createdAt)}
+
+
+
+ {#if 'memberCount' in crew && crew.memberCount !== undefined}
+
+
+ {crew.memberCount} member{crew.memberCount === 1 ? '' : 's'}
+
+
+ {/if}
+
+ {#if expired}
+
+ This invitation has expired.
+
+ {:else}
+
+ Expires: {formatDate(invitation.expiresAt)}
+
+
+
+ handleReject(invitation.id)}
+ disabled={processingId === invitation.id}
+ >
+ {processingId === invitation.id && rejectMutation.isPending ? 'Declining...' : 'Decline'}
+
+ handleAccept(invitation.id)}
+ disabled={processingId === invitation.id}
+ >
+ {processingId === invitation.id && acceptMutation.isPending ? 'Joining...' : 'Accept'}
+
+
+ {/if}
+
+ {/if}
+ {/each}
+
+ {/if}
+
+
+
diff --git a/src/routes/(app)/crew/settings/+page.svelte b/src/routes/(app)/crew/settings/+page.svelte
new file mode 100644
index 00000000..14fce1b1
--- /dev/null
+++ b/src/routes/(app)/crew/settings/+page.svelte
@@ -0,0 +1,555 @@
+
+
+
+
+ {#if errors.form}
+
+ {errors.form}
+
+ {/if}
+
+
+
+ Crew Name *
+
+
+ {#if errors.name}
+ {errors.name}
+ {/if}
+
+
+
+ Gamertag
+
+ {#if errors.gamertag}
+ {errors.gamertag}
+ {/if}
+ Short tag displayed next to member usernames
+
+
+
+ In-Game Crew ID
+
+
+
+
+ Description
+
+ {#if errors.description}
+ {errors.description}
+ {/if}
+
+
+
+
+ {updateCrewMutation.isPending ? 'Saving...' : 'Save Changes'}
+
+
+
+
+
+
+
Danger Zone
+
+ {#if crewStore.isCaptain}
+
+
+
Transfer Captain
+
Transfer ownership of the crew to another member.
+
+
(transferDialogOpen = true)}>
+ Transfer
+
+
+ {/if}
+
+ {#if crewStore.canLeaveCrew}
+
+
+
Leave Crew
+
Leave this crew. You'll need an invitation to rejoin.
+
+
(leaveDialogOpen = true)}>
+ Leave Crew
+
+
+ {:else if crewStore.isCaptain}
+
As captain, you must transfer ownership before leaving the crew.
+ {/if}
+
+
+
+
+
+ {#if eventQuery.isLoading}
+
+ {:else if eventQuery.isError}
+
+
Failed to load event
+
Back to Events
+
+ {:else if event}
+
+ {#snippet leftAccessory()}
+ Back
+ {/snippet}
+ {#snippet rightAccessory()}
+ {#if canEdit}
+ Edit
+ {/if}
+ {/snippet}
+
+
+
+
+
+
+
+ {elementLabels[event.element] ?? 'Unknown'}
+
+
+
+
+
+
+ {#if event.createdAt}
+
+
+ {#if event.updatedAt}
+
+ {/if}
+
+ {/if}
+
+ {:else}
+
+
Event Not Found
+
The event you're looking for could not be found.
+
Back to Events
+
+ {/if}
+
+
+
diff --git a/src/routes/(app)/database/gw-events/[id]/edit/+page.svelte b/src/routes/(app)/database/gw-events/[id]/edit/+page.svelte
new file mode 100644
index 00000000..960f8978
--- /dev/null
+++ b/src/routes/(app)/database/gw-events/[id]/edit/+page.svelte
@@ -0,0 +1,223 @@
+
+ {#if eventQuery.isLoading}
+
+ {:else if event}
+
+ {#snippet leftAccessory()}
+ Cancel
+ {/snippet}
+ {#snippet rightAccessory()}
+
+ {isSaving ? 'Saving...' : 'Save'}
+
+ {/snippet}
+
+
+ {#if saveError}
+
{saveError}
+ {/if}
+
+
+ {:else}
+
+
Event Not Found
+
The event you're looking for could not be found.
+
goto('/database/gw-events')}>
+ Back to Events
+
+
+ {/if}
+
+
+
diff --git a/src/routes/(app)/database/gw-events/new/+page.svelte b/src/routes/(app)/database/gw-events/new/+page.svelte
new file mode 100644
index 00000000..87914d96
--- /dev/null
+++ b/src/routes/(app)/database/gw-events/new/+page.svelte
@@ -0,0 +1,153 @@
+