diff --git a/.github/workflows/build-and-deploy.yaml b/.github/workflows/build-and-deploy.yaml
index 278214958..cbb6ba54c 100644
--- a/.github/workflows/build-and-deploy.yaml
+++ b/.github/workflows/build-and-deploy.yaml
@@ -18,13 +18,6 @@ jobs:
       black: false
       ruff: true
 
-  frontend-check:
-    name: "Frontend Check"
-    uses: pennlabs/shared-actions/.github/workflows/react-check.yaml@v0.1
-    with:
-      path: frontend
-      nodeVersion: 20.11.1
-
   build-backend:
     name: Build backend
     runs-on: ubuntu-latest
@@ -47,7 +40,7 @@ jobs:
           cache-to: type=local,dest=/tmp/.buildx-cache
           tags: pennlabs/penn-clubs-backend:latest,pennlabs/penn-clubs-backend:${{ github.sha }}
           outputs: type=docker,dest=/tmp/image.tar
-      - uses: actions/upload-artifact@v2
+      - uses: actions/upload-artifact@v4
         with:
           name: build-backend
           path: /tmp/image.tar
@@ -75,11 +68,10 @@ jobs:
           cache-to: type=local,dest=/tmp/.buildx-cache
           tags: pennlabs/penn-clubs-frontend:latest,pennlabs/penn-clubs-frontend:${{ github.sha }}
           outputs: type=docker,dest=/tmp/image.tar
-      - uses: actions/upload-artifact@v2
+      - uses: actions/upload-artifact@v4
         with:
           name: build-frontend
           path: /tmp/image.tar
-    needs: frontend-check
 
   publish:
     name: Publish Images
@@ -87,8 +79,8 @@ jobs:
     if: github.ref == 'refs/heads/master'
     steps:
       - uses: actions/checkout@v2
-      - uses: actions/download-artifact@v2
-      - uses: geekyeggo/delete-artifact@v1
+      - uses: actions/download-artifact@v4
+      - uses: geekyeggo/delete-artifact@v5
         with:
           name: |-
             build-backend
diff --git a/README.md b/README.md
index 542249db7..b0d1d97bf 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # Penn Clubs
 
