diff --git a/apps/nominations/admin.py b/apps/nominations/admin.py
index 4b19aff51..b9d157dff 100644
--- a/apps/nominations/admin.py
+++ b/apps/nominations/admin.py
@@ -3,13 +3,23 @@
from django.contrib import admin
from django.db.models.functions import Lower
-from apps.nominations.models import Election, Nomination, Nominee
+from apps.nominations.models import Election, ElectionKind, Nomination, Nominee
+
+
+@admin.register(ElectionKind)
+class ElectionKindAdmin(admin.ModelAdmin):
+ """Admin interface for managing election kinds and their accent colors."""
+
+ list_display = ("name", "accent_color", "slug")
+ readonly_fields = ("slug",)
@admin.register(Election)
class ElectionAdmin(admin.ModelAdmin):
"""Admin interface for managing elections."""
+ list_display = ("name", "kind", "date", "slug")
+ list_filter = ("kind",)
readonly_fields = ("slug",)
diff --git a/apps/nominations/migrations/0003_election_kind.py b/apps/nominations/migrations/0003_election_kind.py
new file mode 100644
index 000000000..8e32e6ffe
--- /dev/null
+++ b/apps/nominations/migrations/0003_election_kind.py
@@ -0,0 +1,46 @@
+# Generated by Django 5.2.13 on 2026-06-24 15:33
+
+import colorfield.fields
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("nominations", "0002_auto_20190514_1435"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="ElectionKind",
+ fields=[
+ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
+ ("name", models.CharField(max_length=100, unique=True)),
+ ("slug", models.SlugField(blank=True, max_length=120, null=True, unique=True)),
+ (
+ "accent_color",
+ colorfield.fields.ColorField(
+ default="#0073b7",
+ help_text="Accent color used to theme this kind's pages.",
+ image_field=None,
+ max_length=25,
+ samples=None,
+ ),
+ ),
+ ],
+ options={
+ "ordering": ["name"],
+ },
+ ),
+ migrations.AddField(
+ model_name="election",
+ name="kind",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="elections",
+ to="nominations.electionkind",
+ ),
+ ),
+ ]
diff --git a/apps/nominations/models.py b/apps/nominations/models.py
index c440999a4..888a2853d 100644
--- a/apps/nominations/models.py
+++ b/apps/nominations/models.py
@@ -2,6 +2,7 @@
import datetime
+from colorfield.fields import ColorField
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
@@ -13,12 +14,48 @@
from apps.users.models import User
from fastly.utils import purge_url
+DEFAULT_ACCENT_COLOR = "#0073b7"
+
+
+class ElectionKind(models.Model):
+ """An admin-managed category of election (Board, Packaging Council, ...).
+
+ Each kind carries an accent color used to visually distinguish its
+ election pages. New kinds can be added in the Django admin without
+ code changes.
+ """
+
+ name = models.CharField(max_length=100, unique=True)
+ slug = models.SlugField(max_length=120, unique=True, blank=True, null=True)
+ accent_color = ColorField(default=DEFAULT_ACCENT_COLOR, help_text="Accent color used to theme this kind's pages.")
+
+ class Meta:
+ """Meta configuration for ElectionKind."""
+
+ ordering = ["name"]
+
+ def __str__(self):
+ """Return the kind name."""
+ return self.name
+
+ def save(self, *args, **kwargs):
+ """Generate slug from name before saving."""
+ self.slug = slugify(self.name)
+ super().save(*args, **kwargs)
+
class Election(models.Model):
"""A PSF board election with nomination open/close dates."""
name = models.CharField(max_length=100)
date = models.DateField()
+ kind = models.ForeignKey(
+ ElectionKind,
+ null=True,
+ blank=True,
+ on_delete=models.SET_NULL,
+ related_name="elections",
+ )
nominations_open_at = models.DateTimeField(blank=True, null=True)
nominations_close_at = models.DateTimeField(blank=True, null=True)
description = MarkupField(escape_html=False, markup_type="markdown", blank=False, null=True)
@@ -39,6 +76,11 @@ def save(self, *args, **kwargs):
self.slug = slugify(self.name)
super().save(*args, **kwargs)
+ @property
+ def accent_color(self):
+ """Return the CSS accent color for this election's kind."""
+ return self.kind.accent_color if self.kind else DEFAULT_ACCENT_COLOR
+
@property
def nominations_open(self):
"""Return True if the current time is within the nomination window."""
diff --git a/apps/nominations/templates/nominations/_theme.html b/apps/nominations/templates/nominations/_theme.html
new file mode 100644
index 000000000..74c0f41fc
--- /dev/null
+++ b/apps/nominations/templates/nominations/_theme.html
@@ -0,0 +1,20 @@
+{% comment %}
+Per-election theming. Expects `election` in context (pass via
+`{% include "nominations/_theme.html" with election=object.election %}`
+where only the nomination object is available). Colors key headings
+with the election's accent so Board vs. Packaging Council pages are
+visually distinct. See Election.accent_color / the ElectionKind model.
+{% endcomment %}
+{% if election %}
+
+{% endif %}
diff --git a/apps/nominations/templates/nominations/election_detail.html b/apps/nominations/templates/nominations/election_detail.html
index cf08b865d..a06f7afa7 100644
--- a/apps/nominations/templates/nominations/election_detail.html
+++ b/apps/nominations/templates/nominations/election_detail.html
@@ -4,6 +4,8 @@
{% block page_title %}Elections | {{ SITE_INFO.site_name }}{% endblock %}
+{% block head %}{% include "nominations/_theme.html" %}{% endblock %}
+
{% block content_attributes %}with-left-sidebar{% endblock %}
diff --git a/apps/nominations/templates/nominations/nomination_accept_form.html b/apps/nominations/templates/nominations/nomination_accept_form.html
index 37a07da60..4e0de693a 100644
--- a/apps/nominations/templates/nominations/nomination_accept_form.html
+++ b/apps/nominations/templates/nominations/nomination_accept_form.html
@@ -6,6 +6,7 @@
{% endblock %}
{% block body_attributes %}class="nominations nominations_view"{% endblock %}
+{% block head %}{% include "nominations/_theme.html" %}{% endblock %}
{% block left_sidebar %}{% endblock %}
{% block content_attributes %}{% endblock %}
diff --git a/apps/nominations/templates/nominations/nomination_detail.html b/apps/nominations/templates/nominations/nomination_detail.html
index 36f6fe773..8c57f6fd3 100644
--- a/apps/nominations/templates/nominations/nomination_detail.html
+++ b/apps/nominations/templates/nominations/nomination_detail.html
@@ -6,6 +6,7 @@
{% endblock %}
{% block body_attributes %}class="nominations nominations_view"{% endblock %}
+{% block head %}{% include "nominations/_theme.html" with election=nomination.election %}{% endblock %}
{% block left_sidebar %}{% endblock %}
{% block content_attributes %}{% endblock %}
diff --git a/apps/nominations/templates/nominations/nomination_form.html b/apps/nominations/templates/nominations/nomination_form.html
index baed48410..f258aa3ad 100644
--- a/apps/nominations/templates/nominations/nomination_form.html
+++ b/apps/nominations/templates/nominations/nomination_form.html
@@ -10,6 +10,7 @@
{% endblock %}
{% block body_attributes %}class="nominations nominations_form"{% endblock %}
+{% block head %}{% include "nominations/_theme.html" %}{% endblock %}
{% block left_sidebar %}{% endblock %}
{% block content_attributes %}{% endblock %}
diff --git a/apps/nominations/templates/nominations/nominee_detail.html b/apps/nominations/templates/nominations/nominee_detail.html
index a0dcd7926..911e9b161 100644
--- a/apps/nominations/templates/nominations/nominee_detail.html
+++ b/apps/nominations/templates/nominations/nominee_detail.html
@@ -7,6 +7,7 @@
{% endblock %}
{% block body_attributes %}class="nominations nominations_view"{% endblock %}
+{% block head %}{% include "nominations/_theme.html" %}{% endblock %}
{% block left_sidebar %}{% endblock %}
{% block content_attributes %}{% endblock %}
diff --git a/apps/nominations/templates/nominations/nominee_list.html b/apps/nominations/templates/nominations/nominee_list.html
index 370de5d37..def5fdca5 100644
--- a/apps/nominations/templates/nominations/nominee_list.html
+++ b/apps/nominations/templates/nominations/nominee_list.html
@@ -11,6 +11,7 @@
{% block content_attributes %}{% endblock %}
{% block head %}
+{% include "nominations/_theme.html" %}