-[![Build and Deploy](https://github.com/pennlabs/penn-clubs/workflows/Build%20and%20Deploy/badge.svg)](https://github.com/pennlabs/penn-clubs/actions)
+[![Build and Deploy Clubs](https://github.com/pennlabs/penn-clubs/workflows/Build%20and%20Deploy%20Clubs/badge.svg)](https://github.com/pennlabs/penn-clubs/actions)
 [![Coverage Status](https://codecov.io/gh/pennlabs/penn-clubs/branch/master/graph/badge.svg)](https://codecov.io/gh/pennlabs/penn-clubs)
 
 Official platform for club discovery, recruitment, and events at Penn.
@@ -8,8 +8,6 @@ React/Next.js frontend and Django-based REST API.
 
 ## Installation
 
-You will need to start both the backend and the frontend to do Penn Clubs development.
-
 You will need to start both the backend and the frontend to develop on Penn Clubs. Clubs supports Mac and Linux/WSL development.
 
 Questions? Check out our [extended guide](https://github.com/pennlabs/penn-clubs/wiki/Development-Guide) for FAQs.
@@ -95,4 +93,4 @@ To test ticketing locally, you will need to [install](https://github.com/FiloSot
 - `$ mkcert localhost 127.0.0.1 ::1`
 - `$ export DOMAIN=https://localhost:3001 NODE_TLS_REJECT_UNAUTHORIZED=0`
 
-Then, after the frontend is running, run `yarn ssl-proxy` **in a new terminal window** and access the application at [https://localhost:3001](https://localhost:3001).
\ No newline at end of file
+Then, after the frontend is running, run `yarn ssl-proxy` **in a new terminal window** and access the application at [https://localhost:3001](https://localhost:3001).
diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock
index e319b9e1c..593f1e71d 100644
--- a/backend/Pipfile.lock
+++ b/backend/Pipfile.lock
@@ -396,12 +396,12 @@
         },
         "django": {
             "hashes": [
-                "sha256:4bd01a8c830bb77a8a3b0e7d8b25b887e536ad17a81ba2dce5476135c73312bd",
-                "sha256:916423499d75d62da7aa038d19aef23d23498d8df229775eb0a6309ee1013775"
+                "sha256:848a5980e8efb76eea70872fb0e4bc5e371619c70fffbe48e3e1b50b2c09455d",
+                "sha256:d3b811bf5371a26def053d7ee42a9df1267ef7622323fe70a601936725aa4557"
             ],
             "index": "pypi",
             "markers": "python_version >= '3.10'",
-            "version": "==5.0.4"
+            "version": "==5.1"
         },
         "django-clone": {
             "hashes": [
@@ -477,20 +477,21 @@
         },
         "djangorestframework": {
             "hashes": [
-                "sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6",
-                "sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1"
+                "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20",
+                "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"
             ],
             "index": "pypi",
-            "markers": "python_version >= '3.6'",
-            "version": "==3.15.1"
+            "markers": "python_version >= '3.8'",
+            "version": "==3.15.2"
         },
         "drf-excel": {
             "hashes": [
-                "sha256:2056483e63c45c03dd36169b268400e5f411568ba1b035286ee7e87aef9c1dcb",
-                "sha256:658d0217672d7d18dac26c483f88f8de33c209f97e9cd306a93b0fada41b86d3"
+                "sha256:0103e4e3b7a923ac017169e56854f0a7be717e1bbcccd6cabaf7aabf9b5b7f28",
+                "sha256:6ae4b7c2c3552d633da88a3f93ea1c1d6237b805a50bb76555f1c5ce14521d65"
             ],
             "index": "pypi",
-            "version": "==2.4.0"
+            "markers": "python_version >= '3.7'",
+            "version": "==2.4.1"
         },
         "drf-nested-routers": {
             "hashes": [
@@ -1018,11 +1019,11 @@
         },
         "openpyxl": {
             "hashes": [
-                "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184",
-                "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5"
+                "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2",
+                "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"
             ],
-            "markers": "python_version >= '3.6'",
-            "version": "==3.1.2"
+            "markers": "python_version >= '3.8'",
+            "version": "==3.1.5"
         },
         "packaging": {
             "hashes": [
@@ -1469,11 +1470,11 @@
         },
         "sqlparse": {
             "hashes": [
-                "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93",
-                "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"
+                "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4",
+                "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"
             ],
             "markers": "python_version >= '3.8'",
-            "version": "==0.5.0"
+            "version": "==0.5.1"
         },
         "tatsu": {
             "hashes": [
diff --git a/backend/clubs/admin.py b/backend/clubs/admin.py
index f0f3b73ef..73046a164 100644
--- a/backend/clubs/admin.py
+++ b/backend/clubs/admin.py
@@ -25,6 +25,7 @@
     Cart,
     Club,
     ClubApplication,
+    ClubApprovalResponseTemplate,
     ClubFair,
     ClubFairBooth,
     ClubFairRegistration,
@@ -324,7 +325,7 @@ def club(self, obj):
 
 class AdvisorAdmin(admin.ModelAdmin):
     search_fields = ("name", "title", "email", "phone", "club__name")
-    list_display = ("name", "title", "email", "phone", "club", "public")
+    list_display = ("name", "title", "email", "phone", "club", "visibility")
 
     def club(self, obj):
         return obj.club.name
@@ -415,6 +416,10 @@ class ApplicationSubmissionAdmin(admin.ModelAdmin):
     list_display = ("user", "id", "created_at", "status")
 
 
+class ClubApprovalResponseTemplateAdmin(admin.ModelAdmin):
+    search_fields = ("title", "content")
+
+
 admin.site.register(Asset)
 admin.site.register(ApplicationCommittee)
 admin.site.register(ApplicationExtension)
@@ -460,3 +465,4 @@ class ApplicationSubmissionAdmin(admin.ModelAdmin):
 admin.site.register(TicketTransferRecord)
 admin.site.register(Cart)
 admin.site.register(ApplicationCycle)
+admin.site.register(ClubApprovalResponseTemplate, ClubApprovalResponseTemplateAdmin)
diff --git a/backend/clubs/management/commands/daily_notifications.py b/backend/clubs/management/commands/daily_notifications.py
index cf60a5e79..6192e27c9 100644
--- a/backend/clubs/management/commands/daily_notifications.py
+++ b/backend/clubs/management/commands/daily_notifications.py
@@ -96,7 +96,7 @@ def send_approval_queue_reminder(self):
         group_name = "Approvers"
 
         # only send notifications if it is currently a weekday
-        if now.isoweekday() not in range(1, 6):
+        if now.isoweekday() not in range(1, 6) or not settings.REAPPROVAL_QUEUE_OPEN:
             return False
 
         # get users in group to send notification to
diff --git a/backend/clubs/management/commands/deactivate.py b/backend/clubs/management/commands/deactivate.py
index 78527e52a..f8eb0f427 100644
--- a/backend/clubs/management/commands/deactivate.py
+++ b/backend/clubs/management/commands/deactivate.py
@@ -1,6 +1,8 @@
 import sys
 
+from django.core.cache import cache
 from django.core.management.base import BaseCommand, CommandError
+from django.db import transaction
 
 from clubs.models import Club
 
@@ -74,21 +76,28 @@ def handle(self, *args, **kwargs):
 
         # deactivate all clubs
         if deactivate_clubs:
-            clubs.update(active=False, approved=None, approved_by=None)
-
-            # allow existing approved version to stay on penn clubs website for now
-            ghosted = 0
-            for club in clubs:
-                if club.history.filter(approved=True).exists():
-                    club.ghost = True
-                    club._change_reason = (
-                        "Mark pending approval (yearly renewal process)"
-                    )
-                    club.save(update_fields=["ghost"])
-                    ghosted += 1
+            num_ghosted = 0
+
+            with transaction.atomic():
+                for club in clubs:
+                    club.active = False
+                    club.approved = None
+                    club.approved_by = None
+
+                    # allow existing approved version to stay on website for now
+                    if club.history.filter(approved=True).exists():
+                        club.ghost = True
+                        club._change_reason = (
+                            "Mark pending approval (yearly renewal process)"
+                        )
+                        num_ghosted += 1
+
+                    club.save()
+                    cache.delete(f"clubs:{club.id}-authed")  # clear cache
+                    cache.delete(f"clubs:{club.id}-anon")
 
             self.stdout.write(
-                f"{clubs.count()} clubs deactivated! {ghosted} clubs ghosted!"
+                f"{clubs.count()} clubs deactivated! {num_ghosted} clubs ghosted!"
             )
 
         # send out renewal emails to all clubs
diff --git a/backend/clubs/management/commands/graduate_users.py b/backend/clubs/management/commands/graduate_users.py
index bf61ca50d..d6e000f63 100644
--- a/backend/clubs/management/commands/graduate_users.py
+++ b/backend/clubs/management/commands/graduate_users.py
@@ -9,7 +9,6 @@ class Command(BaseCommand):
         "Mark all memberships where the student has graduated as inactive. "
         "This script should be run at the beginning of each year."
     )
-    web_execute = True
 
     def handle(self, *args, **kwargs):
         now = timezone.now()
diff --git a/backend/clubs/management/commands/osa_perms_updates.py b/backend/clubs/management/commands/osa_perms_updates.py
new file mode 100644
index 000000000..d54d3e802
--- /dev/null
+++ b/backend/clubs/management/commands/osa_perms_updates.py
@@ -0,0 +1,36 @@
+from django.conf import settings
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import Group, Permission
+from django.contrib.contenttypes.models import ContentType
+from django.core.management.base import BaseCommand
+
+from clubs.models import Club
+
+
+class Command(BaseCommand):
+    help = "Give superuser to hard-coded user accounts affiliated with OSA."
+    web_execute = True
+
+    def handle(self, *args, **kwargs):
+        User = get_user_model()
+        content_type = ContentType.objects.get_for_model(Club)
+        approve_perm = Permission.objects.get(
+            codename="approve_club", content_type=content_type
+        )
+        pending_perm = Permission.objects.get(
+            codename="see_pending_clubs", content_type=content_type
+        )
+        if not settings.OSA_KEYS:
+            raise ValueError("OSA_KEYS not set in settings")
+        if not (approvers := Group.objects.filter(name="Approvers").first()):
+            raise ValueError("Approvers group not found")
+        for key in settings.OSA_KEYS:
+            if not key or not (user := User.objects.get(username=key)):
+                continue
+            user.is_superuser = True
+            user.is_staff = True
+            user.user_permissions.add(approve_perm)
+            user.user_permissions.add(pending_perm)
+            approvers.user_set.add(user)
+            user.save()
+        approvers.save()
diff --git a/backend/clubs/management/commands/populate.py b/backend/clubs/management/commands/populate.py
index 892865ce5..310b17804 100644
--- a/backend/clubs/management/commands/populate.py
+++ b/backend/clubs/management/commands/populate.py
@@ -455,7 +455,7 @@ def get_image(url):
                 department="Accounting Department",
                 email="example@example.com",
                 phone="+12158985000",
-                defaults={"public": True},
+                defaults={"visibility": Advisor.ADVISOR_VISIBILITY_STUDENTS},
             )
 
             club.tags.add(tag_undergrad)
diff --git a/backend/clubs/management/commands/rank.py b/backend/clubs/management/commands/rank.py
index 37bc168ab..5b20afd31 100644
--- a/backend/clubs/management/commands/rank.py
+++ b/backend/clubs/management/commands/rank.py
@@ -1,8 +1,8 @@
 import datetime
-import random
 from math import floor
 
 import bleach
+import numpy as np
 from django.core.management.base import BaseCommand
 from django.utils import timezone
 
@@ -220,8 +220,11 @@ def rank(self):
             if num_testimonials >= 3:
                 ranking += 5
 
-            # rng
-            ranking += random.random() * 10
+            # random number, mostly shuffles similar clubs with average of 25 points
+            # but with long right tail to periodically feature less popular clubs
+            # given ~700 active clubs, multiplier c, expected # clubs with rand > cd
+            # is 257, 95, 35, 13, 5, 2, 1 for c = 1, 2, 3, 4, 5, 6, 7
+            ranking += np.random.standard_exponential() * 25
 
             club.rank = floor(ranking)
             club.skip_history_when_saving = True
diff --git a/backend/clubs/management/commands/send_emails.py b/backend/clubs/management/commands/send_emails.py
index 930a8e05b..c7d2f923e 100644
--- a/backend/clubs/management/commands/send_emails.py
+++ b/backend/clubs/management/commands/send_emails.py
@@ -80,6 +80,7 @@ def add_arguments(self, parser):
                 "hap_intro_remind",
                 "hap_second_round",
                 "hap_partner_communication",
+                "revised_fair_date",
                 "wc_intro",
                 "osa_email_communication",
                 "ics_calendar_ingestation",
@@ -452,6 +453,7 @@ def handle(self, *args, **kwargs):
             "admin_outreach",
             "semesterly_email",
             "update_officers",
+            "revised_fair_date",
         }:
             clubs = Club.objects.all()
             attachment = None
@@ -473,7 +475,7 @@ def handle(self, *args, **kwargs):
             for club in clubs:
                 if action == "semesterly_email":
                     context = {"code": club.code}
-                elif action == "update_officers":
+                elif action == "update_officers" or action == "revised_fair_date":
                     context = {
                         "name": club.name,
                         "url": f"https://pennclubs.com/club/{club.code}/edit/member",
diff --git a/backend/clubs/migrations/0112_clubfair_virtual.py b/backend/clubs/migrations/0112_clubfair_virtual.py
new file mode 100644
index 000000000..607904d6f
--- /dev/null
+++ b/backend/clubs/migrations/0112_clubfair_virtual.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.4 on 2024-08-21 18:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("clubs", "0111_alter_club_rank_alter_historicalclub_rank"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="clubfair",
+            name="virtual",
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/backend/clubs/migrations/0113_badge_message.py b/backend/clubs/migrations/0113_badge_message.py
new file mode 100644
index 000000000..1abd92746
--- /dev/null
+++ b/backend/clubs/migrations/0113_badge_message.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.4 on 2024-08-30 20:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("clubs", "0112_clubfair_virtual"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="badge",
+            name="message",
+            field=models.TextField(blank=True, null=True),
+        ),
+    ]
diff --git a/backend/clubs/migrations/0114_alter_advisor_public.py b/backend/clubs/migrations/0114_alter_advisor_public.py
new file mode 100644
index 000000000..3ab692de9
--- /dev/null
+++ b/backend/clubs/migrations/0114_alter_advisor_public.py
@@ -0,0 +1,35 @@
+# Generated by Django 5.0.4 on 2024-09-29 17:00
+
+from django.db import migrations, models
+
+
+def migrate_public_to_enum(apps, schema_editor):
+    Advisor = apps.get_model("clubs", "Advisor")
+    # Update 'public' field, assume public=True means only students by default
+    Advisor.objects.filter(public=False).update(visibility=1)
+    Advisor.objects.filter(public=True).update(visibility=2)
+
+def reverse_migrate_public_to_enum(apps, schema_editor):
+    Advisor = apps.get_model("clubs", "Advisor")
+    Advisor.objects.filter(visibility=1).update(public=False)
+    Advisor.objects.filter(visibility__in=[2, 3]).update(public=True)
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("clubs", "0113_badge_message"),
+    ]
+    operations = [
+        migrations.AddField(
+            model_name="advisor",
+            name="visibility",
+            field=models.IntegerField(
+                choices=[(1, "Admin Only"), (2, "Signed-in Students"), (3, "Public")],
+                default=2,
+            ),
+        ),
+        migrations.RunPython(migrate_public_to_enum, reverse_migrate_public_to_enum),
+        migrations.RemoveField(
+            model_name="advisor",
+            name="public",
+        ),
+    ]
diff --git a/backend/clubs/migrations/0115_club_beta_historicalclub_beta.py b/backend/clubs/migrations/0115_club_beta_historicalclub_beta.py
new file mode 100644
index 000000000..832c7afdf
--- /dev/null
+++ b/backend/clubs/migrations/0115_club_beta_historicalclub_beta.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.0.4 on 2024-10-05 02:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("clubs", "0114_alter_advisor_public"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="club",
+            name="beta",
+            field=models.BooleanField(default=False),
+        ),
+        migrations.AddField(
+            model_name="historicalclub",
+            name="beta",
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/backend/clubs/migrations/0116_alter_club_approved_on_and_more.py b/backend/clubs/migrations/0116_alter_club_approved_on_and_more.py
new file mode 100644
index 000000000..d332c9988
--- /dev/null
+++ b/backend/clubs/migrations/0116_alter_club_approved_on_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.0.4 on 2024-10-07 14:56
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("clubs", "0115_club_beta_historicalclub_beta"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="club",
+            name="approved_on",
+            field=models.DateTimeField(blank=True, db_index=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name="historicalclub",
+            name="approved_on",
+            field=models.DateTimeField(blank=True, db_index=True, null=True),
+        ),
+    ]
diff --git a/backend/clubs/migrations/0117_clubapprovalresponsetemplate.py b/backend/clubs/migrations/0117_clubapprovalresponsetemplate.py
new file mode 100644
index 000000000..a6ab1a7ce
--- /dev/null
+++ b/backend/clubs/migrations/0117_clubapprovalresponsetemplate.py
@@ -0,0 +1,42 @@
+# Generated by Django 5.0.4 on 2024-10-16 02:18
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("clubs", "0116_alter_club_approved_on_and_more"),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="ClubApprovalResponseTemplate",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("title", models.CharField(max_length=255, unique=True)),
+                ("content", models.TextField()),
+                ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
+                ("updated_at", models.DateTimeField(auto_now=True)),
+                (
+                    "author",
+                    models.ForeignKey(
+                        null=True,
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        related_name="templates",
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+            ],
+        ),
+    ]
diff --git a/backend/clubs/mixins.py b/backend/clubs/mixins.py
index 16fc46137..37b148e58 100644
--- a/backend/clubs/mixins.py
+++ b/backend/clubs/mixins.py
@@ -148,7 +148,7 @@ def get_xlsx_column_name(self, key):
         if hasattr(serializer_class, "get_xlsx_column_name"):
             val = serializer_class.get_xlsx_column_name(key)
         if val is None:
-            val = key.replace("_", " ")
+            val = key
         self._column_cache[key] = val
         return val
 
@@ -238,7 +238,6 @@ def _lookup_field_formatter(self, field):
                     field_object = model._meta.get_field(source_lookup[-1][:-4])
                 else:
                     raise e
-
         # format based on field type
         if isinstance(field_object, (ManyToManyField, ManyToOneRel)):
             return self._many_to_many_formatter
@@ -289,7 +288,6 @@ def finalize_response(self, request, response, *args, **kwargs):
         response = super(XLSXFormatterMixin, self).finalize_response(
             request, response, *args, **kwargs
         )
-
         # If this is a spreadsheet response, intercept and format.
         if (
             isinstance(response, Response)
@@ -324,5 +322,4 @@ def finalize_response(self, request, response, *args, **kwargs):
             response["Content-Disposition"] = "attachment; filename={}".format(
                 self.get_filename()
             )
-
         return response
diff --git a/backend/clubs/models.py b/backend/clubs/models.py
index daf7f5590..22105c7a0 100644
--- a/backend/clubs/models.py
+++ b/backend/clubs/models.py
@@ -280,7 +280,7 @@ class Club(models.Model):
         blank=True,
     )
     approved_comment = models.TextField(null=True, blank=True)
-    approved_on = models.DateTimeField(null=True, blank=True)
+    approved_on = models.DateTimeField(null=True, blank=True, db_index=True)
 
     archived = models.BooleanField(default=False)
     archived_by = models.ForeignKey(
@@ -294,6 +294,7 @@ class Club(models.Model):
 
     code = models.SlugField(max_length=255, unique=True, db_index=True)
     active = models.BooleanField(default=False)
+    beta = models.BooleanField(default=False)  # opts club into all beta features
     name = models.CharField(max_length=255)
     subtitle = models.CharField(blank=True, max_length=255)
     terms = models.CharField(blank=True, max_length=1024)
@@ -620,27 +621,23 @@ def get_officer_emails(self):
         """
         emails = []
 
-        # add club contact email if valid
-        try:
-            validate_email(self.email)
-            emails.append(self.email)
-        except ValidationError:
-            pass
-
-        # add email for all officers and above
-        for user in self.membership_set.filter(
-            role__lte=Membership.ROLE_OFFICER, active=True
-        ):
-            emails.append(user.person.email)
-
-        # remove empty emails
-        emails = [email.strip() for email in emails]
-        emails = [email for email in emails if email]
-
-        # remove duplicate emails
-        emails = list(sorted(set(emails)))
+        # Add club contact email if valid
+        if self.email:
+            try:
+                validate_email(self.email)
+                emails.append(self.email)
+            except ValidationError:
+                pass
+
+        # Add email for all active officers and above
+        emails.extend(
+            self.membership_set.filter(
+                role__lte=Membership.ROLE_OFFICER, active=True
+            ).values_list("person__email", flat=True)
+        )
 
-        return emails
+        # Remove whitespace, empty emails, and duplicates, then sort
+        return sorted(set(email.strip() for email in emails if email.strip()))
 
     def send_confirmation_email(self, request=None):
         """
@@ -680,6 +677,7 @@ def send_approval_email(self, request=None, change=False):
             "view_url": settings.VIEW_URL.format(domain=domain, club=self.code),
             "edit_url": settings.EDIT_URL.format(domain=domain, club=self.code),
             "change": change,
+            "reply_emails": settings.OSA_EMAILS + [settings.BRANDING_SITE_EMAIL],
         }
 
         emails = self.get_officer_emails()
@@ -687,10 +685,8 @@ def send_approval_email(self, request=None, change=False):
         if emails:
             send_mail_helper(
                 name="approval_status",
-                subject="{}{} {} on {}".format(
-                    "Changes to " if change else "",
+                subject="{} status update on {}".format(
                     self.name,
-                    "accepted" if self.approved else "not approved",
                     settings.BRANDING_SITE_NAME,
                 ),
                 emails=emails,
@@ -782,11 +778,7 @@ def __str__(self):
     def send_question_mail(self, request=None):
         domain = get_domain(request)
 
-        owner_emails = list(
-            self.club.membership_set.filter(
-                role__lte=Membership.ROLE_OFFICER
-            ).values_list("person__email", flat=True)
-        )
+        emails = self.club.get_officer_emails()
 
         context = {
             "name": self.club.name,
@@ -794,11 +786,11 @@ def send_question_mail(self, request=None):
             "url": settings.QUESTION_URL.format(domain=domain, club=self.club.code),
         }
 
-        if owner_emails:
+        if emails:
             send_mail_helper(
                 name="question",
                 subject="Question for {}".format(self.club.name),
-                emails=owner_emails,
+                emails=emails,
                 context=context,
             )
 
@@ -826,6 +818,7 @@ class ClubFair(models.Model):
     organization = models.TextField()
     contact = models.TextField()
     time = models.TextField(blank=True)
+    virtual = models.BooleanField(default=False)
 
     # these fields are rendered as raw html
     information = models.TextField(blank=True)
@@ -1113,20 +1106,17 @@ def send_request(self, request=None):
             "full_name": self.person.get_full_name(),
         }
 
-        owner_emails = list(
-            self.club.membership_set.filter(
-                role__lte=Membership.ROLE_OFFICER
-            ).values_list("person__email", flat=True)
-        )
+        emails = self.club.get_officer_emails()
 
-        send_mail_helper(
-            name="request",
-            subject="Membership Request from {} for {}".format(
-                self.person.get_full_name(), self.club.name
-            ),
-            emails=owner_emails,
-            context=context,
-        )
+        if emails:
+            send_mail_helper(
+                name="request",
+                subject="Membership Request from {} for {}".format(
+                    self.person.get_full_name(), self.club.name
+                ),
+                emails=emails,
+                context=context,
+            )
 
     class Meta:
         unique_together = (("person", "club"),)
@@ -1146,7 +1136,20 @@ class Advisor(models.Model):
     phone = PhoneNumberField(null=False, blank=True)
 
     club = models.ForeignKey(Club, on_delete=models.CASCADE)
-    public = models.BooleanField()
+
+    ADVISOR_VISIBILITY_ADMIN = 1
+    ADVISOR_VISIBILITY_STUDENTS = 2
+    ADVISOR_VISIBILITY_ALL = 3
+
+    ADVISOR_VISIBILITY_CHOICES = (
+        (ADVISOR_VISIBILITY_ADMIN, "Admin Only"),
+        (ADVISOR_VISIBILITY_STUDENTS, "Signed-in Students"),
+        (ADVISOR_VISIBILITY_ALL, "Public"),
+    )
+
+    visibility = models.IntegerField(
+        choices=ADVISOR_VISIBILITY_CHOICES, default=ADVISOR_VISIBILITY_STUDENTS
+    )
 
     def __str__(self):
         return self.name
@@ -1453,6 +1456,9 @@ class Badge(models.Model):
     # whether or not users can view and filter by this badge
     visible = models.BooleanField(default=False)
 
+    # optional message to display on club pages with the badge
+    message = models.TextField(null=True, blank=True)
+
     def __str__(self):
         return self.label
 
@@ -1918,8 +1924,6 @@ def send_confirmation_email(self):
         """
         Send a confirmation email to the ticket owner after purchase
         """
-        owner = self.owner
-
         output = BytesIO()
         qr_image = self.get_qr()
         qr_image.save(output, format="PNG")
@@ -1937,8 +1941,9 @@ def send_confirmation_email(self):
         if self.owner.email:
             send_mail_helper(
                 name="ticket_confirmation",
-                subject=f"Ticket confirmation for {owner.get_full_name()}",
-                emails=[owner.email],
+                subject=f"Ticket confirmation for {self.owner.get_full_name()}"
+                f" to {self.event.name}",
+                emails=[self.owner.email],
                 context=context,
                 attachment={
                     "filename": "qr_code.png",
@@ -1992,6 +1997,24 @@ def send_confirmation_email(self):
         )
 
 
+class ClubApprovalResponseTemplate(models.Model):
+    """
+    Represents a (rejection) template for site administrators to use
+    during the club approval process.
+    """
+
+    author = models.ForeignKey(
+        get_user_model(), on_delete=models.SET_NULL, null=True, related_name="templates"
+    )
+    title = models.CharField(max_length=255, unique=True)
+    content = models.TextField()
+    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
+    updated_at = models.DateTimeField(auto_now=True)
+
+    def __str__(self):
+        return self.title
+
+
 @receiver(models.signals.pre_delete, sender=Asset)
 def asset_delete_cleanup(sender, instance, **kwargs):
     if instance.file:
diff --git a/backend/clubs/permissions.py b/backend/clubs/permissions.py
index daf103333..ce634fc9a 100644
--- a/backend/clubs/permissions.py
+++ b/backend/clubs/permissions.py
@@ -116,6 +116,8 @@ class ClubPermission(permissions.BasePermission):
 
     Anyone should be able to view, if the club is approved.
     Otherwise, only members or people with permission should be able to view.
+
+    Actions default to owners/officers only unless specified below.
     """
 
     def has_object_permission(self, request, view, obj):
@@ -163,7 +165,7 @@ def has_object_permission(self, request, view, obj):
         if membership is None:
             return False
         # user has to be an owner to delete a club, an officer to edit it
-        if view.action in ["destroy"]:
+        if view.action in {"destroy"}:
             return membership.role <= Membership.ROLE_OWNER
         else:
             return membership.role <= Membership.ROLE_OFFICER
@@ -171,7 +173,9 @@ def has_object_permission(self, request, view, obj):
     def has_permission(self, request, view):
         if view.action in {
             "children",
+            "create",
             "destroy",
+            "history",
             "parents",
             "partial_update",
             "update",
@@ -179,8 +183,6 @@ def has_permission(self, request, view):
             "upload_file",
         }:
             return request.user.is_authenticated
-        elif view.action in {"create"}:
-            return request.user.is_authenticated
         else:
             return True
 
@@ -223,7 +225,12 @@ def has_object_permission(self, request, view, obj):
 
             if not old_type == FAIR_TYPE and new_type == FAIR_TYPE:
                 return False
-        elif view.action in ["buyers", "create_tickets", "issue_tickets"]:
+        elif view.action in [
+            "buyers",
+            "create_tickets",
+            "issue_tickets",
+            "email_blast",
+        ]:
             if not request.user.is_authenticated:
                 return False
             membership = find_membership_helper(request.user, obj.club)
diff --git a/backend/clubs/serializers.py b/backend/clubs/serializers.py
index 0610b37e6..5597216de 100644
--- a/backend/clubs/serializers.py
+++ b/backend/clubs/serializers.py
@@ -31,6 +31,7 @@
     Badge,
     Club,
     ClubApplication,
+    ClubApprovalResponseTemplate,
     ClubFair,
     ClubFairBooth,
     ClubVisit,
@@ -132,7 +133,7 @@ class BadgeSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = Badge
-        fields = ("id", "purpose", "label", "color", "description")
+        fields = ("id", "purpose", "label", "color", "description", "message")
 
 
 class SchoolSerializer(serializers.ModelSerializer):
@@ -351,7 +352,7 @@ class AdvisorSerializer(
 ):
     class Meta:
         model = Advisor
-        fields = ("id", "name", "title", "department", "email", "phone", "public")
+        fields = ("id", "name", "title", "department", "email", "phone", "visibility")
 
 
 class ClubEventSerializer(serializers.ModelSerializer):
@@ -1278,8 +1279,14 @@ def get_approved_by(self, obj):
         return obj.approved_by.get_full_name()
 
     def get_advisor_set(self, obj):
+        user = self.context["request"].user
+        visibility_level = (
+            Advisor.ADVISOR_VISIBILITY_STUDENTS
+            if user.is_authenticated
+            else Advisor.ADVISOR_VISIBILITY_ALL
+        )
         return AdvisorSerializer(
-            obj.advisor_set.filter(public=True).order_by("name"),
+            obj.advisor_set.filter(visibility__gte=visibility_level),
             many=True,
             read_only=True,
             context=self.context,
@@ -1292,19 +1299,21 @@ def get_is_request(self, obj):
         return obj.membershiprequest_set.filter(person=user, withdrew=False).exists()
 
     def get_target_years(self, obj):
-        qset = TargetYear.objects.filter(club=obj)
+        qset = TargetYear.objects.filter(club=obj).select_related("target_years")
         return [TargetYearSerializer(m).data for m in qset]
 
     def get_target_majors(self, obj):
-        qset = TargetMajor.objects.filter(club=obj)
+        qset = TargetMajor.objects.filter(club=obj).select_related("target_majors")
         return [TargetMajorSerializer(m).data for m in qset]
 
     def get_target_schools(self, obj):
-        qset = TargetSchool.objects.filter(club=obj)
+        qset = TargetSchool.objects.filter(club=obj).select_related("target_schools")
         return [TargetSchoolSerializer(m).data for m in qset]
 
     def get_target_student_types(self, obj):
-        qset = TargetStudentType.objects.filter(club=obj)
+        qset = TargetStudentType.objects.filter(club=obj).select_related(
+            "target_student_types"
+        )
         return [TargetStudentTypeSerializer(m).data for m in qset]
 
     def create(self, validated_data):
@@ -1314,6 +1323,16 @@ def create(self, validated_data):
         # New clubs created through the API must always be approved.
         validated_data["approved"] = None
 
+        request = self.context.get("request", None)
+        perms = request and request.user.has_perm("clubs.approve_club")
+
+        if not perms and (
+            not settings.REAPPROVAL_QUEUE_OPEN or not settings.NEW_APPROVAL_QUEUE_OPEN
+        ):
+            raise serializers.ValidationError(
+                "The approval queue is not currently open."
+            )
+
         obj = super().create(validated_data)
 
         # assign user who created as owner
@@ -1513,6 +1532,11 @@ def save(self):
         """
         Override save in order to replace code with slugified name if not specified.
         """
+        if "beta" in self.validated_data:
+            raise serializers.ValidationError(
+                "The beta field is not allowed to be set by clubs."
+            )
+
         # remove any spaces from the name
         if "name" in self.validated_data:
             self.validated_data["name"] = self.validated_data["name"].strip()
@@ -1529,7 +1553,7 @@ def save(self):
         # if key fields were edited, require re-approval
         needs_reapproval = False
         if self.instance:
-            for field in {"name", "image", "description"}:
+            for field in {"name", "image"}:
                 if field in self.validated_data and not self.validated_data[
                     field
                 ] == getattr(self.instance, field, None):
@@ -1541,6 +1565,11 @@ def save(self):
         if request and request.user.has_perm("clubs.approve_club"):
             needs_reapproval = False
 
+        if needs_reapproval and not settings.REAPPROVAL_QUEUE_OPEN:
+            raise serializers.ValidationError(
+                "The approval queue is not currently open."
+            )
+
         has_approved_version = (
             self.instance and self.instance.history.filter(approved=True).exists()
         )
@@ -1710,6 +1739,7 @@ class Meta(ClubListSerializer.Meta):
             "approved_by",
             "approved_comment",
             "badges",
+            "beta",
             "created_at",
             "description",
             "events",
@@ -2324,8 +2354,8 @@ def get_attribute(self, instance):
 
     def to_representation(self, value):
         """
-        This is called to get the value for a partcular cell, given the club code.
-        The entire field object can be though of as a column in the spreadsheet.
+        This is called to get the value for a particular cell, given the club code.
+        The entire field object can be thought of as a column in the spreadsheet.
         """
         return self._cached_values.get(value, self._default_value)
 
@@ -2970,11 +3000,34 @@ class Meta:
             "registration_start_time",
             "start_time",
             "time",
+            "virtual",
         )
 
 
-class AdminNoteSerializer(serializers.ModelSerializer):
-    club = serializers.SlugRelatedField(queryset=Club.objects.all(), slug_field="code")
+class ApprovalHistorySerializer(serializers.ModelSerializer):
+    approved = serializers.BooleanField()
+    approved_on = serializers.DateTimeField()
+    approved_by = serializers.SerializerMethodField("get_approved_by")
+    approved_comment = serializers.CharField()
+    history_date = serializers.DateTimeField()
+
+    def get_approved_by(self, obj):
+        if obj.approved_by is None:
+            return "Unknown"
+        return obj.approved_by.get_full_name()
+
+    class Meta:
+        model = Club
+        fields = (
+            "approved",
+            "approved_on",
+            "approved_by",
+            "approved_comment",
+            "history_date",
+        )
+
+
+class AdminNoteSerializer(ClubRouteMixin, serializers.ModelSerializer):
     creator = serializers.SerializerMethodField("get_creator")
     title = serializers.CharField(max_length=255, default="Note")
     content = serializers.CharField(required=False)
@@ -2983,16 +3036,16 @@ def get_creator(self, obj):
         return obj.creator.get_full_name()
 
     def create(self, validated_data):
-        return AdminNote.objects.create(
-            creator=self.context["request"].user,
-            club=validated_data["club"],
-            title=validated_data["title"],
-            content=validated_data["content"],
-        )
+        validated_data["creator"] = self.context["request"].user
+        return super().create(validated_data)
+
+    def update(self, instance, validated_data):
+        validated_data.pop("creator", "")
+        return super().update(instance, validated_data)
 
     class Meta:
         model = AdminNote
-        fields = ("id", "creator", "club", "title", "content", "created_at")
+        fields = ("id", "creator", "title", "content", "created_at")
 
 
 class WritableClubFairSerializer(ClubFairSerializer):
@@ -3000,3 +3053,22 @@ class WritableClubFairSerializer(ClubFairSerializer):
 
     class Meta(ClubFairSerializer.Meta):
         pass
+
+
+class ClubApprovalResponseTemplateSerializer(serializers.ModelSerializer):
+    author = serializers.SerializerMethodField("get_author")
+
+    def get_author(self, obj):
+        return obj.author.get_full_name()
+
+    def create(self, validated_data):
+        validated_data["author"] = self.context["request"].user
+        return super().create(validated_data)
+
+    def update(self, instance, validated_data):
+        validated_data.pop("author", "")
+        return super().update(instance, validated_data)
+
+    class Meta:
+        model = ClubApprovalResponseTemplate
+        fields = ("id", "author", "title", "content", "created_at", "updated_at")
diff --git a/backend/clubs/urls.py b/backend/clubs/urls.py
index 4395aa838..48f2f0572 100644
--- a/backend/clubs/urls.py
+++ b/backend/clubs/urls.py
@@ -12,6 +12,7 @@
     BadgeClubViewSet,
     BadgeViewSet,
     ClubApplicationViewSet,
+    ClubApprovalResponseTemplateViewSet,
     ClubBoothsViewSet,
     ClubEventViewSet,
     ClubFairViewSet,
@@ -23,6 +24,7 @@
     FavoriteCalendarAPIView,
     FavoriteEventsAPIView,
     FavoriteViewSet,
+    HealthView,
     MajorViewSet,
     MassInviteAPIView,
     MeetingZoomAPIView,
@@ -92,6 +94,7 @@
     basename="wharton",
 )
 router.register(r"submissions", ApplicationSubmissionUserViewSet, basename="submission")
+router.register(r"templates", ClubApprovalResponseTemplateViewSet, basename="templates")
 
 clubs_router = routers.NestedSimpleRouter(router, r"clubs", lookup="club")
 clubs_router.register(r"members", MemberViewSet, basename="club-members")
@@ -174,6 +177,7 @@
         WhartonApplicationStatusAPIView.as_view(),
         name="wharton-applications-status",
     ),
+    path(r"health/", HealthView.as_view(), name="health"),
 ]
 
 urlpatterns += router.urls
diff --git a/backend/clubs/views.py b/backend/clubs/views.py
index 84506f528..237ac1aea 100644
--- a/backend/clubs/views.py
+++ b/backend/clubs/views.py
@@ -97,6 +97,7 @@
     Cart,
     Club,
     ClubApplication,
+    ClubApprovalResponseTemplate,
     ClubFair,
     ClubFairBooth,
     ClubFairRegistration,
@@ -155,11 +156,13 @@
     ApplicationSubmissionCSVSerializer,
     ApplicationSubmissionSerializer,
     ApplicationSubmissionUserSerializer,
+    ApprovalHistorySerializer,
     AssetSerializer,
     AuthenticatedClubSerializer,
     AuthenticatedMembershipSerializer,
     BadgeSerializer,
     ClubApplicationSerializer,
+    ClubApprovalResponseTemplateSerializer,
     ClubBoothSerializer,
     ClubConstitutionSerializer,
     ClubDiffSerializer,
@@ -1154,6 +1157,18 @@ def get_queryset(self):
         else:
             return queryset.filter(Q(approved=True) | Q(ghost=True))
 
+    def _has_elevated_view_perms(self, instance):
+        """
+        Determine if the current user has elevated view privileges.
+        """
+        see_pending = self.request.user.has_perm("clubs.see_pending_clubs")
+        manage_club = self.request.user.has_perm("clubs.manage_club")
+        is_member = (
+            self.request.user.is_authenticated
+            and instance.membership_set.filter(person=self.request.user).exists()
+        )
+        return see_pending or manage_club or is_member
+
     @action(detail=True, methods=["post"])
     def upload(self, request, *args, **kwargs):
         """
@@ -1195,8 +1210,8 @@ def upload(self, request, *args, **kwargs):
         """
         # ensure user is allowed to upload image
         club = self.get_object()
-        key = f"clubs:{club.id}"
-        cache.delete(key)
+        cache.delete(f"clubs:{club.id}-authed")
+        cache.delete(f"clubs:{club.id}-anon")
 
         # reset approval status after upload
         resp = upload_endpoint_helper(request, club, "file", "image", save=False)
@@ -1265,6 +1280,49 @@ def upload_file(self, request, *args, **kwargs):
 
         return file_upload_endpoint_helper(request, code=club.code)
 
+    @action(detail=True, methods=["get"])
+    def history(self, request, *args, **kwargs):
+        """
+        Return a simplified approval history for the club.
+        ---
+        responses:
+            "200":
+                content:
+                    application/json:
+                        schema:
+                            type: array
+                            items:
+                                type: object
+                                properties:
+                                    approved:
+                                        type: boolean
+                                    approved_on:
+                                        type: string
+                                        format: date-time
+                                    approved_by:
+                                        type: string
+                                        description: >
+                                            The full name of the user who approved
+                                            the club.
+                                    approved_comment:
+                                        type: string
+                                    history_date:
+                                        type: string
+                                        format: date-time
+                                        description: >
+                                            The time in which the specific version
+                                            of the club was saved at.
+        ---
+        """
+        club = self.get_object()
+        return Response(
+            ApprovalHistorySerializer(
+                club.history.order_by("-history_date"),
+                many=True,
+                context={"request": request},
+            ).data
+        )
+
     @action(detail=True, methods=["get"])
     def owned_badges(self, request, *args, **kwargs):
         """
@@ -1360,6 +1418,8 @@ def alumni(self, request, *args, **kwargs):
                                             type: string
         ---
         """
+        if not request.user.is_authenticated:
+            raise PermissionDenied
         club = self.get_object()
         results = collections.defaultdict(list)
         for first, last, year, show, username in club.membership_set.filter(
@@ -2005,9 +2065,18 @@ def list(self, *args, **kwargs):
 
     def retrieve(self, *args, **kwargs):
         """
-        Retrieve data about a specific club. Responses cached for 1 hour
+        Retrieve data about a specific club. Responses cached for 1 hour. Caching is
+        disabled for users with elevated view perms so that changes without approval
+        granted don't spill over to public.
         """
-        key = f"clubs:{self.get_object().id}"
+        club = self.get_object()
+
+        # don't cache if user has elevated view perms
+        if self._has_elevated_view_perms(club):
+            return super().retrieve(*args, **kwargs)
+
+        key = f"""clubs:{club.id}-{"authed" if self.request.user.is_authenticated
+                                            else "anon"}"""
         cached = cache.get(key)
         if cached:
             return Response(cached)
@@ -2021,8 +2090,8 @@ def update(self, request, *args, **kwargs):
         Invalidate caches
         """
         self.check_approval_permission(request)
-        key = f"clubs:{self.get_object().id}"
-        cache.delete(key)
+        cache.delete(f"clubs:{self.get_object().id}-authed")
+        cache.delete(f"clubs:{self.get_object().id}-anon")
         return super().update(request, *args, **kwargs)
 
     def partial_update(self, request, *args, **kwargs):
@@ -2030,8 +2099,8 @@ def partial_update(self, request, *args, **kwargs):
         Invalidate caches
         """
         self.check_approval_permission(request)
-        key = f"clubs:{self.get_object().id}"
-        cache.delete(key)
+        cache.delete(f"clubs:{self.get_object().id}-authed")
+        cache.delete(f"clubs:{self.get_object().id}-anon")
         return super().partial_update(request, *args, **kwargs)
 
     def perform_destroy(self, instance):
@@ -2048,17 +2117,17 @@ def perform_destroy(self, instance):
         # Send notice to club officers and executor
         context = {
             "name": club.name,
-            "branding_site_name": settings.BRANDING_SITE_NAME,
-            "branding_site_email": settings.BRANDING_SITE_EMAIL,
+            "reply_emails": settings.OSA_EMAILS + [settings.BRANDING_SITE_EMAIL],
         }
         emails = club.get_officer_emails() + [self.request.user.email]
         send_mail_helper(
             name="club_deletion",
-            subject="Removal of {} from {}".format(
+            subject="{} status update on {}".format(
                 club.name, settings.BRANDING_SITE_NAME
             ),
             emails=emails,
             context=context,
+            reply_to=settings.OSA_EMAILS + [settings.BRANDING_SITE_EMAIL],
         )
 
     def _get_club_diff_queryset(self):
@@ -2299,6 +2368,8 @@ def get_serializer_class(self):
             return ClubConstitutionSerializer
         if self.action == "notes_about":
             return NoteSerializer
+        if self.action == "history":
+            return ApprovalHistorySerializer
         if self.action in {"list", "fields"}:
             if self.request is not None and (
                 self.request.accepted_renderer.format == "xlsx"
@@ -2424,11 +2495,14 @@ class AdvisorSearchFilter(filters.BaseFilterBackend):
 
     def filter_queryset(self, request, queryset, view):
         public = request.GET.get("public")
-
         if public is not None:
             public = public.strip().lower()
             if public in {"true", "false"}:
-                queryset = queryset.filter(public=public == "true")
+                queryset = queryset.filter(
+                    visibility__gte=Advisor.ADVISOR_VISIBILITY_STUDENTS
+                    if public == "true"
+                    else Advisor.ADVISOR_VISIBILITY_ADMIN
+                )
 
         return queryset
 
@@ -2436,7 +2510,7 @@ def filter_queryset(self, request, queryset, view):
 class AdvisorViewSet(viewsets.ModelViewSet):
     """
     list:
-    Return a list of advisors for this club.
+    Return a list of advisors for this club for club administrators.
 
     create:
     Add an advisor to this club.
@@ -2455,7 +2529,7 @@ class AdvisorViewSet(viewsets.ModelViewSet):
     """
 
     serializer_class = AdvisorSerializer
-    permission_classes = [ClubItemPermission | IsSuperuser]
+    permission_classes = [ClubSensitiveItemPermission | IsSuperuser]
     filter_backends = [AdvisorSearchFilter]
     lookup_field = "id"
     http_method_names = ["get", "post", "put", "patch", "delete"]
@@ -2557,10 +2631,17 @@ def add_to_cart(self, request, *args, **kwargs):
         event = self.get_object()
         cart, _ = Cart.objects.get_or_create(owner=self.request.user)
 
+        # Check if the event has already ended
+        if event.end_time < timezone.now():
+            return Response(
+                {"detail": "This event has already ended", "success": False},
+                status=status.HTTP_403_FORBIDDEN,
+            )
+
         # Cannot add tickets that haven't dropped yet
         if event.ticket_drop_time and timezone.now() < event.ticket_drop_time:
             return Response(
-                {"detail": "Ticket drop time has not yet elapsed"},
+                {"detail": "Ticket drop time has not yet elapsed", "success": False},
                 status=status.HTTP_403_FORBIDDEN,
             )
 
@@ -2604,7 +2685,10 @@ def add_to_cart(self, request, *args, **kwargs):
 
             if tickets.count() < count:
                 return Response(
-                    {"detail": f"Not enough tickets of type {type} left!"},
+                    {
+                        "detail": f"Not enough tickets of type {type} left!",
+                        "success": False,
+                    },
                     status=status.HTTP_403_FORBIDDEN,
                 )
             cart.tickets.add(*tickets[:count])
@@ -2963,7 +3047,8 @@ def create_tickets(self, request, *args, **kwargs):
             event.ticket_drop_time = drop_time
             event.save()
 
-        cache.delete(f"clubs:{event.club.id}")
+        cache.delete(f"clubs:{event.club.id}-authed")
+        cache.delete(f"clubs:{event.club.id}-anon")
         return Response({"detail": "Successfully created tickets"})
 
     @action(detail=True, methods=["post"])
@@ -3119,6 +3204,93 @@ def issue_tickets(self, request, *args, **kwargs):
             {"success": True, "detail": f"Issued {len(tickets)} tickets", "errors": []}
         )
 
+    @action(detail=True, methods=["post"])
+    def email_blast(self, request, *args, **kwargs):
+        """
+        Send an email blast to all users holding tickets.
+        ---
+        requestBody:
+            content:
+                application/json:
+                    schema:
+                        type: object
+                        properties:
+                            content:
+                                type: string
+                                description: The content of the email blast to send
+                        required:
+                            - content
+        responses:
+            "200":
+                description: Email blast was sent successfully
+                content:
+                    application/json:
+                        schema:
+                            type: object
+                            properties:
+                                detail:
+                                    type: string
+                                    description: A message indicating how many
+                                        recipients received the blast
+            "400":
+                description: Content field was empty or missing
+                content:
+                    application/json:
+                        schema:
+                            type: object
+                            properties:
+                                detail:
+                                    type: string
+                                    description: Error message indicating content
+                                        was not provided
+            "404":
+                description: Event not found
+                content:
+                    application/json:
+                        schema:
+                            type: object
+                            properties:
+                                detail:
+                                    type: string
+                                    description: Error message indicating event was
+                                        not found
+        ---
+        """
+        event = self.get_object()
+
+        holder_emails = Ticket.objects.filter(
+            event=event, owner__isnull=False
+        ).values_list("owner__email", flat=True)
+        officer_emails = event.club.get_officer_emails()
+        emails = list(holder_emails) + list(officer_emails)
+
+        content = request.data.get("content", "").strip()
+        if not content:
+            return Response(
+                {"detail": "Content must be specified"},
+                status=status.HTTP_400_BAD_REQUEST,
+            )
+
+        send_mail_helper(
+            name="blast",
+            subject=f"Update on {event.name} from {event.club.name}",
+            emails=emails,
+            context={
+                "sender": event.club.name,
+                "content": request.data.get("content"),
+                "reply_emails": event.club.get_officer_emails(),
+            },
+        )
+
+        return Response(
+            {
+                "detail": (
+                    f"Blast sent to {len(holder_emails)} ticket holders "
+                    f"and {len(officer_emails)} officers"
+                )
+            }
+        )
+
     @action(detail=True, methods=["post"])
     def upload(self, request, *args, **kwargs):
         """
@@ -3567,7 +3739,8 @@ def get_queryset(self):
         )
 
         if not self.request.user.is_authenticated:
-            return questions.filter(approved=True)
+            # Hide responder if not authenticated
+            return questions.filter(approved=True).extra(select={"responder": "NULL"})
 
         membership = Membership.objects.filter(
             club__code=club_code, person=self.request.user
@@ -5278,8 +5451,12 @@ def cart(self, request, *args, **kwargs):
             owner=self.request.user
         )
 
+        now = timezone.now()
+
         tickets_to_replace = cart.tickets.filter(
-            Q(owner__isnull=False) | Q(holder__isnull=False)
+            Q(owner__isnull=False)
+            | Q(holder__isnull=False)
+            | Q(event__end_time__lt=now)
         ).exclude(holder=self.request.user)
 
         # In most cases, we won't need to replace, so exit early
@@ -5291,16 +5468,30 @@ def cart(self, request, *args, **kwargs):
                 },
             )
 
-        # Attempt to replace all tickets that have gone stale
+        # Attempt to replace all tickets that have gone stale or are for elapsed events
         replacement_tickets, sold_out_tickets = [], []
 
         tickets_in_cart = cart.tickets.values_list("id", flat=True)
         tickets_to_replace = tickets_to_replace.select_related("event")
 
         for ticket_class in tickets_to_replace.values(
-            "type", "event", "event__name"
+            "type", "event", "event__name", "event__end_time"
         ).annotate(count=Count("id")):
             # we don't need to lock, since we aren't updating holder/owner
+            if ticket_class["event__end_time"] < now:
+                # Event has elapsed, mark all tickets as sold out
+                sold_out_tickets.append(
+                    {
+                        "type": ticket_class["type"],
+                        "event": {
+                            "id": ticket_class["event"],
+                            "name": ticket_class["event__name"],
+                        },
+                        "count": ticket_class["count"],
+                    }
+                )
+                continue
+
             available_tickets = Ticket.objects.filter(
                 event=ticket_class["event"],
                 type=ticket_class["type"],
@@ -5420,7 +5611,6 @@ def initiate_checkout(self, request, *args, **kwargs):
             order_info = {
                 "amountDetails": {"totalAmount": "0.00"},
                 "billTo": {
-                    "reconciliationId": None,
                     "firstName": self.request.user.first_name,
                     "lastName": self.request.user.last_name,
                     "phoneNumber": None,
@@ -5429,7 +5619,7 @@ def initiate_checkout(self, request, *args, **kwargs):
             }
 
             # Place hold on tickets for 10 mins
-            self._place_hold_on_tickets(tickets)
+            self._place_hold_on_tickets(self.request.user, tickets)
             # Skip payment process and give tickets to user/buyer
             self._give_tickets(self.request.user, order_info, cart, None)
 
@@ -5485,7 +5675,7 @@ def initiate_checkout(self, request, *args, **kwargs):
                 cart.save()
 
             # Place hold on tickets for 10 mins
-            self._place_hold_on_tickets(tickets)
+            self._place_hold_on_tickets(self.request.user, tickets)
 
             return Response(
                 {
@@ -5738,7 +5928,8 @@ def get_queryset(self):
             ).select_related("event__club")
         return Ticket.objects.filter(owner=self.request.user.id)
 
-    def _give_tickets(self, user, order_info, cart, reconciliation_id):
+    @staticmethod
+    def _give_tickets(user, order_info, cart, reconciliation_id):
         """
         Helper function that gives user/buyer their held tickets
         and archives the transaction data
@@ -5759,25 +5950,26 @@ def _give_tickets(self, user, order_info, cart, reconciliation_id):
             buyer_phone=order_info["billTo"].get("phoneNumber", None),
             buyer_email=order_info["billTo"]["email"],
         )
+        tickets.update(owner=user, holder=None, transaction_record=transaction_record)
 
-        tickets.update(
-            owner=self.request.user, holder=None, transaction_record=transaction_record
-        )
         cart.tickets.clear()
-        for ticket in tickets:
-            ticket.send_confirmation_email()
-
         Ticket.objects.update_holds()
-
         cart.checkout_context = None
         cart.save()
 
-    def _place_hold_on_tickets(self, tickets):
+        tickets = Ticket.objects.filter(
+            owner=user, transaction_record=transaction_record
+        )
+        for ticket in tickets:
+            ticket.send_confirmation_email()
+
+    @staticmethod
+    def _place_hold_on_tickets(user, tickets):
         """
         Helper function that places a 10 minute hold on tickets for a user
         """
         holding_expiration = timezone.now() + datetime.timedelta(minutes=10)
-        tickets.update(holder=self.request.user, holding_expiration=holding_expiration)
+        tickets.update(holder=user, holding_expiration=holding_expiration)
 
 
 class MemberInviteViewSet(viewsets.ModelViewSet):
@@ -6668,6 +6860,52 @@ def remove_clubs_from_exception(self, *args, **kwargs):
         )
         return Response([])
 
+    @action(detail=True, methods=["GET"])
+    def club_applications(self, *args, **kwargs):
+        """
+        Retrieve club applications for given cycle
+        ---
+        requestBody:
+            content: {}
+        responses:
+            "200":
+                content:
+                    application/json:
+                        schema:
+                            type: array
+                            items:
+                                type: object
+                                properties:
+                                    name:
+                                        type: string
+                                    id:
+                                        type: integer
+                                    application_end_time:
+                                        type: string
+                                        format: date-time
+                                    application_end_time_exception:
+                                        type: string
+                                    club__name:
+                                        type: string
+                                    club__code:
+                                        type: string
+        ---
+        """
+        cycle = self.get_object()
+
+        return Response(
+            ClubApplication.objects.filter(application_cycle=cycle)
+            .select_related("club")
+            .values(
+                "name",
+                "id",
+                "application_end_time",
+                "application_end_time_exception",
+                "club__name",
+                "club__code",
+            )
+        )
+
     @action(detail=True, methods=["GET"])
     def applications(self, *args, **kwargs):
         """
@@ -6676,7 +6914,10 @@ def applications(self, *args, **kwargs):
         requestBody: {}
         responses:
             "200":
-                content: {}
+                content:
+                    text/csv:
+                        schema:
+                            type: string
         ---
         """
         cycle = self.get_object()
@@ -7383,8 +7624,10 @@ def get(self, request):
             happening = fair.start_time <= now - datetime.timedelta(minutes=3)
             close = fair.start_time >= now - datetime.timedelta(weeks=1)
             options["FAIR_NAME"] = fair.name
+            options["FAIR_CONTACT"] = fair.contact
             options["FAIR_ID"] = fair.id
             options["FAIR_OPEN"] = happening
+            options["FAIR_VIRTUAL"] = fair.virtual
             options["PRE_FAIR"] = not happening and close
         else:
             options["FAIR_OPEN"] = False
@@ -7516,7 +7759,18 @@ class AdminNoteViewSet(viewsets.ModelViewSet):
     http_method_names = ["get", "post", "put", "patch", "delete"]
 
     def get_queryset(self):
-        return AdminNote.objects.filter(club__code=self.kwargs.get("club_code"))
+        return AdminNote.objects.filter(
+            club__code=self.kwargs.get("club_code")
+        ).order_by("-created_at")
+
+
+class ClubApprovalResponseTemplateViewSet(viewsets.ModelViewSet):
+    serializer_class = ClubApprovalResponseTemplateSerializer
+    permission_classes = [IsSuperuser]
+    lookup_field = "id"
+
+    def get_queryset(self):
+        return ClubApprovalResponseTemplate.objects.all().order_by("-created_at")
 
 
 class ScriptExecutionView(APIView):
@@ -7609,6 +7863,27 @@ def post(self, request):
             return Response({"output": output.getvalue()})
 
 
+class HealthView(APIView):
+    def get(self, request):
+        """
+        Health check endpoint to confirm the backend is running.
+        ---
+        summary: Health Check
+        responses:
+            "200":
+                content:
+                    application/json:
+                        schema:
+                            type: object
+                            properties:
+                                message:
+                                    type: string
+                                    enum: ["OK"]
+        ---
+        """
+        return Response({"message": "OK"}, status=status.HTTP_200_OK)
+
+
 def get_initial_context_from_types(types):
     """
     Generate a sample context given the specified types.
diff --git a/backend/pennclubs/settings/base.py b/backend/pennclubs/settings/base.py
index 26bc09ede..d74fa029a 100644
--- a/backend/pennclubs/settings/base.py
+++ b/backend/pennclubs/settings/base.py
@@ -204,6 +204,12 @@
 
 OSA_EMAILS = ["vpul-orgs@pobox.upenn.edu"]
 
+
+# Controls whether existing clubs can submit for reapproval
+REAPPROVAL_QUEUE_OPEN = True
+# Controls whether new clubs can submit for initial approval
+NEW_APPROVAL_QUEUE_OPEN = True
+
 # File upload settings
 
 MEDIA_URL = "/api/media/"
@@ -259,3 +265,5 @@
 
 # Cybersource settings
 CYBERSOURCE_CLIENT_VERSION = "0.15"
+
+OSA_KEYS = None
diff --git a/backend/pennclubs/settings/ci.py b/backend/pennclubs/settings/ci.py
index 0f2e707ed..f6b5fa5b5 100644
--- a/backend/pennclubs/settings/ci.py
+++ b/backend/pennclubs/settings/ci.py
@@ -11,7 +11,7 @@
 TEST_OUTPUT_DIR = "test-results"
 
 # Use dummy cache for testing
-CACHES = {"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}}
+CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}}
 
 del PLATFORM_ACCOUNTS["REDIRECT_URI"]
 
diff --git a/backend/pennclubs/settings/development.py b/backend/pennclubs/settings/development.py
index 90c2d1504..720e07a23 100644
--- a/backend/pennclubs/settings/development.py
+++ b/backend/pennclubs/settings/development.py
@@ -42,3 +42,5 @@
     "run_environment": "apitest.cybersource.com",
 }
 CYBERSOURCE_TARGET_ORIGIN = "https://localhost:3001"
+
+OSA_KEYS = ["gwashington"]
diff --git a/backend/pennclubs/settings/production.py b/backend/pennclubs/settings/production.py
index f8d20a44b..ed445fe2b 100644
--- a/backend/pennclubs/settings/production.py
+++ b/backend/pennclubs/settings/production.py
@@ -26,7 +26,7 @@
         integrations=[DjangoIntegration(cache_spans=True)],
         send_default_pii=False,
         enable_tracing=True,
-        traces_sample_rate=0.1,
+        traces_sample_rate=0.01,
         profiles_sample_rate=1.0,
     )
 
@@ -89,3 +89,5 @@
     "run_environment": "api.cybersource.com",
 }
 CYBERSOURCE_TARGET_ORIGIN = "https://pennclubs.com"
+
+OSA_KEYS = os.getenv("OSA_KEYS", "").split(",")
diff --git a/backend/templates/emails/approval_status.html b/backend/templates/emails/approval_status.html
index 8671f5227..9d0e78eec 100644
--- a/backend/templates/emails/approval_status.html
+++ b/backend/templates/emails/approval_status.html
@@ -13,43 +13,49 @@
     type: string
 year:
     type: number
+reply_emails:
+    type: array
+    items:
+        type: string
 -->
 {% extends 'emails/base.html' %}
 
 {% block content %}
-    <h2><b>{{ name }}</b> {% if change %}changes{% else %}renewal{% endif %} marked as {% if approved %}approved{% else %}not approved{% endif %} on Penn Clubs</h2>
+    <h2><b>{{ name }}</b> status update on Penn Clubs</h2>
     {% if approved %}
-    <p style="font-size: 1.2em; color: #228B22">
-        <b>🎉 Congratulations!</b> Your club {% if change %}changes have{% else %}has{% endif %} been approved by the <b>Office of Student Affairs</b> for the {{ year }} school year! 
-        Your {% if change %}updated{% else %}new{% endif %} club information is now open to the public on Penn Clubs.
+    <p style="font-size: 1.2em;">
+        Your club {% if change %}updates have{% else %}has{% endif %} <b>been approved</b> by the <b>Office of Student Affairs</b>. 
+        Your {% if change %}updated{% else %}new{% endif %} club is now visible to the public on Penn Clubs.
     </p>
     {% if approved_comment %}
         <p style="font-size: 1.2em; padding: 8px; border-left: 5px solid #ccc; white-space: pre-wrap;">{{ approved_comment }}</p>
     {% endif %}
     <p style="font-size: 1.2em">
-        You do not need to perform any further actions.
+        No further action is required at this time.
     </p>
     <p style="font-size: 1.2em">
-        You can view your club <a href="{{ view_url }}">here</a> and make further edits <a href="{{ view_url }}">here</a>.
-        If you have any feedback about the club renewal process, you can respond to this email or fill out <a href="https://airtable.com/shrCsYFWxCwfwE7cf">this form</a>.
+        You can view your club <a href="{{ view_url }}">here</a> and make further edits <a href="{{ edit_url }}">here</a>.
+        If you have any feedback about the club renewal process, please respond to this email or <a href="mailto:{% for email in reply_emails %}{% if not forloop.first %},{% endif %}{{ email }}{% endfor %}">contact the Office of Student Affairs</a>.
     </p>
     <p style="font-size: 1.2em">
-        Thank you for using Penn Clubs!
+        Thank you for using Penn Clubs.
     </p>
     {% else %}
-    <p style="font-size: 1.2em; color: #8B2222">
-        <b>Oh no!</b> Your club {% if change %}updates have{% else %}renewal has{% endif %} <b>not been approved</b> by the <b>Office of Student Affairs</b>. You will need to make changes to your club information before gaining approval.
+    <p style="font-size: 1.2em;">
+        Your club {% if change %}updates have{% else %}has{% endif %} <b>not been approved</b> by the <b>Office of Student Affairs</b>. Changes to your club's information are required before approval can be granted.
     </p>
-    <p style="font-size: 1.2em">The reason your club was not approved is shown below:</p>
+    <p style="font-size: 1.2em">The feedback regarding your club's status is as follows:</p>
     <p style="font-size: 1.2em; padding: 8px; border-left: 5px solid #ccc;">{{ approved_comment }}</p>
-    <p style="font-size: 1.2em">You will need to <a href="{{ edit_url }}">make edits</a> to your club and press the button found <a href="{{ view_url }}">here</a> to apply again for approval.</p>
-    <p style="font-size: 1.2em">If you run into any issues with the approval process, you can respond to this email.</p>
+    <p style="font-size: 1.2em">Please <a href="{{ edit_url }}">make the necessary edits</a> to your club information and use the button <a href="{{ view_url }}">here</a> to resubmit for approval.</p>
     <p style="font-size: 1.2em">
-    <a
-       style="text-decoration: none; padding: 5px 20px; font-size: 1.5em; margin-top: 20px; color: white; background-color: green; border-radius: 3px; font-weight: bold"
-       href="{{ view_url }}">
-       Review {{ name }}
-    </a>
+        If you have any questions about the approval process, please feel free to respond to this email or <a href="mailto:{% for email in reply_emails %}{% if not forloop.first %},{% endif %}{{ email }}{% endfor %}">contact the Office of Student Affairs</a>.
+    </p>
+    <p style="font-size: 1.2em">
+        <a
+            style="text-decoration: none; padding: 5px 20px; font-size: 1.5em; margin-top: 20px; color: white; background-color: green; border-radius: 3px; font-weight: bold"
+            href="{{ view_url }}">
+            Review {{ name }}
+        </a>
     </p>
     {% endif %}
 {% endblock %}
diff --git a/backend/templates/emails/blast.html b/backend/templates/emails/blast.html
new file mode 100644
index 000000000..09545581e
--- /dev/null
+++ b/backend/templates/emails/blast.html
@@ -0,0 +1,25 @@
+<!-- TYPES:
+sender:
+    type: string
+content:
+    type: string
+reply_emails:
+    type: array
+    items:
+        type: string
+-->
+{% extends 'emails/base.html' %}
+
+{% block content %}
+    <h2>Message from {{ sender }}</h2>
+    
+    <p style="font-size: 1.2em">
+        The following message was sent by <b>{{ sender }}</b>:
+    </p>
+
+    <p style="font-size: 1.2em; padding: 8px; border-left: 5px solid #ccc; white-space: pre-wrap;">{{ content }}</p>
+
+    <p style="font-size: 1.2em">
+        If you have any questions, please respond to this email or <a href="mailto:{% for email in reply_emails %}{% if not forloop.first %},{% endif %}{{ email }}{% endfor %}">contact the sender</a>.
+    </p>
+{% endblock %}
diff --git a/backend/templates/emails/club_deletion.html b/backend/templates/emails/club_deletion.html
index 3d15ab29d..ccaa30e85 100644
--- a/backend/templates/emails/club_deletion.html
+++ b/backend/templates/emails/club_deletion.html
@@ -1,17 +1,20 @@
 <!-- TYPES:
 name:
     type: string
-branding_site_name: 
-    type: string
-branding_site_email: 
-    type: string
+reply_emails:
+    type: array
+    items:
+        type: string
 -->
 {% extends 'emails/base.html' %}
 
 {% block content %}
-    <h2><b>{{ name }}</b> has been marked as deleted on {{ branding_site_name }}</h2>
-    <p style="font-size: 1.2em; color: #8B2222">
-        <b>Oh no!</b> Your club has <b>been removed</b> from {{ branding_site_name }} by the <b>Office of Student Affairs</b>.
+    <h2><b>{{ name }}</b> status update on Penn Clubs</h2>
+    <p style="font-size: 1.2em;">
+        Your club has been removed from Penn Clubs by the Office of Student Affairs.
+    </p>
+    <p style="font-size: 1.2em">
+        If you have any questions or concerns, please
+        respond to this email or <a href="mailto:{% for email in reply_emails %}{% if not forloop.first %},{% endif %}{{ email }}{% endfor %}">contact the Office of Student Affairs</a>.
     </p>
-    <p style="font-size: 1.2em">If you believe this is an error, please contact <a href="mailto:{{ branding_site_email }}">{{ branding_site_email }}</a>.</p>
-{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/backend/templates/emails/revised_fair_date.html b/backend/templates/emails/revised_fair_date.html
new file mode 100644
index 000000000..5704cd37f
--- /dev/null
+++ b/backend/templates/emails/revised_fair_date.html
@@ -0,0 +1,44 @@
+<!-- SUBJECT: [CORRECTION] Updated information on Fall Activities Fair registration -->
+<!-- TYPES:
+name:
+    type: string
+url:
+    type: string
+-->
+{% extends 'emails/base.html' %} {% block content %}
+<p style="font-size: 1.2em">Hello,</p>
+<p style="font-size: 1.2em">
+  You previously received an email from us regarding club re-registration, as
+  well as information on a virtual SAC fair. This was outdated information on
+  our part: the activities fair will be in-person from August 27-29.
+</p>
+<p style="font-size: 1.2em; color: #ff3860; font-weight: bold">
+  Re-registration on the Penn Clubs platform, while still necessary, does not
+  constitute registration for the fair. Please sign up for the fair
+  <a
+    href="https://docs.google.com/forms/d/e/1FAIpQLScLn0XrAdkPXgSBoT8bSt0xzWGcmJhwutjuRVmDYVPv8CjVjQ/viewform"
+    >here</a
+  >
+  by August 23rd, 11:59 PM ET.
+</p>
+<p style="font-size: 1.2em">
+  Note that only returning student groups that were University registered last
+  year may sign up for the fair.
+</p>
+<p style="font-size: 1.2em">
+  We have also enabled editing existing clubs and removed the need to re-queue
+  after updating club description. In addition, we've received a large number of
+  requests to transfer club ownership. While we are working through these,
+  existing owners can also transfer ownership <a href="{{ url }}">here</a> at
+  any time (please let us know if you do so and had already submitted a
+  request).
+</p>
+<p style="font-size: 1.2em">
+  Please refer to the official email sent from the Office of Student Affairs for
+  more information or contact the organizers
+  <a href="mailto:fair@sacfunded.net">here</a>.
+</p>
+<p style="font-size: 1.2em">Apologies,</p>
+<p style="font-size: 1.2em">Penn Clubs</p>
+
+{% endblock %}
diff --git a/backend/templates/emails/ticket_confirmation.html b/backend/templates/emails/ticket_confirmation.html
index 735bd7dbf..b14cd6b0e 100644
--- a/backend/templates/emails/ticket_confirmation.html
+++ b/backend/templates/emails/ticket_confirmation.html
@@ -21,31 +21,18 @@
 <h2>Thanks for using Penn Clubs!</h2>
 
 <p style="font-size: 1.2em">
-    {{ first_name }}, thank you for your recent acquisition of a ticket to {{ name }} with ticket type {{type }}.
+    {{ first_name }}, thank you for your recent acquisition of a ticket to <b>{{ name }}</b> with ticket type <b>{{ type }}</b>.
 </p>
 
 <p style="font-size: 1.2em">
-    As a reminder, the event starts at {{ start_time }} and ends at {{ end_time }}.
-
-
+    As a reminder, the event starts at <b>{{ start_time }}</b> and ends at <b>{{ end_time }}</b>.
 </p>
 
-<p style="font-size: 1.2em"> Below is a
-    QR code for
-    your confirmation. </p>
-
+<p style="font-size: 1.2em"> Below is a QR code for your confirmation: </p>
 
 <img id="now" style="max-width: 60%; width: auto;" alt="Ticket QR code" src="cid:{{ cid }}" />
 
-<p style="font-size: 1.2em"> Note: all tickets issued by us are <b>non-refundable</b>. </p>
-
-
+<p style="font-size: 1.2em"> All tickets issued by us are <b>non-refundable</b>. You can view your tickets and transfer them to other users through <a href="{{ ticket_url }}">your Penn Clubs profile</a>.</p>
 
-<p style="font-size: 1.2em">
-    If you have any questions, feel free to respond to this email.
-</p>
-
-<p style="font-size: 1.2em">
-    You can view your tickets and transfer them to other users <a href="{{ ticket_url }}">here</a>.
-</p>
+<p style="font-size: 1.2em">If you have any questions, feel free to respond to this email.</p>
 {% endblock %}
\ No newline at end of file
diff --git a/backend/templates/emails/ticket_transfer.html b/backend/templates/emails/ticket_transfer.html
index d4b69f39f..1490da7e1 100644
--- a/backend/templates/emails/ticket_transfer.html
+++ b/backend/templates/emails/ticket_transfer.html
@@ -17,11 +17,11 @@
 <h2>Ticket Transfer Confirmation</h2>
 
 <p style="font-size: 1.2em">
-    <b>{{ sender_first_name }}</b>, this is confirmation that you have transferred a ticket to <b>{{ receiver_first_name }}</b> (<b>{{ receiver_username }}</b>) for <b>{{ event_name }}</b> with ticket type <b>{{ type }}</b>.
+    {{ sender_first_name }}, this is confirmation that you have transferred a ticket to <b>{{ receiver_first_name }}</b> (<b>{{ receiver_username }}</b>) for <b>{{ event_name }}</b> with ticket type <b>{{ type }}</b>.
 </p>
 
 
 <p style="font-size: 1.2em">
-    If you believe that this was sent in error or have any questions, feel free to respond to this email.
+    If you have any questions or believe this was sent in error, feel free to respond to this email.
 </p>
 {% endblock %}
\ No newline at end of file
diff --git a/backend/tests/clubs/test_commands.py b/backend/tests/clubs/test_commands.py
index bdc6df470..90ceefda1 100644
--- a/backend/tests/clubs/test_commands.py
+++ b/backend/tests/clubs/test_commands.py
@@ -15,9 +15,12 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth.models import Group
 from django.core import mail
+from django.core.cache import caches
 from django.core.management import call_command
 from django.core.management.base import CommandError
 from django.test import TestCase
+from django.test.utils import override_settings
+from django.urls import reverse
 from django.utils import timezone
 from ics import Calendar
 from ics import Event as ICSEvent
@@ -373,8 +376,11 @@ def test_daily_notifications(self):
             "django.utils.timezone.now",
             return_value=now,
         ):
-            call_command("daily_notifications", stderr=errors)
-
+            with mock.patch("django.conf.settings.REAPPROVAL_QUEUE_OPEN", False):
+                call_command("daily_notifications", stderr=errors)
+            self.assertFalse(any(m.to == [self.user1.email] for m in mail.outbox))
+            with mock.patch("django.conf.settings.REAPPROVAL_QUEUE_OPEN", True):
+                call_command("daily_notifications", stderr=errors)
         # ensure approval email was sent out
         self.assertTrue(any(m.to == [self.user1.email] for m in mail.outbox))
 
@@ -590,6 +596,39 @@ def test_renewal(self):
 
         self.assertGreater(len(mail.outbox), current_email_count)
 
+    @override_settings(
+        CACHES={  # don't want to clear prod cache while testing
+            "default": {
+                "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
+            }
+        }
+    )
+    def test_deactivate_invalidates_cache(self):
+        # Clear the cache before starting the test
+        caches["default"].clear()
+
+        with open(os.devnull, "w") as f:
+            call_command("populate", stdout=f)
+
+        club = Club.objects.first()
+
+        # make request to the club detail view to cache it
+        self.client.get(reverse("clubs-detail", args=(club.code,)))
+
+        # club should now be cached
+        cache_key = f"clubs:{club.id}-anon"
+        self.assertIsNotNone(caches["default"].get(cache_key))
+
+        call_command("deactivate", "all", "--force")
+
+        # club should no longer be cached
+        self.assertIsNone(caches["default"].get(cache_key))
+
+        # club should be deactivated and inactive
+        club.refresh_from_db()
+        self.assertFalse(club.active)
+        self.assertIsNone(club.approved)
+
 
 class MergeDuplicatesTestCase(TestCase):
     def setUp(self):
@@ -694,3 +733,75 @@ def test_expire_membership_invites(self):
 
         self.assertFalse(self.expired_invite.active)
         self.assertTrue(self.active_invite.active)
+
+
+class GraduateUsersTestCase(TestCase):
+    def setUp(self):
+        self.club = Club.objects.create(code="test", name="Test Club", active=True)
+        self.user1 = get_user_model().objects.create_user(
+            "bfranklin", "bfranklin@seas.upenn.edu", "test"
+        )
+        self.user2 = get_user_model().objects.create_user(
+            "tjefferson", "tjefferson@seas.upenn.edu", "test"
+        )
+
+        # Set graduation years
+        self.user1.profile.graduation_year = timezone.now().year - 1
+        self.user1.profile.save()
+        self.user2.profile.graduation_year = timezone.now().year + 1
+        self.user2.profile.save()
+
+        # Create active memberships
+        Membership.objects.create(person=self.user1, club=self.club, active=True)
+        Membership.objects.create(person=self.user2, club=self.club, active=True)
+
+    def test_graduate_users(self):
+        # Ensure both memberships are active initially
+        self.assertEqual(Membership.objects.filter(active=True).count(), 2)
+
+        # Run the command
+        call_command("graduate_users")
+
+        # Check that only the graduated user's membership is inactive
+        self.assertEqual(Membership.objects.filter(active=True).count(), 1)
+        self.assertFalse(Membership.objects.get(person=self.user1).active)
+        self.assertTrue(Membership.objects.get(person=self.user2).active)
+
+    def test_graduate_users_output(self):
+        # Capture command output
+        out = io.StringIO()
+        call_command("graduate_users", stdout=out)
+
+        # Check the output
+        self.assertIn(
+            "Updated the membership status of 1 student club relationships!",
+            out.getvalue(),
+        )
+
+
+class OsaPermsUpdatesTestCase(TestCase):
+    def setUp(self):
+        self.user1 = get_user_model().objects.create_user("gwashington")
+
+    def test_osa_perms_updates(self):
+        # Test error when OSA_KEYS is not set
+        with mock.patch("django.conf.settings.OSA_KEYS", None):
+            with self.assertRaises(ValueError):
+                call_command("osa_perms_updates")
+            self.assertFalse(self.user1.is_superuser)
+
+        with mock.patch("django.conf.settings.OSA_KEYS", ["gwashington"]):
+            # Test error when Approvers group is not found
+            with self.assertRaises(ValueError):
+                call_command("osa_perms_updates")
+            self.assertFalse(self.user1.is_superuser)
+
+            # Create Approvers group
+            Group.objects.create(name="Approvers")
+            call_command("osa_perms_updates")
+            self.user1.refresh_from_db()
+            self.assertTrue(self.user1.groups.filter(name="Approvers").exists())
+            self.assertTrue(self.user1.is_staff)
+            self.assertTrue(self.user1.is_superuser)
+            self.assertTrue(self.user1.has_perm("approve_club"))
+            self.assertTrue(self.user1.has_perm("see_pending_clubs"))
diff --git a/backend/tests/clubs/test_models.py b/backend/tests/clubs/test_models.py
index e70ca7fdf..04e42d1d1 100644
--- a/backend/tests/clubs/test_models.py
+++ b/backend/tests/clubs/test_models.py
@@ -63,6 +63,60 @@ def test_parent_children(self):
         self.assertEqual(self.club2.parent_orgs.first(), self.club1)
         self.assertEqual(self.club1.children_orgs.first(), self.club2)
 
+    def test_get_officer_emails(self):
+        # Create test users with various email formats
+        user1 = get_user_model().objects.create_user(
+            "user1", "user1@example.com", "password"
+        )
+        user2 = get_user_model().objects.create_user(
+            "user2", "   user2@example.com  ", "password"
+        )  # whitespace
+        user3 = get_user_model().objects.create_user(
+            "user3", "", "password"
+        )  # empty email
+        user4 = get_user_model().objects.create_user(
+            "user4", "user4@example.com", "password"
+        )
+
+        # Create memberships for the test users
+        Membership.objects.create(
+            person=user1, club=self.club1, role=Membership.ROLE_OFFICER
+        )
+        Membership.objects.create(
+            person=user2, club=self.club1, role=Membership.ROLE_OWNER
+        )
+        Membership.objects.create(
+            person=user3, club=self.club1, role=Membership.ROLE_OFFICER
+        )
+        Membership.objects.create(
+            person=user4, club=self.club1, role=Membership.ROLE_OFFICER, active=False
+        )  # alumni
+
+        # Test with valid club email
+        self.club1.email = "club@example.com"
+        self.club1.save()
+
+        officer_emails = self.club1.get_officer_emails()
+        expected_emails = ["club@example.com", "user1@example.com", "user2@example.com"]
+        self.assertEqual(officer_emails, expected_emails)
+
+        # Ensure alumni are not included
+        self.assertNotIn("user4@example.com", officer_emails)
+
+        # Test with invalid club email
+        self.club1.email = "invalid-email"
+        self.club1.save()
+
+        officer_emails = self.club1.get_officer_emails()
+        expected_emails = ["user1@example.com", "user2@example.com"]
+        self.assertEqual(officer_emails, expected_emails)
+
+        # Test with empty club email
+        self.club1.email = ""
+        self.club1.save()
+        officer_emails = self.club1.get_officer_emails()
+        self.assertEqual(officer_emails, expected_emails)
+
 
 class ProfileTestCase(TestCase):
     def test_profile_creation(self):
@@ -144,7 +198,10 @@ def setUp(self):
             code="a", name="a", subtitle="a", founded=date, description="a", size=1
         )
         self.advisor = Advisor.objects.create(
-            name="Eric Wang", phone="+12025550133", club=club, public=True
+            name="Eric Wang",
+            phone="+12025550133",
+            club=club,
+            visibility=Advisor.ADVISOR_VISIBILITY_ALL,
         )
 
     def test_str(self):
diff --git a/backend/tests/clubs/test_ticketing.py b/backend/tests/clubs/test_ticketing.py
index a5415961a..8f047b9d8 100644
--- a/backend/tests/clubs/test_ticketing.py
+++ b/backend/tests/clubs/test_ticketing.py
@@ -6,6 +6,7 @@
 
 import freezegun
 from django.contrib.auth import get_user_model
+from django.core import mail
 from django.db.models import Count
 from django.db.models.deletion import ProtectedError
 from django.test import TestCase
@@ -398,6 +399,44 @@ def test_issue_tickets_insufficient_quantity(self):
             Ticket.objects.filter(type="normal", holder__isnull=False).count(), 0
         )
 
+    def test_email_blast(self):
+        Membership.objects.create(
+            person=self.user1, club=self.club1, role=Membership.ROLE_OFFICER
+        )
+        self.client.login(username=self.user1.username, password="test")
+
+        ticket1 = self.tickets1[0]
+        ticket1.owner = self.user2
+        ticket1.save()
+
+        resp = self.client.post(
+            reverse("club-events-email-blast", args=(self.club1.code, self.event1.pk)),
+            {"content": "Test email blast content"},
+            format="json",
+        )
+
+        self.assertEqual(resp.status_code, 200, resp.content)
+
+        self.assertEqual(len(mail.outbox), 1)
+        email = mail.outbox[0]
+
+        self.assertIn(self.user2.email, email.to)
+        self.assertIn(self.user1.email, email.to)
+
+        self.assertEqual(
+            email.subject, f"Update on {self.event1.name} from {self.club1.name}"
+        )
+        self.assertIn("Test email blast content", email.body)
+
+    def test_email_blast_empty_content(self):
+        self.client.login(username=self.user1.username, password="test")
+        resp = self.client.post(
+            reverse("club-events-email-blast", args=(self.club1.code, self.event1.pk)),
+            {"content": ""},
+            format="json",
+        )
+        self.assertEqual(resp.status_code, 400, resp.content)
+
     def test_get_tickets_information_no_tickets(self):
         # Delete all the tickets
         Ticket.objects.all().delete()
@@ -495,6 +534,27 @@ def test_add_to_cart(self):
         self.assertEqual(cart.tickets.filter(type="normal").count(), 2, cart.tickets)
         self.assertEqual(cart.tickets.filter(type="premium").count(), 1, cart.tickets)
 
+    def test_add_to_cart_elapsed_event(self):
+        self.client.login(username=self.user1.username, password="test")
+
+        # Set the event end time to the past
+        self.event1.end_time = timezone.now() - timezone.timedelta(days=1)
+        self.event1.save()
+
+        tickets_to_add = {
+            "quantities": [
+                {"type": "normal", "count": 1},
+            ]
+        }
+        resp = self.client.post(
+            reverse("club-events-add-to-cart", args=(self.club1.code, self.event1.pk)),
+            tickets_to_add,
+            format="json",
+        )
+
+        self.assertEqual(resp.status_code, 403, resp.content)
+        self.assertIn("This event has already ended", resp.data["detail"], resp.data)
+
     def test_add_to_cart_twice_accumulates(self):
         self.client.login(username=self.user1.username, password="test")
 
@@ -947,6 +1007,124 @@ def test_get_cart_replacement_required_sold_out(self):
         to_add = set(map(lambda t: str(t.id), tickets_to_add))
         self.assertEqual(len(in_cart & to_add), 0, in_cart | to_add)
 
+    def test_get_cart_elapsed_event(self):
+        self.client.login(username=self.user1.username, password="test")
+
+        # Add a few tickets
+        cart, _ = Cart.objects.get_or_create(owner=self.user1)
+        tickets_to_add = self.tickets1[:5]
+        for ticket in tickets_to_add:
+            cart.tickets.add(ticket)
+        cart.save()
+
+        # Set the event end time to the past
+        self.event1.end_time = timezone.now() - timezone.timedelta(days=1)
+        self.event1.save()
+
+        resp = self.client.get(reverse("tickets-cart"), format="json")
+        data = resp.json()
+
+        # The cart should now be empty
+        self.assertEqual(len(data["tickets"]), 0, data)
+
+        # All tickets should be in the sold out array
+        self.assertEqual(len(data["sold_out"]), 1, data)
+
+        expected_sold_out = {
+            "type": self.tickets1[0].type,
+            "event": {
+                "id": self.event1.id,
+                "name": self.event1.name,
+            },
+            "count": 5,
+        }
+        for key, val in expected_sold_out.items():
+            self.assertEqual(data["sold_out"][0][key], val, data)
+
+    def test_place_hold_on_tickets(self):
+        from clubs.views import TicketViewSet
+
+        self.client.login(username=self.user1.username, password="test")
+
+        # Add a few tickets
+        cart, _ = Cart.objects.get_or_create(owner=self.user1)
+        tickets_to_add = self.tickets1[:3]
+        cart.tickets.add(*tickets_to_add)
+        cart.save()
+
+        TicketViewSet._place_hold_on_tickets(self.user1, cart.tickets)
+        holding_expiration = timezone.now() + timezone.timedelta(minutes=10)
+
+        for ticket in cart.tickets.all():
+            self.assertIsNone(ticket.owner)
+            self.assertEqual(self.user1, ticket.holder)
+            self.assertAlmostEqual(
+                holding_expiration,
+                ticket.holding_expiration,
+                delta=timedelta(seconds=10),
+            )
+
+        # Move Django's internal clock 10 minutes forward
+        with freezegun.freeze_time(holding_expiration):
+            Ticket.objects.update_holds()
+            for ticket in cart.tickets.all():
+                self.assertIsNone(ticket.owner)
+                self.assertIsNone(ticket.holder)
+
+    def test_give_tickets(self):
+        from clubs.views import TicketViewSet
+
+        self.client.login(username=self.user1.username, password="test")
+        # Add a few tickets
+        cart, _ = Cart.objects.get_or_create(owner=self.user1)
+        tickets_to_add = self.tickets1[:3]
+        cart.tickets.add(*tickets_to_add)
+        cart.save()
+
+        order_info = {
+            "amountDetails": {"totalAmount": TicketViewSet._calculate_cart_total(cart)},
+            "billTo": {
+                "firstName": self.user1.first_name,
+                "lastName": self.user1.last_name,
+                "phoneNumber": "3021239234",
+                "email": self.user1.email,
+            },
+        }
+
+        TicketViewSet._place_hold_on_tickets(self.user1, cart.tickets)
+        TicketViewSet._give_tickets(
+            self.user1,
+            order_info,
+            cart,
+            reconciliation_id=MockPaymentResponse().reconciliation_id,
+        )
+
+        # Check that tickets are assigned their owner
+        for ticket in cart.tickets.all():
+            self.assertEqual(self.user1, ticket.owner)
+            self.assertIsNone(ticket.holder)
+
+        # Check that the cart is empty
+        self.assertEqual(0, cart.tickets.count())
+
+        # Check that transaction record is created
+        record_exists = TicketTransactionRecord.objects.filter(
+            reconciliation_id=MockPaymentResponse().reconciliation_id
+        ).exists()
+        self.assertTrue(record_exists)
+
+        # Check that confirmation emails were sent
+        self.assertEqual(len(mail.outbox), len(tickets_to_add))
+        for msg in mail.outbox:
+            self.assertIn(
+                f"Ticket confirmation for {self.user1.first_name} "
+                f"{self.user1.last_name}",
+                msg.subject,
+            )
+            self.assertIn(self.user1.first_name, msg.body)
+            self.assertIn(self.event1.name, msg.body)
+            self.assertIsNotNone(msg.attachments)
+
     def test_initiate_checkout_non_free_tickets(self):
         self.client.login(username=self.user1.username, password="test")
 
@@ -1088,7 +1266,7 @@ def test_initiate_checkout_only_free_tickets(self):
         held_tickets = Ticket.objects.filter(holder=self.user1)
         self.assertEqual(held_tickets.count(), 0, held_tickets)
 
-        # Transaction record created
+        # Check that transaction record is created
         record_exists = TicketTransactionRecord.objects.filter(
             reconciliation_id="None"
         ).exists()
@@ -1267,7 +1445,7 @@ def test_complete_checkout(self):
             held_tickets = Ticket.objects.filter(holder=self.user1)
             self.assertEqual(held_tickets.count(), 0, held_tickets)
 
-            # Transaction record created
+            # Check that transaction record is created
             record_exists = TicketTransactionRecord.objects.filter(
                 reconciliation_id=MockPaymentResponse().reconciliation_id
             ).exists()
diff --git a/backend/tests/clubs/test_views.py b/backend/tests/clubs/test_views.py
index e829f898a..789c5435f 100644
--- a/backend/tests/clubs/test_views.py
+++ b/backend/tests/clubs/test_views.py
@@ -18,11 +18,13 @@
 
 from clubs.filters import DEFAULT_PAGE_SIZE
 from clubs.models import (
+    Advisor,
     ApplicationSubmission,
     Asset,
     Badge,
     Club,
     ClubApplication,
+    ClubApprovalResponseTemplate,
     ClubFair,
     ClubFairRegistration,
     Event,
@@ -167,6 +169,8 @@ def setUpTestData(cls):
         Tag.objects.create(name="Undergraduate")
 
     def setUp(self):
+        cache.clear()  # clear the cache between tests
+
         self.client = Client()
 
         self.club1 = Club.objects.create(
@@ -202,6 +206,77 @@ def setUp(self):
             responder=self.user1,
         )
 
+        self.advisor_admin = Advisor.objects.create(
+            name="Anonymous Avi",
+            phone="+12025550133",
+            club=self.club1,
+            visibility=Advisor.ADVISOR_VISIBILITY_ADMIN,
+        )
+
+        self.advisor_students = Advisor.objects.create(
+            name="Reclusive Rohan",
+            phone="+12025550133",
+            club=self.club1,
+            visibility=Advisor.ADVISOR_VISIBILITY_STUDENTS,
+        )
+
+        self.advisor_public = Advisor.objects.create(
+            name="Jocular Julian",
+            phone="+12025550133",
+            club=self.club1,
+            visibility=Advisor.ADVISOR_VISIBILITY_ALL,
+        )
+
+    def test_advisor_visibility(self):
+        """
+        Tests each tier of advisor visibility.
+        """
+        # Anonymous view
+        self.client.logout()
+        resp = self.client.get(reverse("clubs-detail", args=(self.club1.code,)))
+        self.assertIn(resp.status_code, [200, 201], resp.content)
+        self.assertEqual(len(resp.data["advisor_set"]), 1)
+        self.assertEqual(resp.data["advisor_set"][0]["name"], "Jocular Julian")
+
+        # Student view
+        self.client.login(username=self.user1.username, password="test")
+        resp = self.client.get(reverse("clubs-detail", args=(self.club1.code,)))
+        self.assertIn(resp.status_code, [200, 201], resp.content)
+        self.assertEqual(len(resp.data["advisor_set"]), 2)
+        sorted_advisors = sorted(
+            [advisor["name"] for advisor in resp.data["advisor_set"]]
+        )
+        self.assertEqual(sorted_advisors, ["Jocular Julian", "Reclusive Rohan"])
+
+        # Admin view
+        self.client.login(username=self.user5.username, password="test")
+        resp = self.client.get(reverse("clubs-detail", args=(self.club1.code,)))
+        self.assertIn(resp.status_code, [200, 201], resp.content)
+        self.assertEqual(len(resp.data["advisor_set"]), 3)
+        sorted_advisors = sorted(
+            [advisor["name"] for advisor in resp.data["advisor_set"]]
+        )
+        self.assertEqual(
+            sorted_advisors, ["Anonymous Avi", "Jocular Julian", "Reclusive Rohan"]
+        )
+
+    def test_advisor_viewset(self):
+        # Make sure we can't view advisors via the viewset if not authed
+        self.client.logout()
+        resp = self.client.get(reverse("club-advisors-list", args=(self.club1.code,)))
+        self.assertIn(resp.status_code, [400, 403], resp.content)
+        self.assertIn("detail", resp.data)
+
+        self.client.login(username=self.user1.username, password="test")
+        resp = self.client.get(reverse("club-advisors-list", args=(self.club1.code,)))
+        self.assertIn(resp.status_code, [400, 403], resp.content)
+        self.assertIn("detail", resp.data)
+
+        self.client.login(username=self.user5.username, password="test")
+        resp = self.client.get(reverse("club-advisors-list", args=(self.club1.code,)))
+        self.assertIn(resp.status_code, [200, 201], resp.content)
+        self.assertEqual(len(resp.data), 3)
+
     def test_club_upload(self):
         """
         Test uploading a club logo.
@@ -973,23 +1048,26 @@ def test_club_create_empty(self):
         """
         self.client.login(username=self.user4.username, password="test")
 
-        resp = self.client.post(
-            reverse("clubs-list"),
-            {
-                "code": "penn-labs",
-                "name": "Penn Labs",
-                "description": "This is an example description.",
-                "tags": [{"name": "Graduate"}],
-                "email": "example@example.com",
-                "facebook": "",
-                "twitter": "",
-                "instagram": "",
-                "website": "",
-                "linkedin": "",
-                "github": "",
-            },
-            content_type="application/json",
-        )
+        with patch("django.conf.settings.REAPPROVAL_QUEUE_OPEN", True), patch(
+            "django.conf.settings.NEW_APPROVAL_QUEUE_OPEN", True
+        ):
+            resp = self.client.post(
+                reverse("clubs-list"),
+                {
+                    "code": "penn-labs",
+                    "name": "Penn Labs",
+                    "description": "This is an example description.",
+                    "tags": [{"name": "Graduate"}],
+                    "email": "example@example.com",
+                    "facebook": "",
+                    "twitter": "",
+                    "instagram": "",
+                    "website": "",
+                    "linkedin": "",
+                    "github": "",
+                },
+                content_type="application/json",
+            )
         self.assertIn(resp.status_code, [200, 201], resp.content)
 
         # continue these tests as not a superuser but still logged in
@@ -1012,6 +1090,32 @@ def test_club_create_empty(self):
         codes = [club["code"] for club in resp.data]
         self.assertNotIn(club.code, codes)
 
+    def test_club_create_new_approval_queue_closed(self):
+        """
+        Test creating a club when the new approval queue is closed, but the
+        reapproval queue is open.
+        """
+        self.client.login(username=self.user4.username, password="test")
+
+        with patch("django.conf.settings.REAPPROVAL_QUEUE_OPEN", True), patch(
+            "django.conf.settings.NEW_APPROVAL_QUEUE_OPEN", False
+        ):
+            resp = self.client.post(
+                reverse("clubs-list"),
+                {
+                    "code": "new-club",
+                    "name": "New Club",
+                    "description": "This is a new club.",
+                    "tags": [{"name": "Undergraduate"}],
+                    "email": "newclub@example.com",
+                },
+                content_type="application/json",
+            )
+
+        self.assertEqual(resp.status_code, 400, resp.content)
+        self.assertIn("The approval queue is not currently open.", str(resp.content))
+        self.assertFalse(Club.objects.filter(code="new-club").exists())
+
     def test_club_approve(self):
         """
         Test approving an existing unapproved club.
@@ -1036,6 +1140,58 @@ def test_club_approve(self):
         self.assertIsNotNone(self.club1.approved_on)
         self.assertIsNotNone(self.club1.approved_by)
 
+    def test_club_display_after_deactivation_for_permissioned_vs_non_permissioned(self):
+        """
+        Test club retrieval after deactivation script runs. Non-permissioned users
+        should see the last approved version of the club. Permissioned users (e.g.
+        admins, club members) should see the most up-to-date version.
+        """
+        # club is approved before deactivation
+        self.assertTrue(self.club1.approved)
+
+        call_command("deactivate", "all", "--force")
+
+        club = self.club1
+        club.refresh_from_db()
+
+        # after deactivation, club should not be approved and should not have approver
+        self.assertIsNone(club.approved)
+        self.assertIsNone(club.approved_by)
+
+        # non-permissioned users should see the last approved version
+        non_admin_resp = self.client.get(reverse("clubs-detail", args=(club.code,)))
+        self.assertEqual(non_admin_resp.status_code, 200)
+        non_admin_data = non_admin_resp.json()
+        self.assertTrue(non_admin_data["approved"])
+
+        # permissioned users should see the club as it is in the DB
+        self.client.login(username=self.user5.username, password="test")
+        admin_resp = self.client.get(reverse("clubs-detail", args=(club.code,)))
+        self.assertEqual(admin_resp.status_code, 200)
+        admin_data = admin_resp.json()
+        self.assertIsNone(admin_data["approved"])
+        self.client.logout()
+
+        cache.clear()
+
+        # reversing the order of operations shouldn't change anything
+        self.client.login(username=self.user5.username, password="test")
+        admin_resp = self.client.get(reverse("clubs-detail", args=(club.code,)))
+        self.assertEqual(admin_resp.status_code, 200)
+        admin_data = admin_resp.json()
+        self.assertIsNone(admin_data["approved"])
+        self.client.logout()
+
+        non_admin_resp = self.client.get(reverse("clubs-detail", args=(club.code,)))
+        self.assertEqual(non_admin_resp.status_code, 200)
+        non_admin_data = non_admin_resp.json()
+        self.assertTrue(non_admin_data["approved"])
+
+        # club object itself shouldn't have changed
+        club.refresh_from_db()
+        self.assertFalse(club.active)
+        self.assertIsNone(club.approved)
+
     def test_club_create_url_sanitize(self):
         """
         Test creating clubs with malicious URLs.
@@ -1044,20 +1200,23 @@ def test_club_create_url_sanitize(self):
 
         exploit_string = "javascript:alert(1)"
 
-        resp = self.client.post(
-            reverse("clubs-list"),
-            {
-                "name": "Bad Club",
-                "tags": [],
-                "facebook": exploit_string,
-                "twitter": exploit_string,
-                "instagram": exploit_string,
-                "website": exploit_string,
-                "linkedin": exploit_string,
-                "github": exploit_string,
-            },
-            content_type="application/json",
-        )
+        with patch("django.conf.settings.REAPPROVAL_QUEUE_OPEN", True), patch(
+            "django.conf.settings.NEW_APPROVAL_QUEUE_OPEN", True
+        ):
+            resp = self.client.post(
+                reverse("clubs-list"),
+                {
+                    "name": "Bad Club",
+                    "tags": [],
+                    "facebook": exploit_string,
+                    "twitter": exploit_string,
+                    "instagram": exploit_string,
+                    "website": exploit_string,
+                    "linkedin": exploit_string,
+                    "github": exploit_string,
+                },
+                content_type="application/json",
+            )
         self.assertIn(resp.status_code, [400, 403], resp.content)
 
     def test_club_create_description_sanitize_good(self):
@@ -1079,16 +1238,20 @@ def test_club_create_description_sanitize_good(self):
 <img src=\"/test.png\">"""
 
         self.client.login(username=self.user5.username, password="test")
-        resp = self.client.post(
-            reverse("clubs-list"),
-            {
-                "name": "Penn Labs",
-                "tags": [{"name": "Undergraduate"}],
-                "description": test_good_string,
-                "email": "example@example.com",
-            },
-            content_type="application/json",
-        )
+
+        with patch("django.conf.settings.REAPPROVAL_QUEUE_OPEN", True), patch(
+            "django.conf.settings.NEW_APPROVAL_QUEUE_OPEN", True
+        ):
+            resp = self.client.post(
+                reverse("clubs-list"),
+                {
+                    "name": "Penn Labs",
+                    "tags": [{"name": "Undergraduate"}],
+                    "description": test_good_string,
+                    "email": "example@example.com",
+                },
+                content_type="application/json",
+            )
         cache.clear()
         self.assertIn(resp.status_code, [200, 201], resp.content)
 
@@ -1105,16 +1268,20 @@ def test_club_create_description_sanitize_bad(self):
         test_bad_string = '<script>alert(1);</script><img src="javascript:alert(1)">'
 
         self.client.login(username=self.user5.username, password="test")
-        resp = self.client.post(
-            reverse("clubs-list"),
-            {
-                "name": "Penn Labs",
-                "tags": [{"name": "Graduate"}],
-                "description": test_bad_string,
-                "email": "example@example.com",
-            },
-            content_type="application/json",
-        )
+
+        with patch("django.conf.settings.REAPPROVAL_QUEUE_OPEN", True), patch(
+            "django.conf.settings.NEW_APPROVAL_QUEUE_OPEN", True
+        ):
+            resp = self.client.post(
+                reverse("clubs-list"),
+                {
+                    "name": "Penn Labs",
+                    "tags": [{"name": "Graduate"}],
+                    "description": test_bad_string,
+                    "email": "example@example.com",
+                },
+                content_type="application/json",
+            )
         self.assertIn(resp.status_code, [200, 201], resp.content)
 
         resp = self.client.get(reverse("clubs-detail", args=("penn-labs",)))
@@ -1130,9 +1297,12 @@ def test_club_create_no_input(self):
         """
         self.client.login(username=self.user5.username, password="test")
 
-        resp = self.client.post(
-            reverse("clubs-list"), {}, content_type="application/json"
-        )
+        with patch("django.conf.settings.REAPPROVAL_QUEUE_OPEN", True), patch(
+            "django.conf.settings.NEW_APPROVAL_QUEUE_OPEN", True
+        ):
+            resp = self.client.post(
+                reverse("clubs-list"), {}, content_type="application/json"
+            )
         self.assertIn(resp.status_code, [400, 403], resp.content)
 
     def test_club_create_nonexistent_tag(self):
@@ -1141,35 +1311,42 @@ def test_club_create_nonexistent_tag(self):
         """
         self.client.login(username=self.user5.username, password="test")
 
-        resp = self.client.post(
-            reverse("clubs-list"),
-            {
-                "name": "Penn Labs",
-                "description": "We code stuff.",
-                "email": "contact@pennlabs.org",
-                "tags": [{"name": "totally definitely nonexistent tag"}],
-            },
-            content_type="application/json",
-        )
+        with patch("django.conf.settings.REAPPROVAL_QUEUE_OPEN", True), patch(
+            "django.conf.settings.NEW_APPROVAL_QUEUE_OPEN", True
+        ):
+            resp = self.client.post(
+                reverse("clubs-list"),
+                {
+                    "name": "Penn Labs",
+                    "description": "We code stuff.",
+                    "email": "contact@pennlabs.org",
+                    "tags": [{"name": "totally definitely nonexistent tag"}],
+                },
+                content_type="application/json",
+            )
         self.assertIn(resp.status_code, [400, 404], resp.content)
 
     def test_club_create_no_auth(self):
         """
         Creating a club without authentication should result in an error.
         """
-        resp = self.client.post(
-            reverse("clubs-list"),
-            {
-                "name": "Penn Labs",
-                "description": "We code stuff.",
-                "email": "contact@pennlabs.org",
-                "facebook": "966590693376781",
-                "twitter": "@Penn",
-                "instagram": "@uofpenn",
-                "tags": [],
-            },
-            content_type="application/json",
-        )
+
+        with patch("django.conf.settings.REAPPROVAL_QUEUE_OPEN", True), patch(
+            "django.conf.settings.NEW_APPROVAL_QUEUE_OPEN", True
+        ):
+            resp = self.client.post(
+                reverse("clubs-list"),
+                {
+                    "name": "Penn Labs",
+                    "description": "We code stuff.",
+                    "email": "contact@pennlabs.org",
+                    "facebook": "966590693376781",
+                    "twitter": "@Penn",
+                    "instagram": "@uofpenn",
+                    "tags": [],
+                },
+                content_type="application/json",
+            )
         self.assertIn(resp.status_code, [400, 403], resp.content)
 
     def test_club_create(self):
@@ -1185,31 +1362,34 @@ def test_club_create(self):
 
         self.client.login(username=self.user5.username, password="test")
 
-        resp = self.client.post(
-            reverse("clubs-list"),
-            {
-                "name": "Penn Labs",
-                "description": "We code stuff.",
-                "badges": [{"label": "SAC Funded"}],
-                "tags": [
-                    {"name": tag1.name},
-                    {"name": tag2.name},
-                    {"name": "Graduate"},
-                ],
-                "target_schools": [{"id": school1.id}],
-                "email": "example@example.com",
-                "facebook": "https://www.facebook.com/groups/966590693376781/"
-                + "?ref=nf_target&fref=nf",
-                "twitter": "https://twitter.com/Penn",
-                "instagram": "https://www.instagram.com/uofpenn/?hl=en",
-                "website": "https://pennlabs.org",
-                "linkedin": "https://www.linkedin.com"
-                "/school/university-of-pennsylvania/",
-                "youtube": "https://youtu.be/dQw4w9WgXcQ",
-                "github": "https://github.com/pennlabs",
-            },
-            content_type="application/json",
-        )
+        with patch("django.conf.settings.REAPPROVAL_QUEUE_OPEN", True), patch(
+            "django.conf.settings.NEW_APPROVAL_QUEUE_OPEN", True
+        ):
+            resp = self.client.post(
+                reverse("clubs-list"),
+                {
+                    "name": "Penn Labs",
+                    "description": "We code stuff.",
+                    "badges": [{"label": "SAC Funded"}],
+                    "tags": [
+                        {"name": tag1.name},
+                        {"name": tag2.name},
+                        {"name": "Graduate"},
+                    ],
+                    "target_schools": [{"id": school1.id}],
+                    "email": "example@example.com",
+                    "facebook": "https://www.facebook.com/groups/966590693376781/"
+                    + "?ref=nf_target&fref=nf",
+                    "twitter": "https://twitter.com/Penn",
+                    "instagram": "https://www.instagram.com/uofpenn/?hl=en",
+                    "website": "https://pennlabs.org",
+                    "linkedin": "https://www.linkedin.com"
+                    "/school/university-of-pennsylvania/",
+                    "youtube": "https://youtu.be/dQw4w9WgXcQ",
+                    "github": "https://github.com/pennlabs",
+                },
+                content_type="application/json",
+            )
         self.assertIn(resp.status_code, [200, 201], resp.content)
 
         # ensure club was actually created
@@ -1249,11 +1429,14 @@ def test_club_create_duplicate(self):
         """
         self.client.login(username=self.user5.username, password="test")
 
-        resp = self.client.post(
-            reverse("clubs-list"),
-            {"name": "Test Club", "tags": []},
-            content_type="application/json",
-        )
+        with patch("django.conf.settings.REAPPROVAL_QUEUE_OPEN", True), patch(
+            "django.conf.settings.NEW_APPROVAL_QUEUE_OPEN", True
+        ):
+            resp = self.client.post(
+                reverse("clubs-list"),
+                {"name": "Test Club", "tags": []},
+                content_type="application/json",
+            )
         self.assertIn(resp.status_code, [400, 403], resp.content)
 
     def test_club_list_search(self):
@@ -1578,16 +1761,22 @@ def test_club_modify(self):
         """
         tag3 = Tag.objects.create(name="College")
 
-        self.client.login(username=self.user5.username, password="test")
-        resp = self.client.patch(
-            reverse("clubs-detail", args=(self.club1.code,)),
-            {
-                "description": "We do stuff.",
-                "tags": [{"name": tag3.name}, {"name": "Graduate"}],
-            },
-            content_type="application/json",
+        Membership.objects.create(
+            person=self.user1, club=self.club1, role=Membership.ROLE_OWNER
         )
-        self.assertIn(resp.status_code, [200, 201], resp.content)
+
+        self.client.login(username=self.user1.username, password="test")
+
+        with patch("django.conf.settings.REAPPROVAL_QUEUE_OPEN", True):
+            resp = self.client.patch(
+                reverse("clubs-detail", args=(self.club1.code,)),
+                {
+                    "description": "We do stuff.",
+                    "tags": [{"name": tag3.name}, {"name": "Graduate"}],
+                },
+                content_type="application/json",
+            )
+            self.assertIn(resp.status_code, [200, 201], resp.content)
 
         # ensure that changes were made
         resp = self.client.get(reverse("clubs-detail", args=(self.club1.code,)))
@@ -2199,22 +2388,50 @@ def test_club_sensitive_field_renew(self):
         # login to officer user
         self.client.login(username=self.user4.username, password="test")
 
-        for field in {"name", "description"}:
-            # edit sensitive field
-            resp = self.client.patch(
-                reverse("clubs-detail", args=(club.code,)),
-                {field: "New Club Name/Description"},
-                content_type="application/json",
-            )
-            self.assertIn(resp.status_code, [200, 201], resp.content)
+        with patch("django.conf.settings.REAPPROVAL_QUEUE_OPEN", False):
+            for field in {"name"}:
+                # edit sensitive field
+                resp = self.client.patch(
+                    reverse("clubs-detail", args=(club.code,)),
+                    {field: "New Club Name/Description"},
+                    content_type="application/json",
+                )
+                self.assertIn(resp.status_code, [400], resp.content)
 
-            # ensure club is marked as not approved
-            club.refresh_from_db()
-            self.assertFalse(club.approved)
+                # ensure club is marked as approved (request didn't go through)
+                club.refresh_from_db()
+                self.assertTrue(club.approved)
 
-            # reset to approved
-            club.approved = True
-            club.save(update_fields=["approved"])
+        # store result of approval history query
+        resp = self.client.get(reverse("clubs-history", args=(club.code,)))
+        self.assertIn(resp.status_code, [200], resp.content)
+        previous_history = json.loads(resp.content.decode("utf-8"))
+        self.assertTrue(previous_history[0]["approved"])
+
+        with patch("django.conf.settings.REAPPROVAL_QUEUE_OPEN", True):
+            for field in {"name"}:
+                # edit sensitive field
+                resp = self.client.patch(
+                    reverse("clubs-detail", args=(club.code,)),
+                    {field: "New Club Name/Description"},
+                    content_type="application/json",
+                )
+                self.assertIn(resp.status_code, [200, 201], resp.content)
+                resp = self.client.get(reverse("clubs-history", args=(club.code,)))
+                # find the approval history
+                resp = self.client.get(reverse("clubs-history", args=(club.code,)))
+                self.assertIn(resp.status_code, [200], resp.content)
+                history = json.loads(resp.content.decode("utf-8"))
+                self.assertEqual(len(history), len(previous_history) + 1)
+                self.assertFalse(history[0]["approved"])
+
+                # ensure club is marked as not approved
+                club.refresh_from_db()
+                self.assertFalse(club.approved)
+
+                # reset to approved
+                club.approved = True
+                club.save(update_fields=["approved"])
 
         # login to superuser account
         self.client.login(username=self.user5.username, password="test")
@@ -2510,6 +2727,7 @@ def test_club_fair_registration(self):
             end_time=now + datetime.timedelta(days=14),
             registration_start_time=now - datetime.timedelta(days=1),
             registration_end_time=now + datetime.timedelta(days=1),
+            virtual=True,
             questions=json.dumps(
                 [
                     {
@@ -2659,7 +2877,7 @@ def test_user_profile(self):
 
     def test_alumni_page(self):
         """
-        Ensure alumni page can be seen, even for users who are not logged in.
+        Ensure alumni page can be seen
         """
         now = timezone.now()
         for i, user in enumerate([self.user1, self.user2, self.user3]):
@@ -2672,6 +2890,10 @@ def test_alumni_page(self):
 
         # fetch alumni page
         resp = self.client.get(reverse("clubs-alumni", args=(self.club1.code,)))
+        self.assertIn(resp.status_code, [403], resp.content)
+
+        self.client.login(username=self.user4.username, password="test")
+        resp = self.client.get(reverse("clubs-alumni", args=(self.club1.code,)))
         self.assertIn(resp.status_code, [200], resp.content)
         data = resp.json()
 
@@ -2688,7 +2910,7 @@ def test_execute_script(self):
         self.assertIn(resp.status_code, [200], resp.content)
         self.assertIsInstance(resp.data, list, resp.content)
 
-        resp = self.client.post(reverse("scripts"), {"action": "graduate_users"})
+        resp = self.client.post(reverse("scripts"), {"action": "find_broken_images"})
         self.assertIn(resp.status_code, [200], resp.content)
         self.assertIn("output", resp.data, resp.content)
 
@@ -2868,3 +3090,77 @@ def test_event_add_meeting(self):
         self.event1.refresh_from_db()
         self.assertIn("url", resp.data, resp.content)
         self.assertTrue(self.event1.url, resp.content)
+
+    def test_club_approval_response_templates(self):
+        """
+        Test operations and permissions for club approval response templates.
+        """
+
+        # Log in as superuser
+        self.client.login(username=self.user5.username, password="test")
+
+        # Create a new template
+        resp = self.client.post(
+            reverse("templates-list"),
+            {
+                "title": "Test template",
+                "content": "This is a new template",
+            },
+            content_type="application/json",
+        )
+        self.assertEqual(resp.status_code, 201)
+
+        # Create another template
+        template = ClubApprovalResponseTemplate.objects.create(
+            author=self.user5,
+            title="Another template",
+            content="This is another template",
+        )
+
+        # List templates
+        resp = self.client.get(reverse("templates-list"))
+        self.assertEqual(resp.status_code, 200)
+        self.assertEqual(len(resp.json()), 2)
+
+        # Update a template
+        resp = self.client.patch(
+            reverse("templates-detail", args=[template.id]),
+            {"title": "Updated title"},
+            content_type="application/json",
+        )
+        self.assertIn(resp.status_code, [200, 201], resp.content)
+
+        # Verify update
+        template.refresh_from_db()
+        self.assertEqual(template.title, "Updated title")
+
+        # Delete the template
+        resp = self.client.delete(reverse("templates-detail", args=[template.id]))
+        self.assertEqual(resp.status_code, 204)
+
+        # Verify the template has been deleted
+        self.assertIsNone(
+            ClubApprovalResponseTemplate.objects.filter(id=template.id).first()
+        )
+
+        # Test non-superuser access restrictions
+        self.client.logout()
+        self.client.login(
+            username=self.user4.username, password="test"
+        )  # non-superuser
+
+        # Non-superuser shouldn't be able to create a template
+        resp = self.client.post(
+            reverse("templates-list"),
+            {"title": "Template", "content": "This should not exist"},
+            content_type="application/json",
+        )
+        self.assertEqual(resp.status_code, 403)
+
+
+class HealthTestCase(TestCase):
+    def test_health(self):
+        url = reverse("health")
+        resp = self.client.get(url)
+        self.assertEqual(resp.status_code, 200)
+        self.assertEqual(resp.data, {"message": "OK"})
diff --git a/frontend/.babelrc b/frontend/.babelrc
deleted file mode 100644
index 2781700cf..000000000
--- a/frontend/.babelrc
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  "presets": ["next/babel"],
-  "plugins": ["istanbul"],
-  "env": {
-    "production": {
-      "plugins": [["styled-components", { "ssr": true }]]
-    },
-    "development": {
-      "plugins": ["babel-plugin-styled-components"]
-    }
-  }
-}
diff --git a/frontend/.swcrc b/frontend/.swcrc
new file mode 100644
index 000000000..823bd0b8e
--- /dev/null
+++ b/frontend/.swcrc
@@ -0,0 +1,9 @@
+{
+  "jsc": {
+    "experimental": {
+      "plugins": [
+        ["swc-plugin-coverage-instrument", {}]
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/frontend/components/ClubCard.tsx b/frontend/components/ClubCard.tsx
index 85541a845..4587aee50 100644
--- a/frontend/components/ClubCard.tsx
+++ b/frontend/components/ClubCard.tsx
@@ -100,7 +100,9 @@ const ClubCard = ({ club, fullWidth }: ClubCardProps): ReactElement => {
   const { name, active, approved, subtitle, tags, enables_subscription, code } =
     club
   const img = club.image_url
-  const textDescription = shorten(subtitle || 'This club has no description.')
+  const textDescription = shorten(
+    subtitle || 'This club has not provided a mission statement.',
+  )
 
   return (
     <CardWrapper className={fullWidth ? '' : 'column is-half-desktop'}>
@@ -116,14 +118,6 @@ const ClubCard = ({ club, fullWidth }: ClubCardProps): ReactElement => {
               {!active && (
                 <InactiveTag className="tag is-rounded">Inactive</InactiveTag>
               )}
-              {approved === null && (
-                <InactiveTag className="tag is-rounded">
-                  Pending Approval
-                </InactiveTag>
-              )}
-              {approved === false && (
-                <InactiveTag className="tag is-rounded">Rejected</InactiveTag>
-              )}
               <TagGroup tags={tags} />
             </div>
             {img && (
diff --git a/frontend/components/ClubDisplay.tsx b/frontend/components/ClubDisplay.tsx
index 7bbafa6d2..c1c75922b 100644
--- a/frontend/components/ClubDisplay.tsx
+++ b/frontend/components/ClubDisplay.tsx
@@ -1,9 +1,10 @@
 import { ReactElement, useEffect } from 'react'
+import Masonry from 'react-masonry-css'
 import styled from 'styled-components'
 
 import ClubCard from '../components/ClubCard'
 import ClubTableRow from '../components/ClubTableRow'
-import { mediaMaxWidth, SM } from '../constants/measurements'
+import { getNumberFromPx, mediaMaxWidth, SM } from '../constants/measurements'
 import { Club, Tag } from '../types'
 
 const ClubTableRowWrapper = styled.div`
@@ -14,6 +15,26 @@ const ClubTableRowWrapper = styled.div`
   }
 `
 
+const StyledMasonry = styled(Masonry)`
+  display: flex;
+  width: auto;
+  margin: -12px;
+
+  .masonry-column {
+    background-clip: padding-box;
+  }
+
+  .masonry-column > div {
+    width: 100% !important;
+  }
+
+  ${mediaMaxWidth(SM)} {
+    .masonry-column {
+      width: 100% !important;
+    }
+  }
+`
+
 type ClubDisplayProps = {
   displayClubs: Club[]
   tags: Tag[]
@@ -50,11 +71,18 @@ const ClubDisplay = ({
 
   if (display === 'cards') {
     return (
-      <div className="columns is-multiline is-desktop is-tablet">
+      <StyledMasonry
+        breakpointCols={{
+          default: 2,
+          [getNumberFromPx(SM)]: 1,
+        }}
+        className="masonry-grid"
+        columnClassName="masonry-column"
+      >
         {displayClubs.map((club) => (
           <ClubCard key={club.code} club={club} />
         ))}
-      </div>
+      </StyledMasonry>
     )
   }
 
diff --git a/frontend/components/ClubEditPage.tsx b/frontend/components/ClubEditPage.tsx
index 462fa002f..72445905e 100644
--- a/frontend/components/ClubEditPage.tsx
+++ b/frontend/components/ClubEditPage.tsx
@@ -26,6 +26,7 @@ import {
   School,
   StudentType,
   Tag,
+  UserInfo,
   VisitType,
   Year,
 } from '../types'
@@ -42,6 +43,7 @@ import {
   SHOW_ORG_MANAGEMENT,
   SITE_NAME,
 } from '../utils/branding'
+import AdminNoteCard from './ClubEditPage/AdminNoteCard'
 import AdvisorCard from './ClubEditPage/AdvisorCard'
 import AnalyticsCard from './ClubEditPage/AnalyticsCard'
 import ApplicationsCard from './ClubEditPage/ApplicationsCard'
@@ -74,6 +76,7 @@ type ClubFormProps = {
   tags: Tag[]
   studentTypes: StudentType[]
   tab?: string | null
+  userInfo?: UserInfo
 }
 
 const ClubForm = ({
@@ -85,6 +88,7 @@ const ClubForm = ({
   studentTypes,
   clubId,
   tab,
+  userInfo,
 }: ClubFormProps): ReactElement => {
   const [club, setClub] = useState<Club | null>(null)
   const [isEdit, setIsEdit] = useState<boolean>(typeof clubId !== 'undefined')
@@ -237,6 +241,15 @@ const ClubForm = ({
           />
         ),
       },
+      ...(userInfo !== undefined && userInfo.is_superuser
+        ? [
+            {
+              name: 'notes',
+              label: 'Administrator Notes',
+              content: <AdminNoteCard club={club} />,
+            },
+          ]
+        : []),
       {
         name: 'member',
         label: OBJECT_TAB_MEMBERSHIP_LABEL,
diff --git a/frontend/components/ClubEditPage/AdminNoteCard.tsx b/frontend/components/ClubEditPage/AdminNoteCard.tsx
new file mode 100644
index 000000000..810eba016
--- /dev/null
+++ b/frontend/components/ClubEditPage/AdminNoteCard.tsx
@@ -0,0 +1,46 @@
+import { Field } from 'formik'
+import moment from 'moment-timezone'
+import { ReactElement } from 'react'
+
+import { Club } from '../../types'
+import { TextField } from '../FormComponents'
+import { ModelForm } from '../ModelForm'
+import BaseCard from './BaseCard'
+
+type AdminNoteCardProps = {
+  club: Club
+}
+
+export default function AdminNoteCard({
+  club,
+}: AdminNoteCardProps): ReactElement {
+  const noteTableFields = [
+    { label: 'Author', name: 'creator' },
+    { label: 'Note', name: 'content' },
+    {
+      label: 'Created On',
+      name: 'created_at',
+      converter: (field) => moment(field).format('MMMM Do, YYYY'),
+    },
+  ]
+
+  return (
+    <BaseCard title="Notes">
+      <p className="mb-3">
+        Below is a list of notes about {club.name}. These notes are only visible
+        to site administrators.
+      </p>
+      <ModelForm
+        baseUrl={`/clubs/${club.code}/adminnotes/`}
+        fields={
+          <>
+            <Field name="content" as={TextField} type="textarea" required />
+          </>
+        }
+        tableFields={noteTableFields}
+        searchableColumns={['content']}
+        noun="Note"
+      />
+    </BaseCard>
+  )
+}
diff --git a/frontend/components/ClubEditPage/AdvisorCard.tsx b/frontend/components/ClubEditPage/AdvisorCard.tsx
index b8762a60f..7be10aae5 100644
--- a/frontend/components/ClubEditPage/AdvisorCard.tsx
+++ b/frontend/components/ClubEditPage/AdvisorCard.tsx
@@ -1,17 +1,10 @@
 import { Field } from 'formik'
 import { ReactElement, useState } from 'react'
-import styled from 'styled-components'
 
-import { RED } from '../../constants/colors'
-import { Advisor, Club } from '../../types'
-import {
-  OBJECT_NAME_SINGULAR,
-  SHOW_MEMBERS,
-  SITE_ID,
-  SITE_NAME,
-} from '../../utils/branding'
+import { Advisor, AdvisorVisibilityType, Club } from '../../types'
+import { OBJECT_NAME_SINGULAR, SHOW_MEMBERS } from '../../utils/branding'
 import { Text } from '../common'
-import { CheckboxField, TextField } from '../FormComponents'
+import { SelectField, TextField } from '../FormComponents'
 import { ModelForm } from '../ModelForm'
 import BaseCard from './BaseCard'
 
@@ -20,10 +13,20 @@ type Props = {
   validateAdvisors?: (valid: boolean) => void
 }
 
-const RequireText = styled.p`
-  color: ${RED};
-  margin-top: 1rem;
-`
+export const VISIBILITY_TYPES = [
+  {
+    value: AdvisorVisibilityType.AdminOnly,
+    label: 'Admin Only',
+  },
+  {
+    value: AdvisorVisibilityType.Students,
+    label: 'Students (Logged In)',
+  },
+  {
+    value: AdvisorVisibilityType.All,
+    label: 'All (Public, External)',
+  },
+]
 
 export default function AdvisorCard({
   club,
@@ -39,7 +42,7 @@ export default function AdvisorCard({
     if (newAdvisors.length) {
       validCount = newAdvisors.filter(
         (advisor) =>
-          (advisor._status || !advisor._errorMessage) && advisor.public,
+          (advisor._status || !advisor._errorMessage) && advisor.visibility,
       ).length
     }
     if (validateAdvisors) {
@@ -56,19 +59,25 @@ export default function AdvisorCard({
       <Field name="email" as={TextField} type="email" />
       <Field name="phone" as={TextField} />
       <Field
-        name="public"
-        as={CheckboxField}
+        name="visibility"
         label="Show contact information to the public?"
+        as={SelectField}
+        required
+        choices={VISIBILITY_TYPES}
+        serialize={({ value }) => value}
+        isMulti={false}
+        valueDeserialize={(val) =>
+          VISIBILITY_TYPES.find((x) => x.value === val)
+        }
       />
     </>
   )
 
   return (
     <>
-      <BaseCard title="Public Points of Contact">
+      <BaseCard title="Points of Contact">
         <Text>
-          Provide points of contact for your organization. These public points
-          of contact will be shown to the public.
+          Provide points of contact for your organization.
           {SHOW_MEMBERS && (
             <>
               {' '}
@@ -82,32 +91,8 @@ export default function AdvisorCard({
         <ModelForm
           onUpdate={updateAdvisors}
           baseUrl={`/clubs/${club.code}/advisors/`}
-          listParams="&public=true"
-          defaultObject={{ public: true }}
-          initialData={club.advisor_set.filter(
-            ({ public: isPublic }) => isPublic,
-          )}
-          fields={fields}
-        />
-        {SITE_ID === 'fyh' && advisorsCount <= 0 && (
-          <RequireText>
-            * At least one public point of contact is required.
-          </RequireText>
-        )}
-      </BaseCard>
-
-      <BaseCard title="Internal Points of Contact">
-        <Text>
-          These private points of contact will be shown to only {SITE_NAME}{' '}
-          administrators.
-        </Text>
-        <ModelForm
-          baseUrl={`/clubs/${club.code}/advisors/`}
-          listParams="&public=false"
-          defaultObject={{ public: false }}
-          initialData={club.advisor_set.filter(
-            ({ public: isPublic }) => !isPublic,
-          )}
+          defaultObject={{ public: AdvisorVisibilityType.Students }}
+          initialData={club.advisor_set}
           fields={fields}
         />
       </BaseCard>
diff --git a/frontend/components/ClubEditPage/ApplicationsPage.tsx b/frontend/components/ClubEditPage/ApplicationsPage.tsx
index fae8f65bf..99d353ebc 100644
--- a/frontend/components/ClubEditPage/ApplicationsPage.tsx
+++ b/frontend/components/ClubEditPage/ApplicationsPage.tsx
@@ -418,8 +418,8 @@ export default function ApplicationsPage({
         if (applications.length !== 0) {
           setApplications(applications)
           setCurrentApplication({
-            ...applications[0],
-            name: format_app_name(applications[0]),
+            ...applications[applications.length - 1],
+            name: format_app_name(applications[applications.length - 1]),
           })
         }
       })
@@ -532,7 +532,7 @@ export default function ApplicationsPage({
         CSV.
       </Text>
       <Select
-        options={applications.map((application) => {
+        options={applications.toReversed().map((application) => {
           return {
             ...application,
             value: application.id,
diff --git a/frontend/components/ClubEditPage/ClubEditCard.tsx b/frontend/components/ClubEditPage/ClubEditCard.tsx
index 3b1061226..58e380d88 100644
--- a/frontend/components/ClubEditPage/ClubEditCard.tsx
+++ b/frontend/components/ClubEditPage/ClubEditCard.tsx
@@ -31,14 +31,18 @@ import {
   FORM_TAG_DESCRIPTION,
   FORM_TARGET_DESCRIPTION,
   MEMBERSHIP_ROLE_NAMES,
+  NEW_APPROVAL_QUEUE_ENABLED,
   OBJECT_NAME_SINGULAR,
   OBJECT_NAME_TITLE_SINGULAR,
   OBJECT_TAB_ADMISSION_LABEL,
+  REAPPROVAL_QUEUE_ENABLED,
   SHOW_RANK_ALGORITHM,
   SITE_ID,
   SITE_NAME,
 } from '../../utils/branding'
-import { Checkbox, CheckboxLabel, Contact, Text } from '../common'
+import { ModalContent } from '../ClubPage/Actions'
+import { LiveBanner, LiveSub, LiveTitle } from '../ClubPage/LiveEventsDialog'
+import { Checkbox, CheckboxLabel, Contact, Modal, Text } from '../common'
 import {
   CheckboxField,
   CheckboxTextField,
@@ -147,6 +151,49 @@ const Card = ({
     </div>
   )
 }
+interface EmailModalProps {
+  closeModal: () => void
+  email: string
+  setEmail: (inp: string) => void
+  confirmSubmission: () => void
+}
+
+const EmailModal = ({
+  closeModal,
+  email,
+  setEmail,
+  confirmSubmission,
+}: EmailModalProps): ReactElement => {
+  return (
+    <Modal
+      width={'450px'}
+      show={true}
+      closeModal={closeModal}
+      marginBottom={false}
+    >
+      <div className="card-content mb-2">
+        <Text className="has-text-danger">
+          This email will be visible to the public.
+          <br />
+          We recommend that you don’t use a personal email, and instead use a
+          club email.
+        </Text>
+        <Field
+          name="email"
+          value={email}
+          onChange={(e) => setEmail(e.target.value)}
+          className="input mb-5"
+          style={{ maxWidth: '350px' }}
+        ></Field>
+        <div>
+          <button onClick={confirmSubmission} className="button is-primary">
+            Confirm
+          </button>
+        </div>
+      </div>
+    </Modal>
+  )
+}
 
 /**
  * Remove fields in an object that are not part of a whitelist.
@@ -186,6 +233,7 @@ export default function ClubEditCard({
   isEdit,
   onSubmit = () => Promise.resolve(undefined),
 }: ClubEditCardProps): ReactElement {
+  const [showRankModal, setShowRankModal] = useState<boolean>(false)
   const [showTargetFields, setShowTargetFields] = useState<boolean>(
     !!(
       club.target_majors?.length ||
@@ -224,6 +272,8 @@ export default function ClubEditCard({
     ),
   )
 
+  const [emailModal, showEmailModal] = useState<boolean>(false)
+
   const submit = (data, { setSubmitting, setStatus }): Promise<void> => {
     const photo = data.image
     if (photo !== null) {
@@ -394,12 +444,65 @@ export default function ClubEditCard({
     {
       name: 'General',
       type: 'group',
+      description: (
+        <div className="mb-4">
+          <a onClick={() => setShowRankModal(true)}>
+            How does filling out this information affect your club?
+          </a>
+          <Modal
+            show={showRankModal}
+            closeModal={() => setShowRankModal(false)}
+            marginBottom={false}
+            width="80%"
+          >
+            <ModalContent className="content mb-4">
+              <h2>How we calculate club rankings</h2>
+              <hr />
+              <h5>
+                The following positively affects your club's ranking in homepage
+                search results:
+              </h5>
+              <ul>
+                <li>
+                  Upcoming events with filled out name, description, and image
+                </li>
+                <li>Upcoming, open applications for membership</li>
+                <li>
+                  Having at least 3 active officers, plus a bonus for any
+                  additional non-officer member on the platform
+                </li>
+                <li>
+                  Having between 3 and 7 useful tags (please email <Contact />{' '}
+                  if none apply)
+                </li>
+                <li>
+                  Posting a public (non-personal) contact email and 2 or more
+                  social links
+                </li>
+                <li>
+                  Having a club logo image uploaded and subtitle filled out
+                </li>
+                <li>
+                  Filling out a club mission with images and detail (rewarded up
+                  to 1000 words)
+                </li>
+                <li>Displaying 3 or more student testimonials (experiences)</li>
+                <li>Filling out the {FIELD_PARTICIPATION_LABEL} section</li>
+                <li>
+                  Updating the club listing recently (within the last 8 months)
+                </li>
+              </ul>
+            </ModalContent>
+          </Modal>
+        </div>
+      ),
       fields: [
         {
           name: 'name',
           type: 'text',
           required: true,
           label: `${OBJECT_NAME_TITLE_SINGULAR} Name`,
+          disabled: !REAPPROVAL_QUEUE_ENABLED,
           help: isEdit ? (
             <>
               If you would like to change your {OBJECT_NAME_SINGULAR} URL in
@@ -442,10 +545,11 @@ export default function ClubEditCard({
         },
         {
           name: 'description',
+          label: 'Club Mission',
           required: true,
-          help: `Changing this field will require reapproval from the ${APPROVAL_AUTHORITY}.`,
-          placeholder: `Type your ${OBJECT_NAME_SINGULAR} description here!`,
+          placeholder: `Type your ${OBJECT_NAME_SINGULAR} mission here!`,
           type: 'html',
+          hidden: !REAPPROVAL_QUEUE_ENABLED,
         },
         {
           name: 'tags',
@@ -461,6 +565,7 @@ export default function ClubEditCard({
           accept: 'image/*',
           type: 'image',
           label: `${OBJECT_NAME_TITLE_SINGULAR} Logo`,
+          disabled: !REAPPROVAL_QUEUE_ENABLED,
         },
         {
           name: 'size',
@@ -790,6 +895,7 @@ export default function ClubEditCard({
 
   const creationDefaults = {
     subtitle: '',
+    email: '',
     email_public: true,
     accepting_members: false,
     size: CLUB_SIZES[0].value,
@@ -811,9 +917,57 @@ export default function ClubEditCard({
     : creationDefaults
 
   return (
-    <Formik initialValues={initialValues} onSubmit={submit} enableReinitialize>
-      {({ dirty, isSubmitting }) => (
+    <Formik
+      initialValues={initialValues}
+      onSubmit={(values, actions) =>
+        submit({ ...values, emailOverride: false }, actions)
+      }
+      enableReinitialize
+      validate={(values) => {
+        const errors: { email?: string } = {}
+        if (values.email.includes('upenn.edu') && !emailModal) {
+          showEmailModal(true)
+          errors.email = 'Please confirm your email'
+        }
+        return errors
+      }}
+      validateOnChange={false}
+      validateOnBlur={false}
+    >
+      {({ dirty, isSubmitting, setFieldValue, submitForm, values }) => (
         <Form>
+          {emailModal && (
+            <EmailModal
+              closeModal={() => showEmailModal(false)}
+              email={values.email}
+              setEmail={(newEmail) => setFieldValue('email', newEmail)}
+              confirmSubmission={() => {
+                showEmailModal(false)
+                submitForm()
+              }}
+            />
+          )}
+          {!REAPPROVAL_QUEUE_ENABLED && (
+            <LiveBanner>
+              <LiveTitle>Queue Closed for Summer Break</LiveTitle>
+              <LiveSub>
+                No edits to existing clubs or applications for new clubs will be
+                submitted for review to OSA.
+              </LiveSub>
+            </LiveBanner>
+          )}
+          {!NEW_APPROVAL_QUEUE_ENABLED &&
+            REAPPROVAL_QUEUE_ENABLED &&
+            !isEdit && (
+              <LiveBanner>
+                <LiveTitle>Queue Closed for New Clubs</LiveTitle>
+                <LiveSub>
+                  Submissions for new clubs are closed for the time being.
+                  Please reach out to the Office of Student Affairs at
+                  vpul-pennosa@pobox.upenn.edu with any questions.
+                </LiveSub>
+              </LiveBanner>
+            )}
           <FormStyle isHorizontal>
             {fields.map(({ name, description, fields, hidden }, i) => {
               if (hidden) {
@@ -871,7 +1025,11 @@ export default function ClubEditCard({
               )
             })}
             <button
-              disabled={!dirty || isSubmitting}
+              disabled={
+                !dirty ||
+                isSubmitting ||
+                (!NEW_APPROVAL_QUEUE_ENABLED && !isEdit)
+              }
               type="submit"
               className="button is-primary is-large"
             >
diff --git a/frontend/components/ClubEditPage/EventsCard.tsx b/frontend/components/ClubEditPage/EventsCard.tsx
index 1a741614f..cdd765d91 100644
--- a/frontend/components/ClubEditPage/EventsCard.tsx
+++ b/frontend/components/ClubEditPage/EventsCard.tsx
@@ -401,7 +401,7 @@ const CreateContainer = styled.div`
   align-items: center;
 `
 
-const CreateTickets = ({ event }: { event: ClubEvent }) => {
+const CreateTickets = ({ event, club }: { event: ClubEvent; club: Club }) => {
   const [show, setShow] = useState(false)
   const showModal = () => setShow(true)
   const hideModal = () => setShow(false)
@@ -431,7 +431,11 @@ const CreateTickets = ({ event }: { event: ClubEvent }) => {
           closeModal={hideModal}
           marginBottom={false}
         >
-          <TicketsModal event={event} onSuccessfulSubmit={hideModal} />
+          <TicketsModal
+            club={club}
+            event={event}
+            onSuccessfulSubmit={hideModal}
+          />
         </Modal>
       )}
     </CreateContainer>
@@ -476,7 +480,7 @@ export default function EventsCard({ club }: EventsCardProps): ReactElement {
         }}
       />
       <Line />
-      <CreateTickets event={event} />
+      <CreateTickets event={event} club={club} />
       <Line />
       <div ref={eventDetailsRef}>
         <EventPreview event={event} />
diff --git a/frontend/components/ClubEditPage/TicketsModal.tsx b/frontend/components/ClubEditPage/TicketsModal.tsx
index 7eec51535..12553498e 100644
--- a/frontend/components/ClubEditPage/TicketsModal.tsx
+++ b/frontend/components/ClubEditPage/TicketsModal.tsx
@@ -14,7 +14,7 @@ import {
 } from '../../constants/colors'
 import { BORDER_RADIUS } from '../../constants/measurements'
 import { BODY_FONT } from '../../constants/styles'
-import { ClubEvent } from '../../types'
+import { Club, ClubEvent } from '../../types'
 import { doApiRequest } from '../../utils'
 import CoverPhoto from '../EventPage/CoverPhoto'
 
@@ -56,6 +56,7 @@ const notify = (
 
 type TicketItemProps = {
   ticket: Ticket
+  club: Club
   onChange?: (ticket: Ticket) => void
   onDelete?: () => void
   deletable: boolean
@@ -63,6 +64,7 @@ type TicketItemProps = {
 
 const TicketItem: React.FC<TicketItemProps> = ({
   ticket: propTicket,
+  club,
   onChange,
   onDelete,
   deletable,
@@ -122,7 +124,7 @@ const TicketItem: React.FC<TicketItemProps> = ({
             className="input"
             value={ticket.price ?? ''}
             placeholder="Ticket Price"
-            disabled={!TICKETING_PAYMENT_ENABLED}
+            disabled={!TICKETING_PAYMENT_ENABLED && !club.beta}
             onChange={(e) => {
               const price = e.target.value
               setTicket({ ...ticket, price })
@@ -221,9 +223,11 @@ type Ticket = {
 
 const TicketsModal = ({
   event,
+  club,
   onSuccessfulSubmit,
 }: {
   event: ClubEvent
+  club: Club
   onSuccessfulSubmit: () => void
 }): ReactElement => {
   const { large_image_url, image_url, club_name, name, id } = event
@@ -333,6 +337,7 @@ const TicketsModal = ({
             <TicketItem
               key={index}
               ticket={ticket}
+              club={club}
               deletable={tickets.length > 1}
               onChange={(newTicket) => {
                 setTickets((t) =>
diff --git a/frontend/components/ClubPage/Actions.tsx b/frontend/components/ClubPage/Actions.tsx
index a0ee5afeb..8d0073f48 100644
--- a/frontend/components/ClubPage/Actions.tsx
+++ b/frontend/components/ClubPage/Actions.tsx
@@ -12,12 +12,7 @@ import { mediaMaxWidth, mediaMinWidth, SM } from '~/constants'
 
 import { BORDER, MEDIUM_GRAY, WHITE } from '../../constants/colors'
 import { CLUB_APPLY_ROUTE, CLUB_EDIT_ROUTE } from '../../constants/routes'
-import {
-  Club,
-  ClubApplicationRequired,
-  QuestionAnswer,
-  UserInfo,
-} from '../../types'
+import { Club, ClubApplicationRequired, QuestionAnswer } from '../../types'
 import { apiCheckPermission, apiSetLikeStatus, doApiRequest } from '../../utils'
 import {
   FIELD_PARTICIPATION_LABEL,
@@ -85,7 +80,7 @@ const ActionButton = styled.a`
 
 type ActionsProps = {
   club: Club
-  userInfo: UserInfo
+  authenticated: boolean
   style?: CSSProperties
   className?: string
   updateRequests: (code: string) => Promise<void>
@@ -294,7 +289,7 @@ const Actions = ({
                 updateRequests={updateRequests}
               />
             )}
-          {SHOW_APPLICATIONS && !isMembershipOpen && club.accepting_members && (
+          {SHOW_APPLICATIONS && !isMembershipOpen && !inClub && (
             <Link
               legacyBehavior
               href={CLUB_APPLY_ROUTE()}
diff --git a/frontend/components/ClubPage/ClubApprovalDialog.tsx b/frontend/components/ClubPage/ClubApprovalDialog.tsx
index 6ce2c975f..cace58b99 100644
--- a/frontend/components/ClubPage/ClubApprovalDialog.tsx
+++ b/frontend/components/ClubPage/ClubApprovalDialog.tsx
@@ -1,9 +1,12 @@
+import { EmotionJSX } from '@emotion/react/types/jsx-namespace'
+import moment from 'moment-timezone'
 import { useRouter } from 'next/router'
 import { ReactElement, useEffect, useState } from 'react'
+import Select from 'react-select'
 
 import { CLUB_SETTINGS_ROUTE } from '~/constants/routes'
 
-import { Club, ClubFair, MembershipRank, UserInfo } from '../../types'
+import { Club, ClubFair, MembershipRank, Template, UserInfo } from '../../types'
 import {
   apiCheckPermission,
   doApiRequest,
@@ -17,6 +20,7 @@ import {
   SITE_NAME,
 } from '../../utils/branding'
 import { Contact, Icon, Modal, Text, TextQuote } from '../common'
+import { Chevron } from '../DropdownFilter'
 import { ModalContent } from './Actions'
 
 type Props = {
@@ -29,13 +33,114 @@ type ConfirmParams = {
   message: ReactElement | string
 }
 
+type HistoricItem = {
+  approved: boolean | null
+  approved_on: string | null
+  approved_by: string | null
+  approved_comment: string | null
+  history_date: string
+}
+
+const ClubHistoryDropdown = ({ history }: { history: HistoricItem[] }) => {
+  const [active, setActive] = useState<boolean>(false)
+  const [reason, setReason] = useState<string | null>(null)
+  const getReason = (item: HistoricItem): EmotionJSX.Element | string => {
+    return item.approved_comment ? (
+      item.approved_comment.length > 100 ? (
+        <span
+          style={{
+            cursor: 'pointer',
+            textDecoration: 'underline',
+          }}
+          onClick={() => setReason(item.approved_comment)}
+        >
+          View Reason
+        </span>
+      ) : (
+        item.approved_comment
+      )
+    ) : (
+      'No reason provided'
+    )
+  }
+  return (
+    <>
+      <div
+        style={{
+          cursor: 'pointer',
+        }}
+        className="mt-2"
+        onClick={() => setActive(!active)}
+      >
+        {active ? 'Hide' : 'Show'} History
+        <Chevron
+          name="chevron-down"
+          alt="toggle dropdown"
+          open={active}
+          color="inherit"
+          className="ml-1"
+        />
+      </div>
+      <Modal
+        show={reason !== null}
+        closeModal={() => setReason(null)}
+        marginBottom={false}
+        width="80%"
+      >
+        <ModalContent>{reason}</ModalContent>
+      </Modal>
+      {active && (
+        <div style={{ maxHeight: '300px', overflowY: 'auto' }}>
+          {history.map((item, i) => (
+            <div
+              key={i}
+              className="mt-2"
+              style={{
+                fontSize: '70%',
+              }}
+            >
+              {item.approved === true ? (
+                <TextQuote className="py-0">
+                  <b>Approved</b> by <b>{item.approved_by}</b> on{' '}
+                  {moment(item.history_date)
+                    .tz('America/New_York')
+                    .format('LLL')}{' '}
+                  - {getReason(item)}
+                </TextQuote>
+              ) : item.approved === false ? (
+                <TextQuote className="py-0">
+                  <b>Rejected</b> by <b>{item.approved_by}</b> on{' '}
+                  {moment(item.history_date)
+                    .tz('America/New_York')
+                    .format('LLL')}{' '}
+                  - {getReason(item)}
+                </TextQuote>
+              ) : (
+                <TextQuote className="py-0">
+                  <b>Submitted for re-approval</b> on{' '}
+                  {moment(item.history_date)
+                    .tz('America/New_York')
+                    .format('LLL')}
+                </TextQuote>
+              )}
+            </div>
+          ))}
+        </div>
+      )}
+    </>
+  )
+}
+
 const ClubApprovalDialog = ({ club }: Props): ReactElement | null => {
   const router = useRouter()
   const year = getCurrentSchoolYear()
+  const [history, setHistory] = useState<HistoricItem[]>([])
   const [comment, setComment] = useState<string>(club.approved_comment || '')
   const [loading, setLoading] = useState<boolean>(false)
   const [confirmModal, setConfirmModal] = useState<ConfirmParams | null>(null)
   const [fairs, setFairs] = useState<ClubFair[]>([])
+  const [templates, setTemplates] = useState<Template[]>([])
+  const [selectedTemplates, setSelectedTemplates] = useState<Template[]>([])
 
   const canApprove = apiCheckPermission('clubs.approve_club')
   const seeFairStatus = apiCheckPermission('clubs.see_fair_status')
@@ -54,7 +159,37 @@ const ClubApprovalDialog = ({ club }: Props): ReactElement | null => {
         .then((resp) => resp.json())
         .then(setFairs)
     }
-  }, [])
+
+    if (canApprove) {
+      doApiRequest('/templates/?format=json')
+        .then((resp) => resp.json())
+        .then(setTemplates)
+    }
+
+    if (isOfficer || canApprove) {
+      doApiRequest(`/clubs/${club.code}/history/?format=json`)
+        .then((resp) => resp.json())
+        .then((data) => {
+          // Get last version of club for each change in approved status
+          const lastVersions: HistoricItem[] = []
+
+          for (let i = data.length - 1; i >= 0; i--) {
+            const item = data[i]
+            const lastItem = lastVersions[lastVersions.length - 1]
+
+            if (item.approved !== lastItem?.approved || !lastItem) {
+              lastVersions.push(item)
+            }
+          }
+          lastVersions.reverse() // Avoids O(n^2) of unshift() method
+          setHistory(lastVersions)
+        })
+    }
+
+    setComment(
+      selectedTemplates.map((template) => template.content).join('\n\n'),
+    )
+  }, [selectedTemplates])
 
   return (
     <>
@@ -117,6 +252,7 @@ const ClubApprovalDialog = ({ club }: Props): ReactElement | null => {
           >
             <Icon name="x" /> Revoke Approval
           </button>
+          <ClubHistoryDropdown history={history} />
         </div>
       )}
       {(club.active || canDeleteClub) && club.approved !== true ? (
@@ -200,6 +336,43 @@ const ClubApprovalDialog = ({ club }: Props): ReactElement | null => {
                     className="textarea mb-4"
                     placeholder="Enter approval or rejection notes here! Your notes will be emailed to the requester when you approve or reject this request."
                   ></textarea>
+                  <div className="field is-grouped mb-3">
+                    <div className="control is-expanded">
+                      <Select
+                        isMulti
+                        isClearable
+                        placeholder="Select templates"
+                        options={templates.map((template) => ({
+                          value: template.id,
+                          label: template.title,
+                          content: template.content,
+                          author: template.author,
+                        }))}
+                        onChange={(selectedOptions) => {
+                          if (selectedOptions) {
+                            const selected = selectedOptions.map((option) => ({
+                              id: option.value,
+                              title: option.label,
+                              content: option.content,
+                              author: option.author,
+                            }))
+                            setSelectedTemplates(selected)
+                          } else {
+                            setSelectedTemplates([])
+                          }
+                        }}
+                      />
+                    </div>
+                    <div className="control">
+                      <button
+                        className="button is-primary"
+                        onClick={() => router.push('/admin/templates')}
+                      >
+                        <Icon name="edit" />
+                        Edit Templates
+                      </button>
+                    </div>
+                  </div>
                 </>
               )}
               <div className="buttons">
@@ -316,6 +489,7 @@ const ClubApprovalDialog = ({ club }: Props): ReactElement | null => {
               </button>
             </>
           )}
+          <ClubHistoryDropdown history={history} />
         </div>
       ) : null}
       {(seeFairStatus || isOfficer) && fairs.length > 0 && (
diff --git a/frontend/components/ClubPage/Description.tsx b/frontend/components/ClubPage/Description.tsx
index 7ce9b30e1..48f1d7231 100644
--- a/frontend/components/ClubPage/Description.tsx
+++ b/frontend/components/ClubPage/Description.tsx
@@ -20,7 +20,7 @@ type Props = {
 const Description = ({ club }: Props): ReactElement => (
   <Wrapper>
     <div style={{ width: '100%' }}>
-      <StrongText>Description</StrongText>
+      <StrongText>Club Mission</StrongText>
       <div
         className="content"
         dangerouslySetInnerHTML={{
diff --git a/frontend/components/ClubPage/EventCarousel.tsx b/frontend/components/ClubPage/EventCarousel.tsx
new file mode 100644
index 000000000..0916d1087
--- /dev/null
+++ b/frontend/components/ClubPage/EventCarousel.tsx
@@ -0,0 +1,100 @@
+import 'swiper/css'
+import 'swiper/css/navigation'
+import 'swiper/css/pagination'
+
+import React, { useState } from 'react'
+import styled from 'styled-components'
+import { Navigation, Pagination } from 'swiper/modules'
+import { Swiper, SwiperSlide } from 'swiper/react'
+
+import { ClubEvent } from '../../types'
+import { Icon, StrongText } from '../common'
+import Modal from '../common/Modal'
+import EventCard from '../EventPage/EventCard'
+import EventModal from '../EventPage/EventModal'
+
+const Arrow = styled.div`
+  z-index: 100;
+  opacity: 0.6;
+  transition-duration: 300ms;
+  cursor: pointer;
+  padding: 12px;
+  position: absolute;
+  top: 50%;
+
+  &:hover {
+    opacity: 1;
+  }
+`
+
+const CarouselWrapper = styled.div`
+  padding: 10px 35px;
+`
+
+type EventsProps = {
+  data: ClubEvent[]
+}
+
+const EventCarousel = ({ data }: EventsProps) => {
+  const [show, setShow] = useState(false)
+  const [modalData, setModalData] = useState<ClubEvent>()
+
+  const showModal = (entry: ClubEvent) => {
+    setModalData(entry)
+    setShow(true)
+  }
+  const hideModal = () => setShow(false)
+
+  return (
+    <div
+      style={{
+        position: 'relative',
+      }}
+    >
+      <div>
+        <StrongText className="mb-0">Events</StrongText>
+        <small>Click on an event to get more details.</small>
+      </div>
+
+      <CarouselWrapper>
+        <Swiper
+          modules={[Navigation, Pagination]}
+          spaceBetween={50}
+          navigation={{ nextEl: '.arrow-left', prevEl: '.arrow-right' }}
+          draggable
+          scrollbar={{ draggable: true }}
+          centeredSlides
+          centeredSlidesBounds
+          slidesPerView="auto"
+        >
+          {data.map((entry, index) => (
+            <SwiperSlide
+              key={index}
+              style={{
+                maxWidth: '250px',
+                cursor: 'pointer',
+                boxSizing: 'border-box',
+              }}
+              onClick={() => showModal(entry)}
+            >
+              <EventCard event={entry} key={index} />
+            </SwiperSlide>
+          ))}
+        </Swiper>
+      </CarouselWrapper>
+      <Arrow className="arrow-left" style={{ right: '-15px' }}>
+        <Icon name="chevron-right" size={'2.2rem'} />
+      </Arrow>
+      <Arrow className=" arrow-right" style={{ left: '-15px' }}>
+        <Icon name="chevron-left" size={'2.2rem'} />
+      </Arrow>
+      {show && (
+        <Modal show={show} closeModal={hideModal} marginBottom={false}>
+          {modalData && <EventModal event={modalData} />}
+        </Modal>
+      )}
+    </div>
+  )
+}
+
+export default EventCarousel
diff --git a/frontend/components/ClubPage/Events.tsx b/frontend/components/ClubPage/Events.tsx
index 1c5422c18..daff4c790 100644
--- a/frontend/components/ClubPage/Events.tsx
+++ b/frontend/components/ClubPage/Events.tsx
@@ -6,8 +6,6 @@ import { DARK_BLUE, HOVER_GRAY, PURPLE, WHITE } from '../../constants/colors'
 import { M2, M3 } from '../../constants/measurements'
 import { ClubEvent, ClubEventType } from '../../types'
 import { Card, Icon, StrongText } from '../common'
-import Modal from '../common/Modal'
-import EventModal from '../EventPage/EventModal'
 
 type EventsProps = {
   data: ClubEvent[]
@@ -31,7 +29,8 @@ const Wrapper = styled.div`
   margin-bottom: 0.5rem;
   display: flex;
   cursor: pointer;
-  border-radius: 3px;
+  border-radius: 8px;
+  padding: 2px;
 
   &:hover {
     background-color: ${HOVER_GRAY};
@@ -70,11 +69,6 @@ const Event = ({ entry }: { entry: ClubEvent }): ReactElement => {
           </SmallParagraph>
         </div>
       </Wrapper>
-      {show && (
-        <Modal show={show} closeModal={hideModal} marginBottom={false}>
-          <EventModal event={entry} showDetailsButton={false} />
-        </Modal>
-      )}
     </>
   )
 }
diff --git a/frontend/components/ClubPage/LiveEventsDialog.tsx b/frontend/components/ClubPage/LiveEventsDialog.tsx
index f43eab824..c73fad89a 100644
--- a/frontend/components/ClubPage/LiveEventsDialog.tsx
+++ b/frontend/components/ClubPage/LiveEventsDialog.tsx
@@ -13,7 +13,7 @@ import { ClubEventType, MembershipRank } from '../../types'
 import { doApiRequest, useSetting } from '../../utils'
 import { FAIR_NAME, MEMBERSHIP_ROLE_NAMES } from '../../utils/branding'
 
-const LiveBanner = styled.div`
+export const LiveBanner = styled.div`
   padding: 20px;
   border-radius: 5px;
   background-image: radial-gradient(
@@ -37,13 +37,13 @@ const LiveBanner = styled.div`
   margin-bottom: 10px;
 `
 
-const LiveTitle = styled.div`
+export const LiveTitle = styled.div`
   font-size: ${M4};
   font-weight: bold;
   color: white;
 `
 
-const LiveSub = styled.div`
+export const LiveSub = styled.div`
   margin-top: -3px;
   margin-bottom: 3px;
   font-size: ${M2};
@@ -73,13 +73,16 @@ const WhiteButton = styled.a`
 interface LiveEventsDialogProps {
   isPreFair: boolean
   isFair: boolean
+  isVirtual: boolean
 }
 
 const LiveEventsDialog = ({
   isPreFair,
   isFair,
+  isVirtual,
 }: LiveEventsDialogProps): ReactElement | null => {
   const fairName = useSetting('FAIR_NAME')
+  const fairContactEmail = useSetting('FAIR_CONTACT')
   const [liveEventCount, setLiveEventCount] = useState<number>(0)
 
   useEffect(() => {
@@ -99,7 +102,7 @@ const LiveEventsDialog = ({
 
   return (
     <LiveBanner>
-      {isPreFair && (
+      {isPreFair && isVirtual && (
         <Link
           legacyBehavior
           href={FAIR_OFFICER_GUIDE_ROUTE}
@@ -111,7 +114,7 @@ const LiveEventsDialog = ({
           </WhiteButton>
         </Link>
       )}
-      {isFair && (
+      {isFair && isVirtual && (
         <Link href={LIVE_EVENTS} as={LIVE_EVENTS} passHref legacyBehavior>
           <WhiteButton>See Live Events</WhiteButton>
         </Link>
@@ -121,8 +124,8 @@ const LiveEventsDialog = ({
       </Link>
       <LiveTitle>{fairName}</LiveTitle>
       <LiveSub>
-        {liveEventCount === 0 ? (
-          `Get ready for the virtual ${FAIR_NAME} fair! If you need help registering email contact@pennclubs.com`
+        {liveEventCount === 0 || !isVirtual ? (
+          `Get ready for the ${isVirtual ? 'virtual ' : ''}${FAIR_NAME} fair! For any issues with registering, email ${fairContactEmail}.`
         ) : (
           <>
             {liveEventCount}{' '}
diff --git a/frontend/components/ClubPage/QuestionList.tsx b/frontend/components/ClubPage/QuestionList.tsx
index bd684856d..e1a7c7518 100644
--- a/frontend/components/ClubPage/QuestionList.tsx
+++ b/frontend/components/ClubPage/QuestionList.tsx
@@ -39,7 +39,6 @@ const QuestionList = ({
   sortBy,
 }: QuestionListProps): ReactElement => {
   const [formSubmitted, setFormSubmitted] = useState(false)
-
   const handleSubmit = (data, { setSubmitting, setStatus }) => {
     doApiRequest(`/clubs/${code}/questions/?format=json`, {
       method: 'POST',
diff --git a/frontend/components/DisplayButtons.tsx b/frontend/components/DisplayButtons.tsx
index d45633674..3f44181cd 100644
--- a/frontend/components/DisplayButtons.tsx
+++ b/frontend/components/DisplayButtons.tsx
@@ -74,10 +74,10 @@ const DisplayButtons = ({
       <Link href="/create" className="button is-small is-primary">
         <Icon
           name="plus"
-          alt={`create ${OBJECT_NAME_SINGULAR}`}
+          alt={`register ${OBJECT_NAME_SINGULAR}`}
           style={iconStylesDark}
         />
-        Add {OBJECT_NAME_TITLE_SINGULAR}
+        Register {OBJECT_NAME_TITLE_SINGULAR}
       </Link>
     )}
   </DisplayButtonsTag>
diff --git a/frontend/components/DropdownFilter.tsx b/frontend/components/DropdownFilter.tsx
index 3805f3089..0ad45306b 100644
--- a/frontend/components/DropdownFilter.tsx
+++ b/frontend/components/DropdownFilter.tsx
@@ -73,17 +73,17 @@ const TableContainer = styled.div`
   }
 `
 
-const Chevron = styled(Icon)<{ open?: boolean }>`
+export const Chevron = styled(Icon)<{ open?: boolean; color?: string }>`
   cursor: pointer;
-  color: ${CLUBS_GREY};
+  color: ${({ color }) => color ?? CLUBS_GREY};
   transform: rotate(0deg) translateY(0);
   transition: transform ${ANIMATION_DURATION}ms ease;
-  ${({ open }) => open && 'transform: rotate(180deg) translateY(-4px);'}
+  ${({ open }) => open && 'transform: rotate(180deg);'}
 
   ${mediaMaxWidth(MD)} {
     margin-top: 0.1em !important;
     margin-left: 0.1em !important;
-    color: ${LIGHT_GRAY};
+    color: ${({ color }) => color ?? LIGHT_GRAY};
     ${({ open }) => open && 'transform: rotate(180deg)'}
   }
 `
diff --git a/frontend/components/EmbedOption.tsx b/frontend/components/EmbedOption.tsx
index 2f4e8c9d1..54af4f99a 100644
--- a/frontend/components/EmbedOption.tsx
+++ b/frontend/components/EmbedOption.tsx
@@ -184,9 +184,8 @@ const EmbedOption = (props: Props): ReactElement => {
           <h1>Embed Content</h1>
           <p>
             You can use this tool to embed multimedia content into your club
-            description. If you run into any issues using the tool, please
-            contact <Contact />. Here are examples of some of the things you can
-            embed.
+            mission. If you run into any issues using the tool, please contact{' '}
+            <Contact />. Here are examples of some of the things you can embed.
           </p>
           <div className="content mb-3">
             <ul>
diff --git a/frontend/components/EventPage/EventModal.tsx b/frontend/components/EventPage/EventModal.tsx
index 34ffd7ad1..322258880 100644
--- a/frontend/components/EventPage/EventModal.tsx
+++ b/frontend/components/EventPage/EventModal.tsx
@@ -1,18 +1,14 @@
 import Color from 'color'
-import Link from 'next/link'
 import React, { ReactElement, useEffect, useState } from 'react'
 import TimeAgo from 'react-timeago'
 import styled from 'styled-components'
 
-import { Icon } from '../../components/common'
-import { CLUB_ROUTE, ZOOM_BLUE } from '../../constants'
+import { Icon, Text } from '../../components/common'
+import { ZOOM_BLUE } from '../../constants'
 import { MEDIUM_GRAY } from '../../constants/colors'
 import { ClubEvent } from '../../types'
 import { doApiRequest } from '../../utils'
-import {
-  OBJECT_NAME_SINGULAR,
-  OBJECT_NAME_TITLE_SINGULAR,
-} from '../../utils/branding'
+import { OBJECT_NAME_SINGULAR } from '../../utils/branding'
 import { ClubName, EventLink, EventName } from './common'
 import CoverPhoto from './CoverPhoto'
 import DateInterval from './DateInterval'
@@ -121,39 +117,6 @@ const LiveEventUpdater = ({
   return null
 }
 
-/**
- * Buttons that allow you to bookmark and subscribe to a club.
- */
-const ActionButtons = ({
-  club: code,
-  isTicketEvent,
-  setDisplayTicketModal,
-  ticketCount,
-  userHasTickets,
-}): ReactElement | null => {
-  return (
-    <>
-      {isTicketEvent && (
-        <>
-          <Link href="/">
-            <button
-              disabled={ticketCount === 0}
-              className="button is-success is-small"
-            >
-              {ticketCount === 0 && !userHasTickets
-                ? ' SOLD OUT'
-                : userHasTickets
-                  ? ' View Tickets'
-                  : ' Get Tickets'}{' '}
-              <Icon name="credit-card" className="ml-2" />
-            </button>
-          </Link>
-        </>
-      )}
-    </>
-  )
-}
-
 type LiveStatsData = {
   attending: number
   attended: number
@@ -196,31 +159,27 @@ export const LiveStats = ({
 
 const EventModal = (props: {
   event: ClubEvent
-  showDetailsButton?: boolean
   onLinkClicked?: () => void
 }): ReactElement => {
-  const { event, showDetailsButton, onLinkClicked } = props
+  const { event, onLinkClicked } = props
   const {
     large_image_url,
     image_url,
-    club,
     club_name,
     start_time,
     end_time,
-    // ticketed,
+    ticketed,
     name,
     url,
     description,
+    id,
   } = event
-  const ticketed = true
   const [userCount, setUserCount] = useState<LiveStatsData | null>(null)
-  const [ticketCount, setTicketCount] = useState<number | null>(null)
+  const [tickets, setTickets] = useState<Record<
+    string,
+    { total: number; available: number; price: number }
+  > | null>(null)
   const [userHasTickets, setUserHasTickets] = useState<boolean | null>(null)
-  const [displayTicketModal, setDisplayTicketModal] = useState<boolean>(false)
-  const [availableTickets, setAvailableTickets] = useState<Array<JSON> | null>(
-    null,
-  )
-
   const now = new Date()
   const startDate = new Date(start_time)
   const endDate = new Date(end_time)
@@ -235,18 +194,26 @@ const EventModal = (props: {
           setUserCount(resp)
         })
     }
-    // TODO: CHANGE TO event.ticketed instead of true when that is added
     if (ticketed) {
-      setTicketCount(0) // TODO: CHANGE BACK TO 0
       doApiRequest(`/events/${event.id}/tickets/`)
         .then((resp) => resp.json())
         .then((resp) => {
-          if (resp.available) {
-            setAvailableTickets(resp.available)
-            for (let i = 0; i < resp.available.length; i++) {
-              setTicketCount(ticketCount + resp.available[i].count)
-            }
-          }
+          const ticketMap = resp.totals.reduce(
+            (acc, cur) => ({
+              ...acc,
+              [cur.type]: {
+                total: cur.count,
+                available:
+                  resp.available.find((t) => t.type === cur.type)?.count ?? 0,
+                price: cur.price,
+              },
+            }),
+            {},
+          ) as Record<
+            string,
+            { total: number; available: number; price: number }
+          >
+          setTickets(ticketMap)
         })
       setUserHasTickets(false)
       doApiRequest(`/tickets/`)
@@ -264,7 +231,7 @@ const EventModal = (props: {
 
   useEffect(refreshLiveData, [])
 
-  return !displayTicketModal ? (
+  return (
     <ModalContainer>
       <CoverPhoto
         image={large_image_url ?? image_url}
@@ -307,43 +274,27 @@ const EventModal = (props: {
           ))}{' '}
         {userCount != null && <LiveStats stats={userCount} />}
         <StyledDescription contents={description} />
-        {showDetailsButton !== false && event.club != null && (
-          <div className="is-clearfix">
-            <div className="buttons is-pulled-right">
-              {club != null && (
-                <ActionButtons
-                  club={club}
-                  isTicketEvent={ticketed}
-                  setDisplayTicketModal={setDisplayTicketModal}
-                  ticketCount={ticketCount}
-                  userHasTickets={userHasTickets}
-                />
-              )}
-              <Link
-                legacyBehavior
-                href={CLUB_ROUTE()}
-                as={CLUB_ROUTE(event.club)}
-              >
-                <a
-                  className="button is-link is-small"
-                  onClick={(e) => {
-                    if (!event.club) {
-                      e.preventDefault()
-                    }
-                  }}
-                >
-                  See {OBJECT_NAME_TITLE_SINGULAR} Details{' '}
-                  <Icon name="chevrons-right" className="ml-2" />
-                </a>
-              </Link>
-            </div>
+        {tickets &&
+          Object.entries(tickets).map(([type, counts]) => (
+            <Text key={type}>
+              {type}: {counts.available} tickets available / {counts.total}{' '}
+              total
+            </Text>
+          ))}
+        <div className="is-clearfix">
+          <div className="buttons is-pulled-right">
+            {userHasTickets && (
+              <a className="button is-secondary" href="/settings/#Tickets">
+                Owned Tickets
+              </a>
+            )}
+            <a className="button is-primary" href={`/events/${id}`}>
+              Event Page
+            </a>
           </div>
-        )}
+        </div>
       </EventDetails>
     </ModalContainer>
-  ) : (
-    // TODO: THIS IS NOTHING RN, SHOULD DISPLAY ALL AVAILABLE TICKETS
-    <ModalContainer>""</ModalContainer>
   )
 }
 
diff --git a/frontend/components/EventPage/HappeningNow.tsx b/frontend/components/EventPage/HappeningNow.tsx
index 52d97b434..555c402fa 100644
--- a/frontend/components/EventPage/HappeningNow.tsx
+++ b/frontend/components/EventPage/HappeningNow.tsx
@@ -16,13 +16,13 @@ const blink = keyframes`
 `
 
 const HappeningNowContainer = styled.p<{
-  urgent?: boolean
+  $urgent?: boolean
   floatRight?: boolean
 }>`
   font-size: 14px;
   font-weight: 500;
   ${({ floatRight }) => (floatRight ? 'float: right;' : '')}
-  ${({ urgent }) => (urgent ? UrgentText : '')}
+  ${({ $urgent }) => ($urgent ? UrgentText : '')}
 `
 
 const HappeningNow = (props: {
diff --git a/frontend/components/FormComponents.tsx b/frontend/components/FormComponents.tsx
index c606c7e65..73a1c6178 100644
--- a/frontend/components/FormComponents.tsx
+++ b/frontend/components/FormComponents.tsx
@@ -643,6 +643,7 @@ export const FileField = useFieldWrapper(
     value,
     isImage = false,
     canDelete = false,
+    disabled = false,
   }: BasicFormField & AnyHack): ReactElement => {
     const { setFieldValue } = useFormikContext()
 
@@ -681,7 +682,12 @@ export const FileField = useFieldWrapper(
             <img style={{ maxWidth: 300 }} src={imageUrl} />
           ))}
         <div className="file">
-          <label className="file-label">
+          <label
+            className="file-label"
+            style={{
+              cursor: disabled ? 'not-allowed' : 'pointer',
+            }}
+          >
             <input
               className="file-input"
               type="file"
@@ -693,11 +699,14 @@ export const FileField = useFieldWrapper(
               }}
               onBlur={onBlur}
               placeholder={placeholder}
+              onClick={(e) => {
+                if (disabled) {
+                  e.preventDefault()
+                }
+              }}
             />
             <span className="file-cta">
-              <span className="file-label">
-                Choose a {isImage ? 'image' : 'file'}...
-              </span>
+              Choose a {isImage ? 'image' : 'file'}...
             </span>
           </label>
           {imageUrl && (canDelete || isImage) && (
diff --git a/frontend/components/Header/Burger.tsx b/frontend/components/Header/Burger.tsx
index a9c9d89b9..0a76bd1c1 100644
--- a/frontend/components/Header/Burger.tsx
+++ b/frontend/components/Header/Burger.tsx
@@ -4,6 +4,9 @@ const Burger = ({ toggle }: BurgerProps): ReactElement => (
   <a
     role="button"
     className="navbar-burger burger"
+    style={{
+      marginLeft: '8px',
+    }}
     aria-label="menu"
     aria-expanded="false"
     data-target="navbarBasicExample"
diff --git a/frontend/components/Header/Links.tsx b/frontend/components/Header/Links.tsx
index 4eb38f118..18e773019 100644
--- a/frontend/components/Header/Links.tsx
+++ b/frontend/components/Header/Links.tsx
@@ -55,6 +55,20 @@ const LoginButton = styled.a`
   }
 `
 
+export const MobileLoginButton = styled(LoginButton)`
+  display: none;
+  margin-right: 0;
+  ${mediaMaxWidth(MD)} {
+    display: inline-flex;
+  }
+`
+const DesktopLoginButton = styled(LoginButton)`
+  margin-left: 20px;
+  ${mediaMaxWidth(MD)} {
+    display: none;
+  }
+`
+
 const StyledLinkAnchor = styled.a`
   padding: ${LINK_MARGIN} 20px;
   color: ${BANNER_TEXT} !important;
@@ -116,13 +130,13 @@ const Links = ({ userInfo, authenticated, show }: Props): ReactElement => {
           FAQ
         </StyledLink>
         {authenticated === false && (
-          <LoginButton
+          <DesktopLoginButton
             className="button"
             href={`${LOGIN_URL}?next=${router.asPath}`}
             onClick={() => logEvent('login', 'click')}
           >
             Login
-          </LoginButton>
+          </DesktopLoginButton>
         )}
         {authenticated && userInfo && (
           <StyledLink href={SETTINGS_ROUTE}>
diff --git a/frontend/components/Header/index.tsx b/frontend/components/Header/index.tsx
index f38c68f8f..eb97e7ecc 100644
--- a/frontend/components/Header/index.tsx
+++ b/frontend/components/Header/index.tsx
@@ -1,7 +1,11 @@
 import Link from 'next/link'
+import { useRouter } from 'next/router'
 import { ReactElement, useEffect, useState } from 'react'
 import styled from 'styled-components'
 
+import { LOGIN_URL } from '~/utils'
+import { logEvent } from '~/utils/analytics'
+
 import { BANNER_BG, BANNER_TEXT, BORDER } from '../../constants/colors'
 import {
   ANIMATION_DURATION,
@@ -31,7 +35,7 @@ import {
 import Burger from './Burger'
 import Feedback from './Feedback'
 import Heading from './Head'
-import Links from './Links'
+import Links, { MobileLoginButton } from './Links'
 
 const Nav = styled.nav`
   height: ${NAV_HEIGHT};
@@ -164,6 +168,7 @@ const isHub = SITE_ID === 'fyh'
 
 const Header = ({ authenticated, userInfo }: HeaderProps): ReactElement => {
   const [show, setShow] = useState(false)
+  const router = useRouter()
 
   const toggle = () => setShow(!show)
 
@@ -184,7 +189,24 @@ const Header = ({ authenticated, userInfo }: HeaderProps): ReactElement => {
               <Title>{SITE_NAME}</Title>
             </LogoItem>
           </Link>
-          <Burger toggle={toggle} />
+          <div
+            style={{
+              flex: 1,
+              display: 'flex',
+              justifyContent: 'flex-end',
+            }}
+          >
+            {authenticated === false && (
+              <MobileLoginButton
+                className="button"
+                href={`${LOGIN_URL}?next=${router.asPath}`}
+                onClick={() => logEvent('login', 'click')}
+              >
+                Login
+              </MobileLoginButton>
+            )}
+            <Burger toggle={toggle} />
+          </div>
         </div>
         <Links userInfo={userInfo} authenticated={authenticated} show={show} />
       </Nav>
diff --git a/frontend/components/OrderInput.tsx b/frontend/components/OrderInput.tsx
index f12d8201e..d9536057a 100644
--- a/frontend/components/OrderInput.tsx
+++ b/frontend/components/OrderInput.tsx
@@ -19,7 +19,7 @@ import { Icon } from './common'
 const ORDERINGS = [
   {
     key: 'featured',
-    name: 'Featured',
+    name: 'Default',
     icon: 'star',
   },
   {
@@ -32,11 +32,6 @@ const ORDERINGS = [
     name: 'Bookmarks',
     icon: 'bookmark',
   },
-  {
-    key: 'random',
-    name: 'Random',
-    icon: 'shuffle',
-  },
 ]
 
 const OrderingWrapper = styled.div`
diff --git a/frontend/components/Settings/ClubTab.tsx b/frontend/components/Settings/ClubTab.tsx
index 114aa0717..08d8a0dd3 100644
--- a/frontend/components/Settings/ClubTab.tsx
+++ b/frontend/components/Settings/ClubTab.tsx
@@ -232,7 +232,7 @@ const ClubTab = ({
     <>
       <EmptyState name="button" />
       <Center>
-        <Text isGray>
+        <Text $isGray>
           No memberships yet! Browse {OBJECT_NAME_PLURAL}{' '}
           <Link href="/">here</Link>.
         </Text>
diff --git a/frontend/components/Settings/FairsTab.tsx b/frontend/components/Settings/FairsTab.tsx
index 168e76125..9cbd8a0c3 100644
--- a/frontend/components/Settings/FairsTab.tsx
+++ b/frontend/components/Settings/FairsTab.tsx
@@ -11,6 +11,7 @@ import {
 } from '../../utils/branding'
 import { Icon, Text } from '../common'
 import {
+  CheckboxField,
   DateTimeField,
   DynamicQuestionField,
   RichTextField,
@@ -84,6 +85,11 @@ const FairsTab = ({ fairs }: FairsTabProps): ReactElement => {
                 MembershipRank.Officer
               ].toLowerCase()}s on the registration page.`}
             />
+            <Field
+              name="virtual"
+              as={CheckboxField}
+              helpText="Check this box if your fair is virtual."
+            />
             <Field
               name="questions"
               as={DynamicQuestionField}
diff --git a/frontend/components/Settings/QueueTab.tsx b/frontend/components/Settings/QueueTab.tsx
index a4470a2e2..e8e7a7faf 100644
--- a/frontend/components/Settings/QueueTab.tsx
+++ b/frontend/components/Settings/QueueTab.tsx
@@ -1,10 +1,11 @@
 import Link from 'next/link'
 import { useRouter } from 'next/router'
 import { ReactElement, useEffect, useState } from 'react'
+import Select from 'react-select'
 import styled from 'styled-components'
 
 import { CLUB_ROUTE } from '../../constants'
-import { Club } from '../../types'
+import { Club, Template } from '../../types'
 import { apiCheckPermission, doApiRequest } from '../../utils'
 import {
   OBJECT_NAME_PLURAL,
@@ -21,6 +22,7 @@ type QueueTableModalProps = {
   closeModal: () => void
   bulkAction: (comment: string) => void
   isApproving: boolean
+  templates: Template[]
 }
 
 const QueueTableModal = ({
@@ -28,13 +30,23 @@ const QueueTableModal = ({
   closeModal,
   bulkAction,
   isApproving,
+  templates,
 }: QueueTableModalProps): ReactElement => {
   const [comment, setComment] = useState<string>('')
+  const [selectedTemplates, setSelectedTemplates] = useState<Template[]>([])
+
+  useEffect(() => {
+    setComment(
+      selectedTemplates.map((template) => template.content).join('\n\n'),
+    )
+  }, [selectedTemplates])
+
   return (
     <Modal
       show={show}
       closeModal={() => {
         setComment('')
+        setSelectedTemplates([])
         closeModal()
       }}
       marginBottom={false}
@@ -45,6 +57,36 @@ const QueueTableModal = ({
           notes will be emailed to the requesters when you{' '}
           {isApproving ? 'approve' : 'reject'} these requests.
         </div>
+        <Select
+          isMulti
+          isClearable
+          placeholder="Select templates"
+          value={selectedTemplates.map((template) => ({
+            value: template.id,
+            label: template.title,
+            content: template.content,
+            author: template.author,
+          }))}
+          options={templates.map((template) => ({
+            value: template.id,
+            label: template.title,
+            content: template.content,
+            author: template.author,
+          }))}
+          onChange={(selectedOptions) => {
+            if (selectedOptions) {
+              const selected = selectedOptions.map((option) => ({
+                id: option.value,
+                title: option.label,
+                content: option.content,
+                author: option.author,
+              }))
+              setSelectedTemplates(selected)
+            } else {
+              setSelectedTemplates([])
+            }
+          }}
+        />
         <textarea
           value={comment}
           onChange={(e) => setComment(e.target.value)}
@@ -52,7 +94,7 @@ const QueueTableModal = ({
           placeholder={`${isApproving ? 'approval' : 'rejection'} notes`}
         ></textarea>
         <button
-          className={`mb-2 button ${isApproving ? 'is-success' : 'is-danger'}`}
+          className={`mt-2 button ${isApproving ? 'is-success' : 'is-danger'}`}
           onClick={() => {
             closeModal()
             bulkAction(comment)
@@ -68,10 +110,11 @@ const QueueTableModal = ({
 
 type QueueTableProps = {
   clubs: Club[] | null
+  templates: Template[]
 }
 /* TODO: refactor with Table component when render and search
 functionality are disconnected */
-const QueueTable = ({ clubs }: QueueTableProps): ReactElement => {
+const QueueTable = ({ clubs, templates }: QueueTableProps): ReactElement => {
   const router = useRouter()
   const [selectedCodes, setSelectedCodes] = useState<string[]>([])
   const [showModal, setShowModal] = useState<boolean>(false)
@@ -106,6 +149,7 @@ const QueueTable = ({ clubs }: QueueTableProps): ReactElement => {
         closeModal={() => setShowModal(false)}
         bulkAction={bulkAction}
         isApproving={approve}
+        templates={templates}
       />
       <QueueTableHeader>
         <QueueTableHeaderText>
@@ -238,6 +282,7 @@ const QueueTab = (): ReactElement => {
   const [rejectedClubs, setRejectedClubs] = useState<Club[] | null>(null)
   const [inactiveClubs, setInactiveClubs] = useState<Club[] | null>(null)
   const [allClubs, setAllClubs] = useState<boolean[] | null>(null)
+  const [templates, setTemplates] = useState<Template[]>([])
   const canApprove = apiCheckPermission('clubs.approve_club')
 
   useEffect(() => {
@@ -261,6 +306,10 @@ const QueueTab = (): ReactElement => {
       doApiRequest('/clubs/directory/?format=json')
         .then((resp) => resp.json())
         .then((data) => setAllClubs(data.map((club: Club) => club.approved)))
+
+      doApiRequest('/templates/?format=json')
+        .then((resp) => resp.json())
+        .then(setTemplates)
     }
   }, [])
 
@@ -327,7 +376,7 @@ const QueueTab = (): ReactElement => {
           {approvedClubsCount} Approved {OBJECT_NAME_TITLE}
         </li>
       </ul>
-      <QueueTable clubs={pendingClubs} />
+      <QueueTable clubs={pendingClubs} templates={templates} />
       <SmallTitle>Other Clubs</SmallTitle>
       <div className="mt-3 mb-3">
         The table below shows a list of {OBJECT_NAME_PLURAL} that have been
diff --git a/frontend/components/Settings/RenewTab.tsx b/frontend/components/Settings/RenewTab.tsx
index c1413f2b2..0cbdea7a9 100644
--- a/frontend/components/Settings/RenewTab.tsx
+++ b/frontend/components/Settings/RenewTab.tsx
@@ -68,7 +68,7 @@ const RenewTab = ({ className }: ClubTabProps): ReactElement => {
         <>
           <EmptyState name="button" />
           <Center>
-            <Text isGray>
+            <Text $isGray>
               You are not listed as an officer for any {OBJECT_NAME_PLURAL} yet.
               If you would like to request access for an existing{' '}
               {OBJECT_NAME_SINGULAR}, please send your name, PennKey, and{' '}
diff --git a/frontend/components/Settings/TemplatesTab.tsx b/frontend/components/Settings/TemplatesTab.tsx
new file mode 100644
index 000000000..37c0959ea
--- /dev/null
+++ b/frontend/components/Settings/TemplatesTab.tsx
@@ -0,0 +1,59 @@
+import { Field } from 'formik'
+import moment from 'moment-timezone'
+import React, { ReactElement } from 'react'
+
+import { Template } from '../../types'
+import { OBJECT_NAME_SINGULAR } from '../../utils/branding'
+import { Text } from '../common'
+import { TextField } from '../FormComponents'
+import ModelForm from '../ModelForm'
+
+type TemplatesTabProps = {
+  templates: Template[]
+}
+
+export default function TemplatesTab({
+  templates,
+}: TemplatesTabProps): ReactElement {
+  return (
+    <>
+      <Text>
+        You can use this page to manage {OBJECT_NAME_SINGULAR} approval response
+        templates. Since your account has the required permissions, you are able
+        to view this page.
+      </Text>
+      <ModelForm
+        baseUrl="/templates/"
+        initialData={templates}
+        fields={
+          <>
+            <Field
+              name="title"
+              as={TextField}
+              required
+              helpText={`The unique title of the ${OBJECT_NAME_SINGULAR} approval response template. This will be shown in the template dropdown menu.`}
+            />
+            <Field name="content" as={TextField} type="textarea" required />
+          </>
+        }
+        tableFields={[
+          { name: 'title', label: 'Title' },
+          { name: 'content', label: 'Content' },
+          { name: 'author', label: 'Author' },
+          {
+            name: 'created_at',
+            label: 'Date Created',
+            converter: (field) => moment(field).format('MMMM Do, YYYY'),
+          },
+          {
+            name: 'updated_at',
+            label: 'Last Updated',
+            converter: (field) => moment(field).format('MMMM Do, YYYY'),
+          },
+        ]}
+        noun="Template"
+        confirmDeletion={true}
+      />
+    </>
+  )
+}
diff --git a/frontend/components/Settings/TicketsTab/index.tsx b/frontend/components/Settings/TicketsTab/index.tsx
index 650a2d6be..ba74c9884 100644
--- a/frontend/components/Settings/TicketsTab/index.tsx
+++ b/frontend/components/Settings/TicketsTab/index.tsx
@@ -257,7 +257,7 @@ const TicketsTab = ({ className, userInfo }: TicketsTabProps): ReactElement => {
     <>
       <EmptyState name="empty_cart" />
       <Center>
-        <Text isGray>
+        <Text $isGray>
           No tickets yet! Browse events to find tickets{' '}
           <Link href="/events">here</Link>.
         </Text>
diff --git a/frontend/components/Settings/WhartonApplicationCycles.tsx b/frontend/components/Settings/WhartonApplicationCycles.tsx
index 88bae600b..223b36ad0 100644
--- a/frontend/components/Settings/WhartonApplicationCycles.tsx
+++ b/frontend/components/Settings/WhartonApplicationCycles.tsx
@@ -13,16 +13,17 @@ import ModelForm from '../ModelForm'
 
 const fields = (
   <>
-    <Field name="name" as={TextField} />
-    <Field name="start_date" as={DateTimeField} />
-    <Field name="end_date" as={DateTimeField} />
-    <Field name="release_date" as={DateTimeField} />
+    <Field name="name" as={TextField} required />
+    <Field name="start_date" as={DateTimeField} required />
+    <Field name="end_date" as={DateTimeField} required />
+    <Field name="release_date" as={DateTimeField} required />
   </>
 )
 
 type Cycle = {
   name: string
   id: number | null
+  endDate: Date
 }
 
 type ClubOption = {
@@ -35,7 +36,8 @@ type ExtensionOption = {
   clubName: string
   endDate: Date
   exception?: boolean
-  changed: boolean
+  originalEndDate: Date
+  originalException: boolean
 }
 
 const ScrollWrapper = styled.div`
@@ -44,17 +46,24 @@ const ScrollWrapper = styled.div`
   height: 40vh;
 `
 
+type ClubApplicationWithClub = ClubApplication & {
+  club__name: string
+  club__code: number
+}
+
 const WhartonApplicationCycles = (): ReactElement => {
   const [editMembership, setEditMembership] = useState(false)
   const [membershipCycle, setMembershipCycle] = useState<Cycle>({
     name: '',
     id: null,
+    endDate: new Date(),
   })
 
   const [editExtensions, setEditExtensions] = useState(false)
   const [extensionsCycle, setExtensionsCycle] = useState<Cycle>({
     name: '',
     id: null,
+    endDate: new Date(),
   })
 
   const [clubsSelectedMembership, setClubsSelectedMembership] = useState<
@@ -81,7 +90,10 @@ const WhartonApplicationCycles = (): ReactElement => {
   const closeExtensionsModal = (): void => {
     setEditExtensions(false)
     // calculate clubs that have changed
-    const clubsToUpdate = clubsExtensions.filter((x) => x.changed)
+    const clubsToUpdate = clubsExtensions.filter(
+      (x) =>
+        x.originalEndDate !== x.endDate || x.originalException !== x.exception,
+    )
     // split into clubs with exceptions and clubs without
     const clubsExceptions = clubsToUpdate.filter((x) => x.exception)
     const clubsNoExceptions = clubsToUpdate.filter((x) => !x.exception)
@@ -147,18 +159,23 @@ const WhartonApplicationCycles = (): ReactElement => {
 
   useEffect(() => {
     if (extensionsCycle && extensionsCycle.id != null) {
-      doApiRequest(`/cycles/${extensionsCycle.id}/clubs?format=json`)
+      doApiRequest(
+        `/cycles/${extensionsCycle.id}/club_applications?format=json`,
+      )
         .then((resp) => resp.json())
         .then((data) => {
-          const initialOptions = data.map((club: ClubApplication) => {
-            return {
-              id: club.id,
-              clubName: club.name,
-              endDate: new Date(club.application_end_time),
-              exception: club.application_end_time_exception,
-              changed: false,
-            }
-          })
+          const initialOptions = data.map(
+            (application: ClubApplicationWithClub) => {
+              return {
+                id: application.id,
+                clubName: application.club__name,
+                endDate: new Date(application.application_end_time),
+                exception: application.application_end_time_exception,
+                originalEndDate: new Date(application.application_end_time),
+                originalException: application.application_end_time_exception,
+              }
+            },
+          )
           setClubsExtensions(initialOptions)
         })
     }
@@ -190,7 +207,11 @@ const WhartonApplicationCycles = (): ReactElement => {
             <button
               className="button is-info is-small"
               onClick={() => {
-                setMembershipCycle({ name: object.name, id: object.id })
+                setMembershipCycle({
+                  name: object.name,
+                  id: object.id,
+                  endDate: new Date(object.end_date),
+                })
                 setEditMembership(true)
                 setEditExtensions(false)
               }}
@@ -200,7 +221,11 @@ const WhartonApplicationCycles = (): ReactElement => {
             <button
               className="button is-info is-small"
               onClick={() => {
-                setExtensionsCycle({ name: object.name, id: object.id })
+                setExtensionsCycle({
+                  name: object.name,
+                  id: object.id,
+                  endDate: new Date(object.end_date),
+                })
                 setEditExtensions(true)
                 setEditMembership(false)
               }}
@@ -290,7 +315,6 @@ const WhartonApplicationCycles = (): ReactElement => {
                             selected={club.endDate}
                             onChange={(date) => {
                               club.endDate = date
-                              club.changed = true
                               setClubsExtensions([...clubsExtensions])
                             }}
                           />
@@ -299,7 +323,6 @@ const WhartonApplicationCycles = (): ReactElement => {
                           <Checkbox
                             onChange={(e) => {
                               club.exception = e.target.checked
-                              club.changed = true
                               setClubsExtensions([...clubsExtensions])
                             }}
                             checked={
@@ -320,9 +343,27 @@ const WhartonApplicationCycles = (): ReactElement => {
               className="button is-primary"
               style={{ position: 'absolute', bottom: 10, right: 10 }}
               onClick={closeExtensionsModal}
+              disabled={clubsExtensions.some(
+                (x) =>
+                  // For the case where we change end date without giving an exception to a club without one
+                  !x.exception &&
+                  !x.originalException &&
+                  x.endDate.getTime() !== extensionsCycle.endDate.getTime(),
+              )}
             >
               Submit
             </button>
+            {clubsExtensions.some(
+              (x) =>
+                !x.exception &&
+                !x.originalException &&
+                x.endDate.getTime() !== extensionsCycle.endDate.getTime(),
+            ) && (
+              <p className="is-danger">
+                To change the end date for a club, you must also check its
+                exception box.
+              </p>
+            )}
           </>
         )}
       </Modal>
diff --git a/frontend/components/Tickets/CartTickets.tsx b/frontend/components/Tickets/CartTickets.tsx
index f593b3097..49d8f640b 100644
--- a/frontend/components/Tickets/CartTickets.tsx
+++ b/frontend/components/Tickets/CartTickets.tsx
@@ -114,13 +114,14 @@ export interface CartTicketsProps {
  * @param tickets - Original array of tickets
  * @returns Array of tickets condensed into unique types
  */
-const combineTickets = (tickets: EventTicket[]): CountedEventTicket[] =>
+const combineTickets = (tickets: EventTicket[]): CountedEventTicketStatus[] =>
   Object.values(
     tickets.reduce(
       (acc, ticket) => ({
         ...acc,
         [`${ticket.event.id}_${ticket.type}`]: {
           ...ticket,
+          pendingEdit: false,
           count: (acc[`${ticket.event.id}_${ticket.type}`]?.count ?? 0) + 1,
         },
       }),
@@ -182,12 +183,17 @@ const useCheckout = (paid: boolean) => {
   }
 }
 
+interface CountedEventTicketStatus extends CountedEventTicket {
+  pendingEdit: boolean
+}
+
 const CartTickets: React.FC<CartTicketsProps> = ({ tickets, soldOut }) => {
   const navigate = useRouter()
-  const [removeModal, setRemoveModal] = useState<CountedEventTicket | null>(
-    null,
-  )
-  const [countedTickets, setCountedTickets] = useState<CountedEventTicket[]>([])
+  const [removeModal, setRemoveModal] =
+    useState<CountedEventTicketStatus | null>(null)
+  const [countedTickets, setCountedTickets] = useState<
+    CountedEventTicketStatus[]
+  >([])
 
   const atLeastOnePaid = tickets.some((ticket) => parseFloat(ticket.price) > 0)
   const {
@@ -207,7 +213,7 @@ const CartTickets: React.FC<CartTicketsProps> = ({ tickets, soldOut }) => {
       .forEach(
         (ticket) => {
           toast.error(
-            `${ticket.event.name} - ${ticket.type} is sold out and ${ticket.count} ticket${ticket.count && ticket.count > 1 ? 's have' : ' has'} been removed from your cart.`,
+            `${ticket.event.name} - ${ticket.type} is no longer available and ${ticket.count} ticket${ticket.count && ticket.count > 1 ? 's have' : ' has'} been removed from your cart.`,
             {
               style: { color: WHITE },
               autoClose: false,
@@ -225,12 +231,25 @@ const CartTickets: React.FC<CartTicketsProps> = ({ tickets, soldOut }) => {
     checkout()
   }
 
-  function handleUpdateTicket(ticket: CountedEventTicket, newCount?: number) {
+  function handleUpdateTicket(
+    ticket: CountedEventTicketStatus,
+    newCount?: number,
+    propogateCount?: (count: number) => void,
+  ) {
     let reqPromise
 
     if (!ticket.count || newCount === ticket.count) {
       return
     }
+
+    function flipPendingEdit(value: boolean) {
+      setCountedTickets(
+        countedTickets.map((t) =>
+          t.id === ticket.id ? { ...t, pendingEdit: value } : t,
+        ),
+      )
+    }
+    flipPendingEdit(true)
     if (newCount && newCount > ticket.count) {
       reqPromise = doApiRequest(`/events/${ticket.event.id}/add_to_cart/`, {
         method: 'POST',
@@ -268,8 +287,11 @@ const CartTickets: React.FC<CartTicketsProps> = ({ tickets, soldOut }) => {
           toast.error(res.detail, {
             style: { color: WHITE },
           })
+          propogateCount?.(ticket.count ?? 0)
+          flipPendingEdit(false)
           return
         }
+        flipPendingEdit(false)
         toast.success(res.detail)
         // TODO: a less naive approach to updating the cart
         setCountedTickets(
@@ -313,7 +335,7 @@ const CartTickets: React.FC<CartTicketsProps> = ({ tickets, soldOut }) => {
           }}
         >
           <Subtitle>Your cart is empty</Subtitle>
-          <Text isGray>
+          <Text $isGray>
             To add tickets to your cart, visit the event page and select the
             tickets you wish to purchase. If you believe this is an error,
             please contact support at
@@ -365,9 +387,9 @@ const CartTickets: React.FC<CartTicketsProps> = ({ tickets, soldOut }) => {
               ticket={ticket}
               hideActions
               removable
-              editable
-              onChange={(count) => {
-                handleUpdateTicket(ticket, count)
+              editable={!ticket.pendingEdit}
+              onChange={(count, propogateCount) => {
+                handleUpdateTicket(ticket, count, propogateCount)
               }}
               onRemove={() => {
                 setRemoveModal(ticket)
diff --git a/frontend/components/Tickets/PaymentForm.tsx b/frontend/components/Tickets/PaymentForm.tsx
index d45d5ebad..8333ace82 100644
--- a/frontend/components/Tickets/PaymentForm.tsx
+++ b/frontend/components/Tickets/PaymentForm.tsx
@@ -36,7 +36,7 @@ const Payment: React.FC<PaymentProps> = ({
   return (
     <>
       <Script
-        src="https://apitest.cybersource.com/up/v1/assets/0.15/SecureAcceptance.js"
+        src="https://api.cybersource.com/up/v1/assets/0.22.0/SecureAcceptance.js"
         async
         onLoad={onLoad}
       />
diff --git a/frontend/components/Tickets/TicketCard.tsx b/frontend/components/Tickets/TicketCard.tsx
index 2d88b6337..1986c919a 100644
--- a/frontend/components/Tickets/TicketCard.tsx
+++ b/frontend/components/Tickets/TicketCard.tsx
@@ -132,7 +132,8 @@ export const TicketCard = ({
   indexProps?: TicketCardIndexProps
 
   onRemove?: () => void
-  onChange?: (count: number) => void
+  // Optimistically update the ticket count but revert if server request fails
+  onChange?: (count: number, propogateCount: (number) => void) => void
   onClick?: () => void
 
   viewModal?: (type: ModalType) => void
@@ -196,7 +197,10 @@ export const TicketCard = ({
                 onKeyDown={(e) => {
                   if (e.key === 'Enter') {
                     setIsEditMode(false)
-                    onChange?.(parseInt(e.currentTarget.value ?? ticket.count))
+                    onChange?.(
+                      parseInt(e.currentTarget.value ?? ticket.count),
+                      setTicketCount,
+                    )
                   }
                 }}
               />
@@ -256,12 +260,12 @@ export const TicketCard = ({
                 css={css`
                   font-size: 12px;
                   position: absolute;
-                  right: 12px;
-                  top: 12px;
+                  right: 8px;
+                  top: 8px;
                 `}
               >
-                {datetimeData.dayDuration < 0 ? '-' : '+'}{' '}
-                {Math.abs(datetimeData.dayDuration)}
+                {datetimeData.dayDuration < 0 ? '-' : '+'}
+                {Math.abs(datetimeData.dayDuration)}d
               </div>
             )}
           </Title>
@@ -318,7 +322,7 @@ export const TicketCard = ({
           `}
           onClick={() => {
             if (isEditMode) {
-              onChange?.(ticketCount || ticket.count!)
+              onChange?.(ticketCount || ticket.count!, setTicketCount)
             }
             setIsEditMode(!isEditMode)
           }}
diff --git a/frontend/components/common/TagGroup.tsx b/frontend/components/common/TagGroup.tsx
index f8e117870..64d9a8045 100644
--- a/frontend/components/common/TagGroup.tsx
+++ b/frontend/components/common/TagGroup.tsx
@@ -14,26 +14,50 @@ function isBadge(tag): tag is Badge {
 export const TagGroup = ({ tags = [] }: TagGroupProps): ReactElement | null => {
   if (!tags || !tags.length) return null
 
+  // sometimes there will be duplicate badges, or a badge will end up with the same content as a tag
+  // when there are duplicate badges, choose one arbitrarily
+  // when there's a tag and a badge with the same content, render the badge
+
+  const uniqueTags = tags.reduce((acc, tag) => {
+    const key = isBadge(tag) ? tag.label : tag.name
+    if (!acc.has(key)) {
+      acc.set(key, tag)
+    } else if (isBadge(tag)) {
+      acc.set(key, tag)
+    }
+    return acc
+  }, new Map())
+
   return (
     <>
-      {tags.map((tag) =>
-        isBadge(tag) ? (
-          <DefaultTag
-            key={`${tag.id}-badge`}
-            color={tag.color}
-            className="tag is-rounded"
-          >
-            {tag.label}
-          </DefaultTag>
-        ) : (
-          <BlueTag
-            key={`${tag.id}-tag`}
-            className="tag is-rounded has-text-white"
-          >
-            {tag.name}
-          </BlueTag>
-        ),
-      )}
+      {Array.from(uniqueTags.values()) // display badges after tags
+        .sort((a, b) => {
+          if (isBadge(a) && !isBadge(b)) return 1
+          if (!isBadge(a) && isBadge(b)) return -1
+          if (isBadge(a) && isBadge(b)) {
+            // badges should be sorted by color
+            return a.color.localeCompare(b.color)
+          }
+          return 0
+        })
+        .map((tag) =>
+          isBadge(tag) ? (
+            <DefaultTag
+              key={`${tag.id}-badge`}
+              color={tag.color}
+              className="tag is-rounded"
+            >
+              {tag.label}
+            </DefaultTag>
+          ) : (
+            <BlueTag
+              key={`${tag.id}-tag`}
+              className="tag is-rounded has-text-white"
+            >
+              {tag.name}
+            </BlueTag>
+          ),
+        )}
     </>
   )
 }
diff --git a/frontend/components/common/Typography.tsx b/frontend/components/common/Typography.tsx
index ed6806d36..c3c467732 100644
--- a/frontend/components/common/Typography.tsx
+++ b/frontend/components/common/Typography.tsx
@@ -7,11 +7,11 @@ import {
   WHITE,
 } from '../../constants/colors'
 
-export const Text = styled.p<{ isGray?: boolean; color?: string }>`
+export const Text = styled.p<{ $isGray?: boolean; color?: string }>`
   font-size: 1rem;
   margin-bottom: 1rem;
   line-height: 1.5;
-  ${({ isGray }) => (isGray ? `color: ${CLUBS_GREY};` : '')}
+  ${({ $isGray }) => ($isGray ? `color: ${CLUBS_GREY};` : '')}
   ${({ color }) => (color ? `color: ${color};` : '')}
 `
 
diff --git a/frontend/constants/measurements.ts b/frontend/constants/measurements.ts
index f53f50eda..4a9492823 100644
--- a/frontend/constants/measurements.ts
+++ b/frontend/constants/measurements.ts
@@ -57,6 +57,8 @@ export const mediaMinWidth = (width: string): string =>
   `@media screen and (min-width: ${width})`
 export const mediaMaxWidth = (width: string): string =>
   `@media screen and (max-width: ${width})`
+export const getNumberFromPx = (px: string): number =>
+  parseInt(px.replace('px', ''), 10)
 
 export const SM = '768px'
 export const MD = '992px'
diff --git a/frontend/next.config.js b/frontend/next.config.js
index 54532dcf2..a03d87a74 100644
--- a/frontend/next.config.js
+++ b/frontend/next.config.js
@@ -6,6 +6,9 @@ module.exports = {
     })
     return config
   },
+  compiler: {
+    styledComponents: true,
+  },
   async rewrites() {
     return [
       {
diff --git a/frontend/package.json b/frontend/package.json
index 19a0e20f1..cf9c57252 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -11,11 +11,10 @@
     "@sentry/browser": "^7.101.1",
     "@sentry/node": "^7.101.1",
     "@svgr/webpack": "^8.1.0",
+    "@swc/core": "^1.9.3",
     "@types/moment-timezone": "^0.5.30",
     "@zeit/next-source-maps": "0.0.4-canary.1",
     "babel-loader": "^9.1.3",
-    "babel-plugin-istanbul": "^6.1.1",
-    "babel-plugin-styled-components": "^2.1.4",
     "babel-preset-next": "^1.2.0",
     "body-parser": "^1.20.2",
     "body-scroll-lock": "^4.0.0-beta.0",
@@ -49,6 +48,7 @@
     "react-ga": "^3.3.1",
     "react-lazy-load": "^4.0.1",
     "react-linkify": "^1.0.0-alpha",
+    "react-masonry-css": "^1.0.16",
     "react-places-autocomplete": "^7.3.0",
     "react-select": "^5.8.0",
     "react-table": "^7.8.0",
@@ -58,6 +58,7 @@
     "react-vis": "^1.12.1",
     "showdown": "^2.1.0",
     "styled-components": "^6.1.8",
+    "swiper": "^11.1.12",
     "use-places-autocomplete": "^4.0.1",
     "yarn": "^1.22.21"
   },
@@ -111,10 +112,12 @@
     "local-ssl-proxy": "^2.0.5",
     "nyc": "^15.0.0",
     "prettier": "^3.2.5",
+    "swc-plugin-coverage-instrument": "^0.0.25",
     "typescript": "^5.3.3",
     "wait-on": "^7.2.0"
   },
   "engines": {
     "node": "^20.0.0"
-  }
-}
\ No newline at end of file
+  },
+  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
+}
diff --git a/frontend/pages/admin/[[...slug]].tsx b/frontend/pages/admin/[[...slug]].tsx
index 22cc78d1e..c6341b37e 100644
--- a/frontend/pages/admin/[[...slug]].tsx
+++ b/frontend/pages/admin/[[...slug]].tsx
@@ -11,15 +11,17 @@ import { NextPageContext } from 'next'
 import { useRouter } from 'next/router'
 import React, { ReactElement } from 'react'
 import renderPage from 'renderPage'
-import { Badge, ClubFair, Report, Tag } from 'types'
+import { Badge, ClubFair, Report, Tag, Template } from 'types'
 import { apiCheckPermission, doBulkLookup } from 'utils'
 
+import TemplatesTab from '~/components/Settings/TemplatesTab'
 import { ADMIN_ROUTE, BG_GRADIENT, WHITE } from '~/constants'
 
 function AdminPage({
   userInfo,
   tags,
   badges,
+  templates,
   clubfairs,
   scripts,
   fair,
@@ -58,6 +60,11 @@ function AdminPage({
       label: 'Approval Queue',
       content: () => <QueueTab />,
     },
+    {
+      name: 'templates',
+      label: 'Approval Templates',
+      content: () => <TemplatesTab templates={templates} />,
+    },
     {
       name: 'fair',
       label: 'Fair Management',
@@ -99,6 +106,7 @@ function AdminPage({
 type BulkResp = {
   tags: Tag[]
   badges: Badge[]
+  templates: Template[]
   clubfairs: ClubFair[]
   scripts: any[]
   reports: Report[]
@@ -109,6 +117,7 @@ AdminPage.getInitialProps = async (ctx: NextPageContext) => {
     [
       'tags',
       ['badges', '/badges/?all=true&format=json'],
+      'templates',
       'clubfairs',
       'scripts',
       ['reports', '/reports/?format=json'],
diff --git a/frontend/pages/club/[club]/alumni.tsx b/frontend/pages/club/[club]/alumni.tsx
index 6a4ebdea3..f33a6c7b5 100644
--- a/frontend/pages/club/[club]/alumni.tsx
+++ b/frontend/pages/club/[club]/alumni.tsx
@@ -4,19 +4,28 @@ import { NextPageContext } from 'next'
 import Link from 'next/link'
 import { ReactElement } from 'react'
 import renderPage from 'renderPage'
-import { Club } from 'types'
+import { Club, UserInfo } from 'types'
 import { doApiRequest } from 'utils'
 
+import AuthPrompt from '~/components/common/AuthPrompt'
 import { CLUB_ROUTE, PROFILE_ROUTE } from '~/constants'
 
 type AlumniPageProps = {
   club: Club
+  userInfo?: UserInfo
   alumni:
     | { [year: string]: { name: string; username: string | null }[] }
     | { detail: string }
 }
 
-const AlumniPage = ({ club, alumni }: AlumniPageProps): ReactElement => {
+const AlumniPage = ({
+  club,
+  alumni,
+  userInfo,
+}: AlumniPageProps): ReactElement => {
+  if (!userInfo) {
+    return <AuthPrompt />
+  }
   return (
     <>
       <ClubMetadata club={club} />
diff --git a/frontend/pages/club/[club]/application/[application]/index.tsx b/frontend/pages/club/[club]/application/[application]/index.tsx
index b06390f0d..69e8bd1ee 100644
--- a/frontend/pages/club/[club]/application/[application]/index.tsx
+++ b/frontend/pages/club/[club]/application/[application]/index.tsx
@@ -5,7 +5,8 @@ import { Container, Icon, Title } from 'components/common'
 import { Field, Form, Formik } from 'formik'
 import moment from 'moment'
 import { NextPageContext } from 'next'
-import React, { ReactElement, useState } from 'react'
+import { ReactElement, useState } from 'react'
+import TimeAgo from 'react-timeago'
 import renderPage from 'renderPage'
 import styled from 'styled-components'
 import {
@@ -16,6 +17,7 @@ import {
 } from 'types'
 import { doApiRequest } from 'utils'
 
+import AuthPrompt from '~/components/common/AuthPrompt'
 import { SelectField, TextField } from '~/components/FormComponents'
 
 type ApplicationPageProps = {
@@ -106,11 +108,30 @@ export function formatQuestionType(
 }
 
 const ApplicationPage = ({
+  userInfo,
   club,
   application,
   questions,
   initialValues,
-}: ApplicationPageProps): ReactElement => {
+}): ReactElement => {
+  if (!userInfo) {
+    return <AuthPrompt />
+  }
+
+  // Second condition will be replaced with perms check or question nullity check once backend is updated
+  // eslint-disable-next-line no-constant-condition
+  if (new Date() < new Date(application.application_start_time) && false) {
+    return (
+      <Container paddingTop>
+        <Title>Application Not Open</Title>
+        <p>
+          This application is not open yet. Please check back{' '}
+          <TimeAgo date={application.application_start_time} />.
+        </p>
+      </Container>
+    )
+  }
+
   const [errors, setErrors] = useState<string | null>(null)
   const [saved, setSaved] = useState<boolean>(false)
   const [currentCommittee, setCurrentCommittee] = useState<{
diff --git a/frontend/pages/club/[club]/apply.tsx b/frontend/pages/club/[club]/apply.tsx
index 2cd034cba..9b2fc8203 100644
--- a/frontend/pages/club/[club]/apply.tsx
+++ b/frontend/pages/club/[club]/apply.tsx
@@ -14,6 +14,7 @@ import {
 } from 'components/common'
 import { NextPageContext } from 'next'
 import Link from 'next/link'
+import { useRouter } from 'next/router'
 import { ReactElement, useState } from 'react'
 import TimeAgo from 'react-timeago'
 import renderPage from 'renderPage'
@@ -35,6 +36,7 @@ type Props = {
 }
 
 const ApplyPage = ({ club, applications }: Props): ReactElement => {
+  const router = useRouter()
   const [updatedIsRequest, setUpdatedIsRequest] = useState<boolean>(
     club.is_request,
   )
@@ -170,13 +172,15 @@ const ApplyPage = ({ club, applications }: Props): ReactElement => {
                   {new Date(app.result_release_time).toLocaleString()} (
                   <TimeAgo date={app.result_release_time} />)
                 </div>
-                <a
-                  href={app.external_url}
-                  rel="noopener noreferrer"
+                <button
+                  onClick={() => {
+                    router.push(app.external_url)
+                  }}
                   className="button is-success mt-3"
+                  disabled={new Date() < new Date(app.application_start_time)}
                 >
                   <Icon name="edit" /> Apply
-                </a>
+                </button>
               </div>
             ))}
             <Subtitle>Already a member?</Subtitle>
diff --git a/frontend/pages/club/[club]/index.tsx b/frontend/pages/club/[club]/index.tsx
index 0c14b3b4b..b6bd88f39 100644
--- a/frontend/pages/club/[club]/index.tsx
+++ b/frontend/pages/club/[club]/index.tsx
@@ -3,7 +3,6 @@ import { DesktopActions, MobileActions } from 'components/ClubPage/Actions'
 import AdvisorList from 'components/ClubPage/AdvisorList'
 import ClubApprovalDialog from 'components/ClubPage/ClubApprovalDialog'
 import Description from 'components/ClubPage/Description'
-import Events from 'components/ClubPage/Events'
 import FilesList from 'components/ClubPage/FilesList'
 import Header from 'components/ClubPage/Header'
 import InfoBox from 'components/ClubPage/InfoBox'
@@ -43,6 +42,7 @@ import {
   SITE_NAME,
 } from 'utils/branding'
 
+import EventCarousel from '~/components/ClubPage/EventCarousel'
 import { CLUB_ALUMNI_ROUTE, CLUB_ORG_ROUTE } from '~/constants'
 import { CLUBS_RED, SNOW, WHITE } from '~/constants/colors'
 import { M0, M2, M3 } from '~/constants/measurements'
@@ -178,15 +178,23 @@ const ClubPage = ({
     testimonials,
     signature_events: signatureEvents,
   } = club
-
   return (
     <WideContainer background={SNOW} fullHeight>
       <ClubMetadata club={club} />
       {userInfo != null && (
         <ClubApprovalDialog club={club} userInfo={userInfo} />
       )}
+      {club.badges.length > 0 &&
+        club.badges
+          .filter((badge) => badge.message && badge.message.length > 0)
+          .map((badge) => (
+            <div className="notification is-info is-light" key={badge.id}>
+              <Icon name="alert-circle" style={{ marginTop: '-3px' }} />{' '}
+              {badge.message}
+            </div>
+          ))}
       <div className="columns">
-        <div className="column">
+        <div className="column is-two-thirds">
           {isActive || (
             <InactiveCard
               $bordered
@@ -217,14 +225,11 @@ const ClubPage = ({
               pending approval from the {APPROVAL_AUTHORITY}.
             </div>
           )}
-
-          {userInfo != null && (
-            <MobileActions
-              club={club}
-              userInfo={userInfo}
-              updateRequests={updateRequests}
-            />
-          )}
+          <MobileActions
+            club={club}
+            authenticated={userInfo !== undefined}
+            updateRequests={updateRequests}
+          />
           <StyledCard $bordered>
             <Description club={club} />
           </StyledCard>
@@ -268,15 +273,14 @@ const ClubPage = ({
               <MemberList club={club} />
             </>
           )}
+          {events.length > 0 && <EventCarousel data={events} />}
         </div>
         <div className="column is-one-third">
-          {userInfo && (
-            <DesktopActions
-              club={club}
-              userInfo={userInfo}
-              updateRequests={updateRequests}
-            />
-          )}
+          <DesktopActions
+            club={club}
+            authenticated={userInfo !== undefined}
+            updateRequests={updateRequests}
+          />
           <QAButton onClick={scrollToQuestions}>
             {questions.length > 0
               ? `Click here to see the ${questions.length} question${
@@ -296,7 +300,6 @@ const ClubPage = ({
               <div dangerouslySetInnerHTML={{ __html: involvement }} />
             </StyledCard>
           )}
-          <Events data={events} />
           {isClubFieldShown('signature_events') &&
             signatureEvents &&
             !!signatureEvents.length && (
@@ -312,17 +315,19 @@ const ClubPage = ({
             <div className="mb-3">
               <StrongText>Additional Pages</StrongText>
               <ul>
-                <li>
-                  <Link
-                    legacyBehavior
-                    href={CLUB_ALUMNI_ROUTE()}
-                    as={CLUB_ALUMNI_ROUTE(club.code)}
-                  >
-                    <a>
-                      <Icon name="database" /> Alumni
-                    </a>
-                  </Link>
-                </li>
+                {userInfo && (
+                  <li>
+                    <Link
+                      legacyBehavior
+                      href={CLUB_ALUMNI_ROUTE()}
+                      as={CLUB_ALUMNI_ROUTE(club.code)}
+                    >
+                      <a>
+                        <Icon name="database" /> Alumni
+                      </a>
+                    </Link>
+                  </li>
+                )}
                 <li>
                   <Link
                     legacyBehavior
diff --git a/frontend/pages/club/[club]/renew.tsx b/frontend/pages/club/[club]/renew.tsx
index 4222c3533..4390583e5 100644
--- a/frontend/pages/club/[club]/renew.tsx
+++ b/frontend/pages/club/[club]/renew.tsx
@@ -98,40 +98,54 @@ const PolicyBox = ({ onChecked = () => undefined }: Props): ReactElement => {
 
   const policies = [
     {
-      name: 'Campus Membership',
+      name: 'Nondiscrimination',
       content: (
         <div>
-          Membership in registered campus organizations must be open to all
-          persons without regard to race, color, sex, sexual or affectional
-          orientation, religion, national or ethnic origin, handicap, or
-          disability. Under Title IX of the U.S. Education Act Amendment of
-          1972, certain exemptions may be granted for intercollegiate and
-          intramural athletics, fraternities and sororities, and musical groups
-          based on vocal range. Members of all campus organizations must conduct
-          themselves at all times in a mature and responsible manner.
+          In alignment with the University of Pennsylvania’s Nondiscrimination
+          Statement, student-run organizations are expected to uphold a
+          commitment to non-discrimination. This means that these organizations
+          will not engage in discriminatory practices based on factors such as
+          race, color, sex, sexual orientation, gender identity, religion,
+          creed, national or ethnic origin, citizenship status, age, disability,
+          veteran status, or any other legally protected class status. It is
+          essential for student organizations to recognize the University’s
+          dedication to providing academic, social, and recreational programs
+          and services that are equally accessible to all, and as such, all
+          student-run organizations are required to conduct themselves and their
+          activities in accordance with this commitment.
         </div>
       ),
     },
     {
-      name: 'Legal Regulations',
+      name: 'Antihazing',
       content: (
         <div>
-          The rights and property of all persons are to be respected regardless
-          of time or place. Failure to comply with University, City, State, or
-          Federal laws and regulations can result in appropriate disciplinary
-          action. Members of campus organizations are expected to adhere to
-          standards of conduct established by Divisions and Departments of the
-          University.
+          Student organizations must fully comply with the University of
+          Pennsylvania’s Antihazing Regulations, as outlined in the{' '}
+          <a href="https://catalog.upenn.edu/pennbook/">Pennbook</a>. To ensure
+          compliance, students are encouraged to thoroughly review the
+          definition of hazing and the illustrative examples provided within the
+          the Antihazing Regulations. Additionally, it is vital for students to
+          understand the potential consequences of violating these regulations,
+          which may encompass University sanctions affecting both individuals
+          and organizations, as well as potential legal ramifications under
+          state law.
         </div>
       ),
     },
     {
-      name: 'Hazing',
+      name: 'Compliance',
       content: (
         <div>
-          The University is an association of equals who, in working together,
-          comprise a scholarly community. Hazing is inconsistent with the goals
-          and purpose of the University and is explicitly forbidden.
+          Student organizations are obligated to adhere to all policies and
+          procedures established by the University of Pennsylvania. This
+          includes, but is not limited to, the policies outlined in the{' '}
+          <a href="https://catalog.upenn.edu/pennbook/">Pennbook</a> and the{' '}
+          <a href="https://catalog.upenn.edu/pennbook/code-of-student-conduct/">
+            Code of Student Conduct
+          </a>
+          . Furthermore, student organizations are expected to operate in
+          compliance with all relevant local, state, and federal laws.
         </div>
       ),
     },
@@ -226,6 +240,77 @@ const RenewPage = (props: RenewPageProps): ReactElement => {
 
   const year = getCurrentSchoolYear()
 
+  const prerequisites = [
+    {
+      name: 'Membership Requirements',
+      content: (
+        <div style={{ display: 'inline-block' }}>
+          Each club must have at least eight active members, with a minimum of
+          three members designated as officers.
+        </div>
+      ),
+    },
+    {
+      name: 'Group Contacts',
+      content: (
+        <div>
+          Listed group contacts are members of the organization who have
+          significant understanding of, and influence on, group operations.
+        </div>
+      ),
+    },
+    {
+      name: 'Club Operations',
+      content: (
+        <div>
+          Student organizations are to be initiated by, organized, primarily
+          comprised of and solely led by undergraduate or graduate students.
+        </div>
+      ),
+    },
+    {
+      name: 'Training and Workshops',
+      content: (
+        <div>
+          Organizations must complete any required student organization
+          trainings or workshops, such as the Student Organization Summit.
+        </div>
+      ),
+    },
+    {
+      name: 'Branding Compliance',
+      content: (
+        <div>
+          Clubs must have logos that adhere to university branding standards and
+          ensure consistent use across all platforms, including social media and
+          websites. Refer to the{' '}
+          <a href="https://universitylife.upenn.edu/student-brand-guidelines/">
+            Student Branding Guidelines
+          </a>{' '}
+          for more information.
+        </div>
+      ),
+    },
+    {
+      name: 'University Affiliation',
+      content: (
+        <div>
+          The club mission must clearly state that the group is a student
+          organization at the University.
+        </div>
+      ),
+    },
+    {
+      name: 'Policy Adherence',
+      content: (
+        <div>
+          All groups must comply with the relevant policies and guidelines for
+          student organizations.
+        </div>
+      ),
+    },
+  ]
+
   const steps = [
     {
       name: 'Introduction',
@@ -254,39 +339,41 @@ const RenewPage = (props: RenewPageProps): ReactElement => {
           )}
           <TextInfoBox>
             <p>
-              The annual club registration process is a procedure conducted by
-              the{' '}
+              The annual club registration process, conducted by the{' '}
               <a target="_blank" href={APPROVAL_AUTHORITY_URL}>
                 {APPROVAL_AUTHORITY}
-              </a>{' '}
-              to ensure that student-run clubs are officially registered and
+              </a>
+              , ensures that student-run clubs are officially registered and
               permitted to operate on campus for the upcoming academic year.
-              During this process, clubs are required to submit update their
-              club officers and membership roster information; if applicable,
-              update primary contact information fulfill any other requirements
-              set by the University.
+              During this process, clubs are required to submit updated officer
+              and membership roster information and, if applicable, update their
+              primary contact details.
             </p>
             <p>
-              The purpose of the annual club registration process is to maintain
-              a well-organized and vibrant campus community, allowing students
-              to explore various interests and engage in extracurricular
-              activities. By registering each year, clubs reaffirm their
-              commitment to following school policies, uphold their mission, and
-              demonstrate their ongoing relevance to the student body.
+              The purpose of this process is to maintain a well-organized and
+              vibrant campus community, enabling students to explore various
+              interests and engage in extracurricular activities. By registering
+              each year, clubs reaffirm their commitment to following university
+              policies, uphold their mission, and demonstrate their ongoing
+              relevance to the student body.
             </p>
             <p>
-              Benefits of club registration include access to funding
-              opportunities, the ability to reserve campus facilities for
-              events, eligibility to participate in campus-wide events like the
-              annual activities’ fairs, and access to resources and support from
-              the student affairs office or other university departments.
+              Registration identifies the organization as active and grants
+              access to essential university resources, such as reserving space,
+              accessing electronic resources, appropriate use of the Penn name,
+              potential funding opportunities, participation in activities
+              fairs, and the ability to advertise as a student-run organization
+              at the University of Pennsylvania.
             </p>
             <p>
-              Overall, the annual club registration process plays a crucial role
-              in fostering a diverse and active campus life, enriching the
-              educational experience of students, and promoting a sense of
-              community and belonging.
+              To successfully register or re-register your organization, the
+              following prerequisites must be met:
             </p>
+            {prerequisites.map(({ name, content }) => (
+              <p>
+                <b>{name}</b>: {content}
+              </p>
+            ))}
             <p>
               If you have any questions about the club registration process,
               please contact the Office of Student Affairs at
diff --git a/frontend/pages/events/[id].tsx b/frontend/pages/events/[id].tsx
index dc7727b84..49469c1aa 100644
--- a/frontend/pages/events/[id].tsx
+++ b/frontend/pages/events/[id].tsx
@@ -201,6 +201,7 @@ const GetTicketItem: React.FC<TicketItemProps> = ({
         </p>
         <Input
           type="number"
+          pattern="[0-9]*"
           className="input"
           min={0}
           max={max}
@@ -208,7 +209,7 @@ const GetTicketItem: React.FC<TicketItemProps> = ({
           step={1}
           placeholder="Ticket Count"
           onChange={handleCountChange}
-          style={{ flex: '0 0 auto', maxWidth: '48px' }}
+          style={{ flex: '0 0 auto', maxWidth: '64px' }}
         />
       </div>
     </div>
@@ -361,10 +362,12 @@ const EventPage: React.FC<EventPageProps> = ({
                   ))}
                   <button
                     className="button is-primary is-fullwidth mt-4"
-                    disabled={totalAvailableTickets === 0}
+                    disabled={
+                      totalAvailableTickets === 0 || endTime < DateTime.now()
+                    }
                     onClick={() => setShowTicketModal(true)}
                   >
-                    Get Tickets
+                    {endTime < DateTime.now() ? 'Event Ended' : 'Get Tickets'}
                   </button>
                 </Card>
               )}
diff --git a/frontend/pages/fair.tsx b/frontend/pages/fair.tsx
index 0bfd8aa38..8992a3861 100644
--- a/frontend/pages/fair.tsx
+++ b/frontend/pages/fair.tsx
@@ -12,7 +12,7 @@ import { ReactElement, useEffect, useState } from 'react'
 import renderPage from 'renderPage'
 import { ClubFair } from 'types'
 import { cache, doApiRequest, useSetting } from 'utils'
-import { FAIR_NAME } from 'utils/branding'
+import { SUPPORT_EMAIL } from 'utils/branding'
 
 import { CLUB_ROUTE, SNOW } from '~/constants'
 
@@ -39,10 +39,8 @@ const FairPage = ({
   )
   const isPreFair = useSetting('PRE_FAIR')
   const fairName = fair?.name ?? useSetting('FAIR_NAME') ?? 'Upcoming Fair'
-  const fairOrgName = fair?.organization ?? 'partner organization'
-  const fairContact = fair?.contact ?? 'the partner organization'
-  const fairTime = fair?.time ?? 'TBD'
-  const fairAdditionalInfo = fair?.information ?? ''
+  const fairContact =
+    fair?.contact ?? useSetting('FAIR_CONTACT') ?? SUPPORT_EMAIL
 
   /**
    * Open up the fair on the designated time client side if we're close.
@@ -64,44 +62,76 @@ const FairPage = ({
     }
   }, [])
 
+  if (!isPreFair && !isFairOpen && !isOverride) {
+    return (
+      <Container background={SNOW}>
+        <Metadata title="Upcoming Fair Guide" />
+        <InfoPageTitle>Upcoming Fair Student Guide</InfoPageTitle>
+        <div className="content">
+          <div className="notification is-warning">
+            <Icon name="alert-triangle" /> There is currently no fair currently
+            occurring or upcoming. If you believe this is an error, please
+            contact <Contact />.
+          </div>
+        </div>
+      </Container>
+    )
+  }
   return (
     <Container background={SNOW}>
       <Metadata title={fairName as string} />
       <InfoPageTitle>{fairName} – Student Guide</InfoPageTitle>
       <div className="content">
-        {!isPreFair && !isFairOpen && !isOverride && (
-          <div className="notification is-warning">
-            <Icon name="alert-triangle" /> There is currently no {FAIR_NAME}{' '}
-            fair that is currently occurring or upcoming. If you believe this is
-            an error, please contact <Contact />.
-          </div>
-        )}
+        {fair ? (
+          <>
+            <p>
+              The {fair.name} sponsored by the {fair.organization}, will be held
+              from {fair.start_time.split('T')[0]} to{' '}
+              {fair.end_time.split('T')[0]}!
+            </p>
 
-        <p>
-          The 2023 Fall Activities Fair sponsored by the Student Activities
-          Council(SAC), will be held from August 29th to August 31st! This event
-          will showcase various student-run clubs, with each day dedicated to
-          highlighting different club categories. The fair will take place on
-          College Green from 12p-4p each day.
-        </p>
-        <p>
-          To participate, sign-up will be facilitated through Penn Clubs and
-          will coincide with the annual club registration process. Only
-          returning undergraduate student-run groups that were registered in
-          Penn Clubs last year are eligible to sign-up for the Fall Activities
-          Fair.
-        </p>
-        <p>
-          To secure your club’s spot, be sure to register before the deadline on
-          August 22nd. Once the registration process is complete, registered
-          clubs will receive information about their scheduled day for the Fair
-          by August 24th.
-        </p>
-        <p>
-          For any inquiries or clarifications about the Fair, don't hesitate to
-          reach out to SAC at fair@sacfunded.net. We look forward to seeing you
-          there!
-        </p>
+            {fair.information.trim().length !== 0 && (
+              <div>
+                <h3>Student Information</h3>
+                <div
+                  dangerouslySetInnerHTML={{
+                    __html: fair.information,
+                  }}
+                />
+              </div>
+            )}
+
+            <br />
+
+            {fair.registration_information.trim().length !== 0 && (
+              <div>
+                <h3>Registration Information</h3>
+                <div
+                  dangerouslySetInnerHTML={{
+                    __html: fair.registration_information,
+                  }}
+                />
+              </div>
+            )}
+
+            <br />
+
+            <p>
+              To secure your club’s spot, be sure to register before the
+              deadline on {fair.registration_end_time.split('T')[0]}.
+            </p>
+            <p>
+              For any inquiries or clarifications about the Fair, don't hesitate
+              to reach out to {fair.contact}. We look forward to seeing you
+              there!
+            </p>
+          </>
+        ) : (
+          <p>
+            The {fairName} is currently ongoing! Please check with {fairContact}{' '}
+            for more information.
+          </p>
+        )}
         <div className="columns mt-3">
           {events.map(({ start_time, end_time, events }, i): ReactElement => {
             const parsedDate = moment(start_time).tz('America/New_York')
diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx
index 3360beeef..a03535c6b 100644
--- a/frontend/pages/index.tsx
+++ b/frontend/pages/index.tsx
@@ -278,6 +278,7 @@ const searchIsEmpty = (input: SearchInput): boolean => {
 
 const Splash = (props: SplashProps): ReactElement => {
   const fairIsOpen = useSetting('FAIR_OPEN')
+  const fairIsVirtual = useSetting('FAIR_VIRTUAL')
   const preFair = useSetting('PRE_FAIR')
   const renewalBanner = useSetting('CLUB_REGISTRATION')
   const currentSearch = useRef<SearchInput>({})
@@ -595,7 +596,11 @@ const Splash = (props: SplashProps): ReactElement => {
             />
 
             {(preFair || fairIsOpen) && (
-              <LiveEventsDialog isPreFair={!!preFair} isFair={!!fairIsOpen} />
+              <LiveEventsDialog
+                isPreFair={!!preFair}
+                isFair={!!fairIsOpen}
+                isVirtual={!!fairIsVirtual}
+              />
             )}
 
             {renewalBanner && <ListRenewalDialog />}
diff --git a/frontend/pages/rank.tsx b/frontend/pages/rank.tsx
deleted file mode 100644
index 06debc3d8..000000000
--- a/frontend/pages/rank.tsx
+++ /dev/null
@@ -1,362 +0,0 @@
-import {
-  Contact,
-  Container,
-  Icon,
-  InfoPageTitle,
-  Metadata,
-  StrongText,
-  Text,
-} from 'components/common'
-import { ReactElement } from 'react'
-import renderPage from 'renderPage'
-import styled from 'styled-components'
-import {
-  FIELD_PARTICIPATION_LABEL,
-  OBJECT_NAME_PLURAL,
-  OBJECT_NAME_SINGULAR,
-  OBJECT_NAME_TITLE,
-  OBJECT_NAME_TITLE_SINGULAR,
-  SHOW_RANK_ALGORITHM,
-  SITE_NAME,
-} from 'utils/branding'
-
-import { GREEN, SNOW } from '~/constants/colors'
-
-const RankItem = styled.div`
-  padding: 0.75em;
-  margin-top: 15px;
-  display: flex;
-
-  & p {
-    margin-bottom: 0;
-  }
-
-  & ul {
-    display: block;
-    font-size: 0.9em;
-    margin-left: 1em;
-  }
-`
-
-const LargeIconWrapper = styled.div`
-  flex-basis: 80px;
-  margin-right: 10px;
-`
-
-const LargeIcon = styled(Icon)`
-  width: 75px;
-  height: 75px;
-  padding: 5px;
-
-  @media (max-width: 769px) {
-    & {
-      width: 45px;
-      height: 45px;
-    }
-  }
-`
-
-type RankItemData = {
-  name: string
-  description: string | ReactElement
-  points?: [number, string][]
-}
-
-type RankListProps = {
-  items: RankItemData[]
-}
-
-const RankList = ({ items }: RankListProps): ReactElement => {
-  return (
-    <div className="is-clearfix mb-5">
-      {items.map(({ name, description, points }) => (
-        <RankItem key={name}>
-          <LargeIconWrapper>
-            <LargeIcon
-              name="check-circle"
-              alt="check"
-              style={{ color: GREEN }}
-            />
-          </LargeIconWrapper>
-          <div>
-            <b>{name}</b>
-            <Text>{description}</Text>
-            {points && (
-              <ul>
-                {points.map(([num, desc], i) => (
-                  <li key={i}>
-                    <b>{num > 0 ? `+${num}` : num}</b>: {desc}
-                  </li>
-                ))}
-              </ul>
-            )}
-          </div>
-        </RankItem>
-      ))}
-    </div>
-  )
-}
-
-const Rank = (): ReactElement => (
-  <Container background={SNOW}>
-    <Metadata title={`${OBJECT_NAME_TITLE_SINGULAR} Ordering`} />
-    <InfoPageTitle>
-      {OBJECT_NAME_TITLE_SINGULAR} Recommendation Algorithm
-    </InfoPageTitle>
-    {SHOW_RANK_ALGORITHM || (
-      <div className="notification is-info">
-        <Icon name="alert-circle" /> The {OBJECT_NAME_SINGULAR} recommendation
-        algorithm is not fully configured for {SITE_NAME}. The categories listed
-        below may or may not be taken into consideration when ordering{' '}
-        {OBJECT_NAME_PLURAL} on the home page.
-      </div>
-    )}
-    <StrongText>How are {OBJECT_NAME_PLURAL} ordered?</StrongText>
-    <Text>
-      The order that {OBJECT_NAME_PLURAL} appear on the home page for the
-      default ordering method is determined by several criteria. A
-      recommendation algorithm uses these criteria to ensure that students
-      receive the best experience when browsing for new {OBJECT_NAME_PLURAL} and
-      that {OBJECT_NAME_PLURAL} can effectively reach their target demographic.
-    </Text>
-    <StrongText>
-      How does the {OBJECT_NAME_SINGULAR} recommendation algorithm work?
-    </StrongText>
-    <Text>
-      The recommendation algorithm uses the following non-targeted criteria to
-      determine how to order {OBJECT_NAME_PLURAL} on the home page.{' '}
-      {OBJECT_NAME_TITLE} are ordered by points, and then this ordering as
-      adjusted based on personalized data. The points obtained from these
-      categories is calculated and saved once per day at 4 AM, so make your
-      changes early! The criteria are:
-    </Text>
-    <RankList
-      items={[
-        {
-          name: 'Upcoming Events',
-          description: `If your ${OBJECT_NAME_SINGULAR} has upcoming events registered on ${SITE_NAME}, it will be prioritized on the home page a short period before and during the event. Only events shorter than 16 hours are eligible.`,
-          points: [
-            [10, 'Participating in upcoming activities fair'],
-            [10, 'At least one upcoming event is today'],
-            [
-              10,
-              'All upcoming events today have a complete picture and description',
-            ],
-            [5, 'At least one upcoming event in the next week'],
-            [
-              5,
-              'All upcoming events this week have a complete picture and description',
-            ],
-          ],
-        },
-        {
-          name: 'Upcoming Applications',
-          description: `If a ${OBJECT_NAME_SINGULAR} application is currently open for your ${OBJECT_NAME_SINGULAR}, it will be prioritized while that application is still open.`,
-          points: [
-            [25, `Has at least one open ${OBJECT_NAME_SINGULAR} application`],
-          ],
-        },
-        {
-          name: 'Membership',
-          description: `Having your ${OBJECT_NAME_SINGULAR} members displayed on ${SITE_NAME} provides more points of contact for questions about your ${OBJECT_NAME_SINGULAR}.`,
-          points: [
-            [15, 'At least 3 active officers'],
-            [10, 'At least 3 active members'],
-            [0.1, 'For every non-officer member'],
-          ],
-        },
-        {
-          name: 'Useful Tags',
-          description: (
-            <>
-              Adding relevant tags to your {OBJECT_NAME_SINGULAR} can help
-              prospective students find the {OBJECT_NAME_PLURAL} that they are
-              interested in. If you cannot find at least 2 relevant tags for
-              your {OBJECT_NAME_SINGULAR}, please email <Contact /> and we will
-              work with you to find something appropriate.
-            </>
-          ),
-          points: [
-            [15, 'Has anywhere between 3 and 7 tags'],
-            [7, 'Has more than 7 tags'],
-          ],
-        },
-        {
-          name: 'Contact Information',
-          description: (
-            <>
-              Having contact information is important for prospective members
-              who want to know more about the {OBJECT_NAME_SINGULAR}. Social
-              links can be used to give students a better idea of what you do
-              and the events that you hold.
-            </>
-          ),
-          points: [
-            [10, 'Has a public email'],
-            [10, 'Has 2 or more social links'],
-          ],
-        },
-        {
-          name: 'Bookmarks',
-          description: (
-            <>
-              Bookmarks are a method for Penn students to show interest in your
-              {OBJECT_NAME_SINGULAR}. The more bookmarks you have, the higher
-              your {OBJECT_NAME_SINGULAR} will appear.
-            </>
-          ),
-          points: [[0.04, 'For each bookmark']],
-        },
-        {
-          name: 'Logo Image',
-          description: (
-            <>
-              Adding a logo to your {OBJECT_NAME_SINGULAR} can make your{' '}
-              {OBJECT_NAME_SINGULAR} more recognizable. The logo is shown on the
-              homepage before the user clicks on your {OBJECT_NAME_SINGULAR}.
-            </>
-          ),
-          points: [[15, 'Has a logo']],
-        },
-        {
-          name: `${OBJECT_NAME_TITLE} Subtitle`,
-          description: (
-            <>
-              Adding a subtitle is a quick change that can give students more
-              information about your {OBJECT_NAME_SINGULAR} without having to
-              visit your {OBJECT_NAME_SINGULAR}
-              page. The subtitle is shown on the homepage before the user clicks
-              on your {OBJECT_NAME_SINGULAR}.
-            </>
-          ),
-          points: [
-            [5, 'Has a subtitle'],
-            [-10, 'Did not change default subtitle'],
-          ],
-        },
-        {
-          name: `${OBJECT_NAME_TITLE} Description`,
-          description: (
-            <>
-              Adding a description helps students learn more about whether or
-              not a {OBJECT_NAME_SINGULAR} is a good fit for them.{' '}
-              {OBJECT_NAME_TITLE} without a description will therefore appear
-              lower on the homepage. Longer and more detailed descriptions are
-              awarded bonus points.
-            </>
-          ),
-          points: [
-            [10, 'At least 25 characters'],
-            [10, 'At least 250 characters'],
-            [10, 'At least 1000 characters'],
-            [3, 'Having images in your description'],
-          ],
-        },
-        {
-          name: 'Student Experiences',
-          description: (
-            <>
-              Adding some testimonials help students gain perspective on what
-              participating in the {OBJECT_NAME_SINGULAR} is like.
-            </>
-          ),
-          points: [
-            [10, 'At least one testimonial'],
-            [5, 'At least 3 testimonials'],
-          ],
-        },
-        {
-          name: FIELD_PARTICIPATION_LABEL,
-          description: `Prospective members want to know how to participate in your ${OBJECT_NAME_SINGULAR}. Omitting this section will result in a large ordering penalty.`,
-          points: [[-30, `Empty ${FIELD_PARTICIPATION_LABEL} section`]],
-        },
-        {
-          name: `Is ${OBJECT_NAME_TITLE_SINGULAR} Updated`,
-          description: `${OBJECT_NAME_TITLE} that have not been updated in the last 8 months will receive a small ordering penalty.`,
-          points: [[-10, 'No updates for 8 months']],
-        },
-        {
-          name: `Is ${OBJECT_NAME_TITLE_SINGULAR} Active`,
-          description: (
-            <>
-              {OBJECT_NAME_TITLE} that are marked as inactive will be shifted to
-              the very bottom of the list. You can easily renew your{' '}
-              {OBJECT_NAME_SINGULAR} from the settings tab in the manage{' '}
-              {OBJECT_NAME_SINGULAR} page.
-            </>
-          ),
-          points: [[-1000, `For inactive ${OBJECT_NAME_PLURAL}`]],
-        },
-        {
-          name: 'Random Factor',
-          description: `A random factor is applied periodically in order to ensure that students see new ${OBJECT_NAME_PLURAL} when they visit ${SITE_NAME}.`,
-          points: [
-            [10, 'Random number between 0 and this number, updated daily'],
-          ],
-        },
-      ]}
-    />
-    <Text>
-      The algorithm also attempts to personalize search results for logged in
-      users, based on the following criteria:
-    </Text>
-    <RankList
-      items={[
-        {
-          name: 'Matches Target Tags',
-          description: (
-            <>
-              Adding tags will case the {OBJECT_NAME_SINGULAR} to appear higher
-              on the home page for students who are interested in those tags.{' '}
-              {OBJECT_NAME_TITLE} that have specified fewer tags are more likely
-              to appear higher than {OBJECT_NAME_PLURAL} that have specified
-              more tags, for relevant students.
-            </>
-          ),
-        },
-        {
-          name: 'Matches Target Schools',
-          description: (
-            <>
-              Adding target schools will cause the {OBJECT_NAME_SINGULAR} to
-              appear higher on the home page for students in those schools.{' '}
-              {OBJECT_NAME_TITLE} that have specified fewer schools are more
-              likely to appear higher than {OBJECT_NAME_PLURAL} that have
-              specified more schools, for relevant students. Specifying all of
-              the schools is the same as specifying none of them.
-            </>
-          ),
-        },
-        {
-          name: 'Matches Target Majors',
-          description: (
-            <>
-              Adding target majors will cause the {OBJECT_NAME_SINGULAR} to
-              appear higher on the home page for students in those majors.{' '}
-              {OBJECT_NAME_TITLE} that have specified fewer majors are more
-              likely to appear higher than {OBJECT_NAME_PLURAL} that have
-              specified more majors, for relevant students. Specifying 10 or
-              more majors is the same as specifying no majors.
-            </>
-          ),
-        },
-        {
-          name: 'Matches Target Years',
-          description: (
-            <>
-              Adding target years will cause the {OBJECT_NAME_SINGULAR} to
-              appear higher on the home page for students in those years.{' '}
-              {OBJECT_NAME_TITLE}
-              that have specified fewer years are more likely to appear higher
-              than {OBJECT_NAME_PLURAL} that have specified more years, for
-              relevant students. Specifying all of the years is the same as
-              specifying none of them.
-            </>
-          ),
-        },
-      ]}
-    />
-  </Container>
-)
-
-export default renderPage(Rank)
diff --git a/frontend/pages/tickets/[[...slug]].tsx b/frontend/pages/tickets/[[...slug]].tsx
index 979c40e8e..d49b0c257 100644
--- a/frontend/pages/tickets/[[...slug]].tsx
+++ b/frontend/pages/tickets/[[...slug]].tsx
@@ -221,7 +221,7 @@ const TicketCard = ({ ticket, event, buyersPerm }: TicketCardProps) => {
       )
       const contents = await resp.json()
       if (contents.success) {
-        toast.info(contents.message, { hideProgressBar: true })
+        toast.info(contents.detail, { hideProgressBar: true })
       } else {
         // eslint-disable-next-line no-console
         console.error(contents.errors)
diff --git a/frontend/pages/user/[user]/index.tsx b/frontend/pages/user/[user]/index.tsx
index 9c3349500..01eeb8a84 100644
--- a/frontend/pages/user/[user]/index.tsx
+++ b/frontend/pages/user/[user]/index.tsx
@@ -82,8 +82,7 @@ const UserProfilePage = ({
         <Metadata title="User Profile" />
         <AuthPrompt title="Oh no!" hasLogin={!authenticated}>
           You cannot view the profile for this user. This user might not exist
-          or have set their profile to private.{' '}
-          <span className="has-text-grey">{profile.detail}</span>
+          or have set their profile to private.
         </AuthPrompt>
       </>
     )
diff --git a/frontend/types.ts b/frontend/types.ts
index a7bf7a785..6e03ad955 100644
--- a/frontend/types.ts
+++ b/frontend/types.ts
@@ -61,7 +61,7 @@ export interface ClubEvent {
   large_image_url: string | null
   location: string | null
   name: string
-  ticketed: string
+  ticketed: boolean
   pinned: boolean
   start_time: string
   type: ClubEventType
@@ -80,6 +80,12 @@ export interface CountedEventTicket extends EventTicket {
   count?: number
 }
 
+export enum AdvisorVisibilityType {
+  AdminOnly = 1,
+  Students = 2,
+  All = 3,
+}
+
 export interface ClubApplication {
   id: number
   name: string
@@ -126,6 +132,7 @@ export interface Badge {
   color: string
   purpose: string
   description: string
+  message?: string
 }
 
 export interface QuestionAnswer {
@@ -146,7 +153,7 @@ export interface Advisor {
   department: string
   email: string
   phone: string
-  public: boolean
+  visibility: AdvisorVisibilityType
 }
 
 export interface Club {
@@ -160,6 +167,7 @@ export interface Club {
   approved_comment: string | null
   available_virtually: boolean
   badges: Badge[]
+  beta: boolean
   code: string
   description: string
   email: string
@@ -418,3 +426,10 @@ export type TicketAvailability = {
   totals: TicketEntry[]
   available: TicketEntry[]
 }
+
+export type Template = {
+  id: number
+  author: string
+  title: string
+  content: string
+}
diff --git a/frontend/utils.tsx b/frontend/utils.tsx
index e32bfb0ba..bc911d72e 100644
--- a/frontend/utils.tsx
+++ b/frontend/utils.tsx
@@ -91,7 +91,7 @@ export const SITE_ORIGIN = publicRuntimeConfig.SITE_ORIGIN
 export const API_BASE_URL = `${SITE_ORIGIN}/api`
 
 export const EMPTY_DESCRIPTION =
-  '<span style="color:#666">This club has not added a description yet.</span>'
+  '<span style="color:#666">This club has not added a club mission yet.</span>'
 export const LOGIN_URL = `${API_BASE_URL}/accounts/login/`
 export const LOGOUT_URL = `${API_BASE_URL}/accounts/logout/`
 
diff --git a/frontend/utils/branding.tsx b/frontend/utils/branding.tsx
index beb289d94..a37877c60 100644
--- a/frontend/utils/branding.tsx
+++ b/frontend/utils/branding.tsx
@@ -251,7 +251,9 @@ const sites = {
   },
 }
 
-export const TICKETING_PAYMENT_ENABLED = false
+export const TICKETING_PAYMENT_ENABLED = true
+export const REAPPROVAL_QUEUE_ENABLED = true
+export const NEW_APPROVAL_QUEUE_ENABLED = true
 export const SITE_ID = site
 export const SITE_NAME = sites[site].SITE_NAME
 export const SITE_SUBTITLE = sites[site].SITE_SUBTITLE
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index cad33ed5d..05f7abbbe 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -292,7 +292,7 @@
   dependencies:
     "@babel/types" "^7.12.13"
 
-"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.22.5":
+"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.22.15":
   version "7.22.15"
   resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0"
   integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==
@@ -742,7 +742,7 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.12.13"
 
-"@babel/plugin-syntax-jsx@^7.22.5", "@babel/plugin-syntax-jsx@^7.23.3":
+"@babel/plugin-syntax-jsx@^7.23.3":
   version "7.23.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz#8f2e4f8a9b5f9aa16067e142c1ac9cd9f810f473"
   integrity sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==
@@ -3374,6 +3374,80 @@
     "@svgr/plugin-jsx" "8.1.0"
     "@svgr/plugin-svgo" "8.1.0"
 
+"@swc/core-darwin-arm64@1.9.3":
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.9.3.tgz#cca13f7ce6e1099612a7ba017f4923857d3a4d5f"
+  integrity sha512-hGfl/KTic/QY4tB9DkTbNuxy5cV4IeejpPD4zo+Lzt4iLlDWIeANL4Fkg67FiVceNJboqg48CUX+APhDHO5G1w==
+
+"@swc/core-darwin-x64@1.9.3":
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.9.3.tgz#01376c6c2caea5dd0c235f21ebc7e41238153c86"
+  integrity sha512-IaRq05ZLdtgF5h9CzlcgaNHyg4VXuiStnOFpfNEMuI5fm5afP2S0FHq8WdakUz5WppsbddTdplL+vpeApt/WCQ==
+
+"@swc/core-linux-arm-gnueabihf@1.9.3":
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.9.3.tgz#4a9705903cebfc8e3e2bee71a42f7c88896e61df"
+  integrity sha512-Pbwe7xYprj/nEnZrNBvZfjnTxlBIcfApAGdz2EROhjpPj+FBqBa3wOogqbsuGGBdCphf8S+KPprL1z+oDWkmSQ==
+
+"@swc/core-linux-arm64-gnu@1.9.3":
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.9.3.tgz#722aefc00a7abfb031fae7539226c7d7683f5c8d"
+  integrity sha512-AQ5JZiwNGVV/2K2TVulg0mw/3LYfqpjZO6jDPtR2evNbk9Yt57YsVzS+3vHSlUBQDRV9/jqMuZYVU3P13xrk+g==
+
+"@swc/core-linux-arm64-musl@1.9.3":
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.9.3.tgz#6643f683311cc1dcee00970e3d6b4872225bdbd8"
+  integrity sha512-tzVH480RY6RbMl/QRgh5HK3zn1ZTFsThuxDGo6Iuk1MdwIbdFYUY034heWUTI4u3Db97ArKh0hNL0xhO3+PZdg==
+
+"@swc/core-linux-x64-gnu@1.9.3":
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.9.3.tgz#e6f5cefa244409abe1451fbb4575696a870cbd7a"
+  integrity sha512-ivXXBRDXDc9k4cdv10R21ccBmGebVOwKXT/UdH1PhxUn9m/h8erAWjz5pcELwjiMf27WokqPgaWVfaclDbgE+w==
+
+"@swc/core-linux-x64-musl@1.9.3":
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.9.3.tgz#4d45399f7a01389add61febd02da9b12f16abc81"
+  integrity sha512-ILsGMgfnOz1HwdDz+ZgEuomIwkP1PHT6maigZxaCIuC6OPEhKE8uYna22uU63XvYcLQvZYDzpR3ms47WQPuNEg==
+
+"@swc/core-win32-arm64-msvc@1.9.3":
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.9.3.tgz#8c352bcea558b9a950877cd724f132d7d51a4d80"
+  integrity sha512-e+XmltDVIHieUnNJHtspn6B+PCcFOMYXNJB1GqoCcyinkEIQNwC8KtWgMqUucUbEWJkPc35NHy9k8aCXRmw9Kg==
+
+"@swc/core-win32-ia32-msvc@1.9.3":
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.9.3.tgz#656f78b9c56413dbd590ac259dbe0d563cd8e166"
+  integrity sha512-rqpzNfpAooSL4UfQnHhkW8aL+oyjqJniDP0qwZfGnjDoJSbtPysHg2LpcOBEdSnEH+uIZq6J96qf0ZFD8AGfXA==
+
+"@swc/core-win32-x64-msvc@1.9.3":
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.9.3.tgz#9595c177d2c11909558da93b18f37e7c5ae1909c"
+  integrity sha512-3YJJLQ5suIEHEKc1GHtqVq475guiyqisKSoUnoaRtxkDaW5g1yvPt9IoSLOe2mRs7+FFhGGU693RsBUSwOXSdQ==
+
+"@swc/core@^1.9.3":
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.9.3.tgz#e5bc9b35df2f4a60026c6759c1a6575070339d4f"
+  integrity sha512-oRj0AFePUhtatX+BscVhnzaAmWjpfAeySpM1TCbxA1rtBDeH/JDhi5yYzAKneDYtVtBvA7ApfeuzhMC9ye4xSg==
+  dependencies:
+    "@swc/counter" "^0.1.3"
+    "@swc/types" "^0.1.17"
+  optionalDependencies:
+    "@swc/core-darwin-arm64" "1.9.3"
+    "@swc/core-darwin-x64" "1.9.3"
+    "@swc/core-linux-arm-gnueabihf" "1.9.3"
+    "@swc/core-linux-arm64-gnu" "1.9.3"
+    "@swc/core-linux-arm64-musl" "1.9.3"
+    "@swc/core-linux-x64-gnu" "1.9.3"
+    "@swc/core-linux-x64-musl" "1.9.3"
+    "@swc/core-win32-arm64-msvc" "1.9.3"
+    "@swc/core-win32-ia32-msvc" "1.9.3"
+    "@swc/core-win32-x64-msvc" "1.9.3"
+
+"@swc/counter@^0.1.3":
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
+  integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
+
 "@swc/helpers@0.5.2":
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d"
@@ -3381,6 +3455,13 @@
   dependencies:
     tslib "^2.4.0"
 
+"@swc/types@^0.1.17":
+  version "0.1.17"
+  resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.17.tgz#bd1d94e73497f27341bf141abdf4c85230d41e7c"
+  integrity sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==
+  dependencies:
+    "@swc/counter" "^0.1.3"
+
 "@tootallnate/once@1":
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@@ -4400,17 +4481,6 @@ babel-plugin-react-require@^3.1.3:
   resolved "https://registry.yarnpkg.com/babel-plugin-react-require/-/babel-plugin-react-require-3.1.3.tgz#ba3d7305b044a90c35c32c5a9ab943fd68e1638d"
   integrity sha512-kDXhW2iPTL81x4Ye2aUMdEXQ56JP0sBJmRQRXJPH5FsNB7fOc/YCsHTqHv8IovPyw9Rk07gdd7MVUz8tUmRBCA==
 
-babel-plugin-styled-components@^2.1.4:
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz#9a1f37c7f32ef927b4b008b529feb4a2c82b1092"
-  integrity sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==
-  dependencies:
-    "@babel/helper-annotate-as-pure" "^7.22.5"
-    "@babel/helper-module-imports" "^7.22.5"
-    "@babel/plugin-syntax-jsx" "^7.22.5"
-    lodash "^4.17.21"
-    picomatch "^2.3.1"
-
 babel-preset-next@^1.2.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/babel-preset-next/-/babel-preset-next-1.4.0.tgz#6f20007befb6d888be315a64d772e0d7101b71ca"
@@ -4662,15 +4732,10 @@ camelize@^1.0.0:
   resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b"
   integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=
 
-caniuse-lite@^1.0.30001181:
-  version "1.0.30001192"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001192.tgz#b848ebc0ab230cf313d194a4775a30155d50ae40"
-  integrity sha512-63OrUnwJj5T1rUmoyqYTdRWBqFFxZFlyZnRRjDR8NSUQFB6A+j/uBORU/SyJ5WzDLg4SPiZH40hQCBNdZ/jmAw==
-
-caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001587:
-  version "1.0.30001588"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz#07f16b65a7f95dba82377096923947fb25bce6e3"
-  integrity sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==
+caniuse-lite@^1.0.30001181, caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001587:
+  version "1.0.30001683"
+  resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001683.tgz"
+  integrity sha512-iqmNnThZ0n70mNwvxpEC2nBJ037ZHZUoBI5Gorh1Mw6IlEAZujEoU1tXA628iZfzm7R9FvFzxbfdgml82a3k8Q==
 
 caseless@~0.12.0:
   version "0.12.0"
@@ -9075,6 +9140,11 @@ react-linkify@^1.0.0-alpha:
     linkify-it "^2.0.3"
     tlds "^1.199.0"
 
+react-masonry-css@^1.0.16:
+  version "1.0.16"
+  resolved "https://registry.yarnpkg.com/react-masonry-css/-/react-masonry-css-1.0.16.tgz#72b28b4ae3484e250534700860597553a10f1a2c"
+  integrity sha512-KSW0hR2VQmltt/qAa3eXOctQDyOu7+ZBevtKgpNDSzT7k5LA/0XntNa9z9HKCdz3QlxmJHglTZ18e4sX4V8zZQ==
+
 react-motion@^0.5.2:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316"
@@ -9960,8 +10030,7 @@ string_decoder@~1.1.1:
   dependencies:
     safe-buffer "~5.1.0"
 
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1:
-  name strip-ansi-cjs
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -9975,6 +10044,13 @@ strip-ansi@^6.0.0:
   dependencies:
     ansi-regex "^5.0.0"
 
+strip-ansi@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
 strip-ansi@^7.0.1:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -10090,6 +10166,16 @@ svgo@^3.0.2:
     csso "^5.0.5"
     picocolors "^1.0.0"
 
+swc-plugin-coverage-instrument@^0.0.25:
+  version "0.0.25"
+  resolved "https://registry.yarnpkg.com/swc-plugin-coverage-instrument/-/swc-plugin-coverage-instrument-0.0.25.tgz#111fdca889029eaa9104a62714c4fdd88d35da60"
+  integrity sha512-7watbzeySulr+7o3NbSaD2icOmEYBKf1uUQs2sBYv1cdWHEsUQETxI/CpXn+Q53vaBU0e3puWPO99OW3DVq34g==
+
+swiper@^11.1.12:
+  version "11.1.12"
+  resolved "https://registry.yarnpkg.com/swiper/-/swiper-11.1.12.tgz#563b90dd0162925025878c2ec4e136cc46bcb4f4"
+  integrity sha512-PUkCToYAZMB4kP7z+YfPnkMHOMwMO71g8vUhz2o5INGIgIMb6Sb0XiP6cEJFsiFTd7FRDn5XCbg+KVKPDZqXLw==
+
 synchronous-promise@^2.0.15:
   version "2.0.17"
   resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.17.tgz#38901319632f946c982152586f2caf8ddc25c032"
@@ -10773,8 +10859,7 @@ wordwrap@^1.0.0:
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
   integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
 
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
-  name wrap-ansi-cjs
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
   integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -10792,6 +10877,15 @@ wrap-ansi@^6.2.0:
     string-width "^4.1.0"
     strip-ansi "^6.0.0"
 
+wrap-ansi@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
 wrap-ansi@^8.1.0:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
diff --git a/k8s/main.ts b/k8s/main.ts
index 23e9c5629..8276c0b29 100644
--- a/k8s/main.ts
+++ b/k8s/main.ts
@@ -31,7 +31,7 @@ export class MyChart extends PennLabsChart {
     new DjangoApplication(this, 'django-wsgi', {
       deployment: {
         image: backendImage,
-        replicas: 4,
+        replicas: 2,
         secret: clubsSecret,
         env: [
           { name: 'REDIS_HOST', value: 'penn-clubs-redis' },
@@ -60,101 +60,61 @@ export class MyChart extends PennLabsChart {
     new ReactApplication(this, 'react', {
       deployment: {
         image: frontendImage,
-        replicas: 3,
+        replicas: 2,
       },
       domain: { host: clubsDomain, paths: ['/'] },
       port: 80,
       ingressProps,
     });
 
-    /** FYH */
-    const fyhSecret = 'hub-at-penn';
-    const fyhDomain = 'hub.provost.upenn.edu';
-
-    new RedisApplication(this, 'hub-redis', {});
-
-    new DjangoApplication(this, 'hub-django-asgi', {
-      deployment: {
-        image: backendImage,
-        cmd: ['/usr/local/bin/asgi-run'],
-        replicas: 1,
-        secret: fyhSecret,
-        env: [
-          { name: 'REDIS_HOST', value: 'penn-clubs-hub-redis' },
-          { name: 'NEXT_PUBLIC_SITE_NAME', value: 'fyh' },
-        ],
-      },
-      ingressProps: {
-        annotations: {
-          ["ingress.kubernetes.io/protocol"]: "https",
-          ["traefik.ingress.kubernetes.io/router.middlewares"]: "default-redict-http@kubernetescrd"
-        }
-      },
-      djangoSettingsModule: 'pennclubs.settings.production',
-      domains: [{ host: fyhDomain, paths: ['/api'] }],
-    });
-
-    new ReactApplication(this, 'hub-react', {
-      deployment: {
-        image: frontendImage,
-        replicas: 1,
-        env: [
-          { name: 'NEXT_PUBLIC_SITE_NAME', value: 'fyh' },
-        ],
-      },
-      domain: { host: fyhDomain, paths: ['/'] },
-      ingressProps,
-      port: 80,
-    });
-
     /** Cronjobs **/
     new CronJob(this, 'rank-clubs', {
-      schedule: cronTime.everyDayAt(8),
+      schedule: cronTime.everyDayAt(6, 18),
       image: backendImage,
       secret: clubsSecret,
       cmd: ['python', 'manage.py', 'rank'],
     });
 
-    new CronJob(this, 'daily-notifications', {
-      schedule: cronTime.everyDayAt(13),
+    new CronJob(this, 'update-club-counts', {
+      schedule: cronTime.everyDayAt(0, 12),
       image: backendImage,
       secret: clubsSecret,
-      cmd: ['python', 'manage.py', 'daily_notifications'],
-    });
+      cmd: ['python', 'manage.py', 'update_club_counts'],
+    })
 
-    new CronJob(this, 'hub-daily-notifications', {
-      schedule: cronTime.everyDayAt(13),
+    new CronJob(this, 'osa-perms-updates', {
+      schedule: cronTime.every(5).minutes(),
       image: backendImage,
-      secret: fyhSecret,
-      cmd: ['python', 'manage.py', 'daily_notifications'],
+      secret: clubsSecret,
+      cmd: ['python', 'manage.py', 'osa_perms_updates'],
     });
 
-    new CronJob(this, 'calendar-import', {
-      schedule: cronTime.everyDayAt(12),
+    new CronJob(this, 'daily-notifications', {
+      schedule: cronTime.onSpecificDaysAt(['monday', 'wednesday', 'friday'], 10, 0),
       image: backendImage,
       secret: clubsSecret,
-      cmd: ['python', 'manage.py', 'import_calendar_events'],
+      cmd: ['python', 'manage.py', 'daily_notifications'],
     });
 
-    new CronJob(this, 'hub-calendar-import', {
+    new CronJob(this, 'calendar-import', {
       schedule: cronTime.everyDayAt(12),
       image: backendImage,
-      secret: fyhSecret,
+      secret: clubsSecret,
       cmd: ['python', 'manage.py', 'import_calendar_events'],
     });
 
-    new CronJob(this, 'hub-paideia-calendar-import', {
+    new CronJob(this, 'expire-stale-membership-invites', {
       schedule: cronTime.everyDayAt(12),
       image: backendImage,
-      secret: fyhSecret,
-      cmd: ["python", "manage.py", "import_paideia_events"],
+      secret: clubsSecret,
+      cmd: ["python", "manage.py", "expire_membership_invites"],
     });
 
-    new CronJob(this, 'expire-stale-membership-invites', {
-      schedule: cronTime.everyDayAt(12),
+    new CronJob(this, 'graduate-users', {
+      schedule: cronTime.everyYearIn(1, 1, 12, 0),
       image: backendImage,
-      secret: fyhSecret,
-      cmd: ["python", "manage.py", "expire_membership_invites"],
+      secret: clubsSecret,
+      cmd: ["python", "manage.py", "graduate_users"],
     });
   }
 }