From 6f66df3ba41041c34228f00fdb0399ea090bd0df Mon Sep 17 00:00:00 2001 From: Bernard Siebens Date: Mon, 13 Jul 2026 15:29:24 +0200 Subject: [PATCH] feat(ui): platform control panel and styled auth screens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the `controlpanel` app: a platform-wide (not club-scoped) admin panel for creating clubs, archiving/restoring them, managing club admins, and per-club statistics (members, teams & staff, events, shop). Statistics are annotated in one query so the club list cannot fan out into N+1, and are returned as stat *groups* so growing the domain means adding one entry. Two access rules, both enforced by PlatformStaffRequiredMixin: - staff only (is_staff/is_superuser); anonymous are sent to login, signed-in non-staff get a 403. Staff already need a second factor, so the panel is 2FA-protected for free. - base domain only: the panel manages *all* clubs, so it 404s if the tenant middleware resolved a club from the subdomain. Granting admin to an unknown email creates the account (unusable password — they set one via password reset) and the Member behind it, since a ClubRole hangs off a Member. A member who already holds a role is promoted in place, because there is only one role per member per club. UI is Tailwind + daisyUI. allauth ships an element system, so overriding allauth/layouts/base.html plus ~13 element partials restyles *every* auth and 2FA screen at once — login, signup, password reset, the 2FA challenge, TOTP enrolment, passkeys and recovery codes — rather than templating 20+ pages. Co-Authored-By: Claude Opus 4.8 --- clubmanager/settings.py | 1 + clubmanager/urls.py | 1 + controlpanel/__init__.py | 0 controlpanel/admin.py | 1 + controlpanel/apps.py | 5 + controlpanel/forms.py | 37 +++ controlpanel/migrations/__init__.py | 0 controlpanel/mixins.py | 27 ++ controlpanel/models.py | 1 + controlpanel/services/__init__.py | 0 controlpanel/services/admins.py | 47 ++++ controlpanel/services/statistics.py | 92 +++++++ controlpanel/templates/controlpanel/base.html | 24 ++ .../controlpanel/club_admin_form.html | 36 +++ .../templates/controlpanel/club_detail.html | 89 ++++++ .../templates/controlpanel/club_form.html | 33 +++ .../templates/controlpanel/club_list.html | 61 +++++ .../templates/controlpanel/dashboard.html | 64 +++++ controlpanel/templatetags/__init__.py | 0 controlpanel/templatetags/ui.py | 30 +++ controlpanel/tests.py | 255 ++++++++++++++++++ controlpanel/urls.py | 17 ++ controlpanel/views.py | 128 +++++++++ static/css/app.css | 2 + .../allauth/controlpanel/services/__init__.py | 0 .../controlpanel/templatetags/__init__.py | 0 templates/allauth/elements/alert.html | 4 + templates/allauth/elements/badge.html | 1 + templates/allauth/elements/button.html | 13 + templates/allauth/elements/button_group.html | 4 + templates/allauth/elements/details.html | 5 + templates/allauth/elements/fields.html | 24 ++ templates/allauth/elements/form.html | 9 + templates/allauth/elements/h1.html | 1 + templates/allauth/elements/h2.html | 1 + templates/allauth/elements/hr.html | 1 + templates/allauth/elements/p.html | 1 + templates/allauth/elements/panel.html | 8 + templates/allauth/elements/table.html | 6 + templates/allauth/layouts/base.html | 15 ++ templates/base.html | 86 ++++++ 41 files changed, 1130 insertions(+) create mode 100644 controlpanel/__init__.py create mode 100644 controlpanel/admin.py create mode 100644 controlpanel/apps.py create mode 100644 controlpanel/forms.py create mode 100644 controlpanel/migrations/__init__.py create mode 100644 controlpanel/mixins.py create mode 100644 controlpanel/models.py create mode 100644 controlpanel/services/__init__.py create mode 100644 controlpanel/services/admins.py create mode 100644 controlpanel/services/statistics.py create mode 100644 controlpanel/templates/controlpanel/base.html create mode 100644 controlpanel/templates/controlpanel/club_admin_form.html create mode 100644 controlpanel/templates/controlpanel/club_detail.html create mode 100644 controlpanel/templates/controlpanel/club_form.html create mode 100644 controlpanel/templates/controlpanel/club_list.html create mode 100644 controlpanel/templates/controlpanel/dashboard.html create mode 100644 controlpanel/templatetags/__init__.py create mode 100644 controlpanel/templatetags/ui.py create mode 100644 controlpanel/tests.py create mode 100644 controlpanel/urls.py create mode 100644 controlpanel/views.py create mode 100644 static/css/app.css create mode 100644 templates/allauth/controlpanel/services/__init__.py create mode 100644 templates/allauth/controlpanel/templatetags/__init__.py create mode 100644 templates/allauth/elements/alert.html create mode 100644 templates/allauth/elements/badge.html create mode 100644 templates/allauth/elements/button.html create mode 100644 templates/allauth/elements/button_group.html create mode 100644 templates/allauth/elements/details.html create mode 100644 templates/allauth/elements/fields.html create mode 100644 templates/allauth/elements/form.html create mode 100644 templates/allauth/elements/h1.html create mode 100644 templates/allauth/elements/h2.html create mode 100644 templates/allauth/elements/hr.html create mode 100644 templates/allauth/elements/p.html create mode 100644 templates/allauth/elements/panel.html create mode 100644 templates/allauth/elements/table.html create mode 100644 templates/allauth/layouts/base.html create mode 100644 templates/base.html diff --git a/clubmanager/settings.py b/clubmanager/settings.py index e743042..a187986 100644 --- a/clubmanager/settings.py +++ b/clubmanager/settings.py @@ -57,6 +57,7 @@ INSTALLED_APPS = [ "events.apps.EventsConfig", "formbuilder.apps.FormbuilderConfig", "shop.apps.ShopConfig", + "controlpanel.apps.ControlpanelConfig", ] MIDDLEWARE = [ diff --git a/clubmanager/urls.py b/clubmanager/urls.py index 59a5a18..16c6dbe 100644 --- a/clubmanager/urls.py +++ b/clubmanager/urls.py @@ -15,4 +15,5 @@ urlpatterns = [ path("admin/login/", RedirectView.as_view(pattern_name="account_login", query_string=True), name="admin_login_redirect"), path("admin/", admin.site.urls), path("accounts/", include("allauth.urls")), + path("controlpanel/", include("controlpanel.urls")), ] diff --git a/controlpanel/__init__.py b/controlpanel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/controlpanel/admin.py b/controlpanel/admin.py new file mode 100644 index 0000000..846f6b4 --- /dev/null +++ b/controlpanel/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/controlpanel/apps.py b/controlpanel/apps.py new file mode 100644 index 0000000..36fad22 --- /dev/null +++ b/controlpanel/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ControlpanelConfig(AppConfig): + name = "controlpanel" diff --git a/controlpanel/forms.py b/controlpanel/forms.py new file mode 100644 index 0000000..bf63cb7 --- /dev/null +++ b/controlpanel/forms.py @@ -0,0 +1,37 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ + +from club.models import Club + +from .services.admins import find_member_by_email + + +class ClubForm(forms.ModelForm): + class Meta: + model = Club + fields = ["name", "slug"] + help_texts = {"slug": _("Drives the club's subdomain. Left blank, it is derived from the name.")} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["slug"].required = False + + +class ClubAdminForm(forms.Form): + """Grant club-admin rights to an email address, creating the person if new.""" + + email = forms.EmailField(label=_("Email address"), help_text=_("If this email has no account yet, one is created and they set a password via the reset link.")) + first_name = forms.CharField(label=_("First name"), required=False) + last_name = forms.CharField(label=_("Last name"), required=False) + + def clean(self): + cleaned = super().clean() + email = cleaned.get("email") + + # Only a brand-new person needs a name; an existing member already has one. + if email and find_member_by_email(email) is None: + for field in ("first_name", "last_name"): + if not cleaned.get(field): + self.add_error(field, _("Required: this email has no account yet.")) + + return cleaned diff --git a/controlpanel/migrations/__init__.py b/controlpanel/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/controlpanel/mixins.py b/controlpanel/mixins.py new file mode 100644 index 0000000..01bdcf0 --- /dev/null +++ b/controlpanel/mixins.py @@ -0,0 +1,27 @@ +from django.contrib.auth.mixins import UserPassesTestMixin +from django.http import Http404 + + +class PlatformStaffRequiredMixin(UserPassesTestMixin): + """Gate for the platform control panel. + + Two rules: + + * **Staff only.** ``is_staff`` or ``is_superuser``. Anonymous visitors are + sent to the login page; signed-in non-staff get a 403 (Django's + AccessMixin already distinguishes those two cases). Staff must also hold a + second factor — ``RequireMFAMiddleware`` enforces that, so the panel is + 2FA-protected for free. + * **Base domain only.** The panel manages *all* clubs, so it must not be + reachable from inside one. If the tenant middleware resolved a club from + the subdomain, the panel does not exist here. + """ + + def dispatch(self, request, *args, **kwargs): + if getattr(request, "club", None) is not None: + raise Http404("The control panel is not available on a club subdomain.") + return super().dispatch(request, *args, **kwargs) + + def test_func(self): + user = self.request.user + return user.is_staff or user.is_superuser diff --git a/controlpanel/models.py b/controlpanel/models.py new file mode 100644 index 0000000..6b20219 --- /dev/null +++ b/controlpanel/models.py @@ -0,0 +1 @@ +# Create your models here. diff --git a/controlpanel/services/__init__.py b/controlpanel/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/controlpanel/services/admins.py b/controlpanel/services/admins.py new file mode 100644 index 0000000..89ba187 --- /dev/null +++ b/controlpanel/services/admins.py @@ -0,0 +1,47 @@ +"""Granting and revoking club-admin rights from the platform panel.""" + +from django.contrib.auth import get_user_model +from django.db import transaction + +from club.models import ClubRole +from members.models import Member + +User = get_user_model() + + +def find_member_by_email(email): + """The Member behind a login email, if that account exists at all.""" + return Member.objects.filter(user__email__iexact=email).first() + + +@transaction.atomic +def grant_club_admin(club, email, first_name="", last_name=""): + """Make the holder of ``email`` an ADMIN of ``club``, creating them if new. + + A ClubRole hangs off a Member, and a Member optionally links to a User — so + an admin who has never existed needs both. The account is created without a + usable password; they set one via the password-reset flow. + """ + email = email.lower() + user, created_user = User.objects.get_or_create(email=email, defaults={"is_active": True}) + if created_user: + user.set_unusable_password() + user.save(update_fields=["password"]) + + member, _ = Member.objects.get_or_create( + user=user, + defaults={"first_name": first_name, "last_name": last_name}, + ) + + # One role per member per club, so promote rather than add a second row. + role, created_role = ClubRole.objects.get_or_create(club=club, member=member, defaults={"role": ClubRole.Roles.ADMIN}) + if not created_role and role.role != ClubRole.Roles.ADMIN: + role.role = ClubRole.Roles.ADMIN + role.save(update_fields=["role"]) + + return role + + +def revoke_club_admin(role): + """Remove admin rights. The membership-status sync never re-adds ADMIN.""" + role.delete() diff --git a/controlpanel/services/statistics.py b/controlpanel/services/statistics.py new file mode 100644 index 0000000..c63961a --- /dev/null +++ b/controlpanel/services/statistics.py @@ -0,0 +1,92 @@ +"""Platform and per-club statistics. + +``club_statistics`` returns a list of stat *groups*, so growing the model later +means adding an entry here and nothing else. ``clubs_with_totals`` annotates in +a single query — the club list must not fan out into N+1. +""" + +from decimal import Decimal + +from django.db.models import Count, Q, Sum +from django.utils import timezone + +from club.models import Club, ClubMembership, ClubRole, Season +from events.models import Event +from members.models import Member +from shop.models import Cart, Order +from teams.models import StaffAssignment, Team, TeamMembership + +ZERO = Decimal("0.00") + +PAID_STATUSES = (Order.OrderStatus.PAID, Order.OrderStatus.DELIVERED) +OWED_STATUSES = (Order.OrderStatus.PENDING, Order.OrderStatus.PARTIALLY_PAID) + + +def clubs_with_totals(queryset=None): + """Clubs annotated with headline counts (one query, no N+1).""" + clubs = Club.objects.all() if queryset is None else queryset + return clubs.annotate( + member_count=Count("clubmemberships__member", distinct=True), + team_count=Count("teams", distinct=True), + event_count=Count("events", distinct=True), + admin_count=Count("clubroles", filter=Q(clubroles__role=ClubRole.Roles.ADMIN), distinct=True), + ) + + +def platform_totals(): + return { + "clubs": Club.objects.active().count(), + "archived_clubs": Club.objects.archived().count(), + "members": Member.objects.count(), + "admins": ClubRole.objects.filter(role=ClubRole.Roles.ADMIN).count(), + } + + +def _money(queryset): + return queryset.aggregate(total=Sum("total"))["total"] or ZERO + + +def club_statistics(club): + """Stat groups for one club. Add new groups here as the domain grows.""" + season = Season.covering(club, timezone.localdate()) + now = timezone.now() + + memberships = ClubMembership.objects.filter(club=club) + events = Event.objects.filter(club=club) + orders = Order.objects.filter(club=club) + + return [ + { + "title": "Members", + "stats": [ + ("Members", memberships.values("member").distinct().count()), + ("Active this season", memberships.filter(season=season, status=ClubMembership.StatusChoices.ACTIVE).count() if season else 0), + ("Pending", memberships.filter(status=ClubMembership.StatusChoices.PENDING).count()), + ("Lapsed", memberships.filter(status=ClubMembership.StatusChoices.LAPSED).count()), + ], + }, + { + "title": "Teams & staff", + "stats": [ + ("Teams", Team.objects.filter(club=club).count()), + ("Players this season", TeamMembership.objects.filter(team__club=club, season=season).count() if season else 0), + ("Staff this season", StaffAssignment.objects.filter(team__club=club, season=season).count() if season else 0), + ], + }, + { + "title": "Events", + "stats": [ + ("Upcoming", events.filter(start__gte=now).count()), + ("This season", events.filter(season=season).count() if season else 0), + ], + }, + { + "title": "Shop", + "stats": [ + ("Orders", orders.count()), + ("Revenue", _money(orders.filter(status__in=PAID_STATUSES))), + ("Outstanding", _money(orders.filter(status__in=OWED_STATUSES))), + ("Open carts", Cart.objects.filter(club=club, status=Cart.CartStatus.OPEN).count()), + ], + }, + ] diff --git a/controlpanel/templates/controlpanel/base.html b/controlpanel/templates/controlpanel/base.html new file mode 100644 index 0000000..d11241c --- /dev/null +++ b/controlpanel/templates/controlpanel/base.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block title %} + {% block panel_title %}Control panel{% endblock panel_title %} · ClubManager +{% endblock title %} + +{% block main %} +
+
+

+ {% block heading %}Control panel{% endblock heading %} +

+ {% block subheading %}{% endblock subheading %} +
+
+ {% block actions %}{% endblock actions %} +
+
+ + {% block panel %}{% endblock panel %} +{% endblock main %} diff --git a/controlpanel/templates/controlpanel/club_admin_form.html b/controlpanel/templates/controlpanel/club_admin_form.html new file mode 100644 index 0000000..71413fc --- /dev/null +++ b/controlpanel/templates/controlpanel/club_admin_form.html @@ -0,0 +1,36 @@ +{% extends "controlpanel/base.html" %} +{% load ui %} + +{% block heading %}Add an admin to {{ club }}{% endblock heading %} + +{% block panel %} +
+
+
+ A club admin can manage everything in this club. They will be required to set up two-factor authentication before they can sign in. +
+
+ {% csrf_token %} + {% for error in form.non_field_errors %} +
+ {{ error }} +
+ {% endfor %} + {% for field in form %} +
+ + {{ field|daisy }} + {% if field.help_text %}{{ field.help_text }}{% endif %} + {% for error in field.errors %}{{ error }}{% endfor %} +
+ {% endfor %} +
+ Cancel + +
+
+
+
+{% endblock panel %} diff --git a/controlpanel/templates/controlpanel/club_detail.html b/controlpanel/templates/controlpanel/club_detail.html new file mode 100644 index 0000000..33582e5 --- /dev/null +++ b/controlpanel/templates/controlpanel/club_detail.html @@ -0,0 +1,89 @@ +{% extends "controlpanel/base.html" %} + +{% block heading %}{{ club.name }}{% endblock heading %} + +{% block subheading %} +

+ {{ club.slug }} + {% if club.is_archived %} + Archived + {% endif %} +

+{% endblock subheading %} + +{% block actions %} + Edit + {% if club.is_archived %} +
+ {% csrf_token %} + +
+ {% else %} +
+ {% csrf_token %} + +
+ {% endif %} +{% endblock actions %} + +{% block panel %} + {% if club.is_archived %} +
+ This club is archived: its subdomain no longer resolves. Nothing has been deleted — restore it to bring it back. +
+ {% endif %} +
+ {% for group in groups %} +
+
+

{{ group.title }}

+
+ {% for label, value in group.stats %} +
+
{{ label }}
+
{{ value }}
+
+ {% endfor %} +
+
+
+ {% endfor %} +
+
+
+
+

Club admins

+ Add admin +
+
+ + + + + + + + + + {% for role in admins %} + + + + + + {% empty %} + + + + {% endfor %} + +
NameEmail
{{ role.member }}{{ role.member.user.email|default:"—" }} +
+ {% csrf_token %} + +
+
No admins yet.
+
+
+
+{% endblock panel %} diff --git a/controlpanel/templates/controlpanel/club_form.html b/controlpanel/templates/controlpanel/club_form.html new file mode 100644 index 0000000..b2e976b --- /dev/null +++ b/controlpanel/templates/controlpanel/club_form.html @@ -0,0 +1,33 @@ +{% extends "controlpanel/base.html" %} +{% load ui %} + +{% block heading %}{% if object %}Edit {{ object }}{% else %}New club{% endif %}{% endblock heading %} + +{% block panel %} +
+
+
+ {% csrf_token %} + {% for error in form.non_field_errors %} +
+ {{ error }} +
+ {% endfor %} + {% for field in form %} +
+ + {{ field|daisy }} + {% if field.help_text %}{{ field.help_text }}{% endif %} + {% for error in field.errors %}{{ error }}{% endfor %} +
+ {% endfor %} +
+ Cancel + +
+
+
+
+{% endblock panel %} diff --git a/controlpanel/templates/controlpanel/club_list.html b/controlpanel/templates/controlpanel/club_list.html new file mode 100644 index 0000000..2e94e32 --- /dev/null +++ b/controlpanel/templates/controlpanel/club_list.html @@ -0,0 +1,61 @@ +{% extends "controlpanel/base.html" %} + +{% block heading %}{% if show_archived %}Archived clubs{% else %}Clubs{% endif %}{% endblock heading %} + +{% block actions %} + {% if show_archived %} + Active clubs + {% else %} + Archived + {% endif %} + New club +{% endblock actions %} + +{% block panel %} +
+ {% if show_archived %}{% endif %} + + +
+
+
+
+ + + + + + + + + + + + {% for club in clubs %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
ClubMembersTeamsEventsAdmins
+ {{ club.name }} +
{{ club.slug }}
+
{{ club.member_count }}{{ club.team_count }}{{ club.event_count }}{{ club.admin_count }}
+ {% if show_archived %}No archived clubs.{% else %}No clubs yet.{% endif %} +
+
+
+
+{% endblock panel %} diff --git a/controlpanel/templates/controlpanel/dashboard.html b/controlpanel/templates/controlpanel/dashboard.html new file mode 100644 index 0000000..a9f8bbd --- /dev/null +++ b/controlpanel/templates/controlpanel/dashboard.html @@ -0,0 +1,64 @@ +{% extends "controlpanel/base.html" %} + +{% block heading %}Platform overview{% endblock heading %} + +{% block actions %} + New club +{% endblock actions %} + +{% block panel %} +
+
+
Active clubs
+
{{ totals.clubs }}
+
+
+
Archived
+
{{ totals.archived_clubs }}
+
+
+
Members
+
{{ totals.members }}
+
+
+
Club admins
+
{{ totals.admins }}
+
+
+
+
+

Clubs

+
+ + + + + + + + + + + + {% for club in clubs %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
ClubMembersTeamsEventsAdmins
+ {{ club.name }} +
{{ club.slug }}
+
{{ club.member_count }}{{ club.team_count }}{{ club.event_count }}{{ club.admin_count }}
No clubs yet.
+
+
+
+{% endblock panel %} diff --git a/controlpanel/templatetags/__init__.py b/controlpanel/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/controlpanel/templatetags/ui.py b/controlpanel/templatetags/ui.py new file mode 100644 index 0000000..ca04f3d --- /dev/null +++ b/controlpanel/templatetags/ui.py @@ -0,0 +1,30 @@ +"""Template helpers for the daisyUI-based UI.""" + +from django import forms, template + +register = template.Library() + +#: daisyUI class per widget kind. Django renders widgets unstyled, so this is +#: what makes every allauth and control-panel form field look right. +WIDGET_CLASSES = ( + (forms.CheckboxInput, "checkbox"), + (forms.RadioSelect, "radio"), + (forms.Select, "select select-bordered w-full"), + (forms.Textarea, "textarea textarea-bordered w-full"), +) +DEFAULT_WIDGET_CLASS = "input input-bordered w-full" + + +@register.filter +def daisy(field): + """Render a bound form field with the right daisyUI classes.""" + widget = field.field.widget + css = next((css for widget_type, css in WIDGET_CLASSES if isinstance(widget, widget_type)), DEFAULT_WIDGET_CLASS) + + classes = [widget.attrs.get("class", ""), css] + if field.errors: + classes.append(f"{css.split()[0]}-error") + + attrs = dict(widget.attrs) + attrs["class"] = " ".join(part for part in classes if part) + return field.as_widget(attrs=attrs) diff --git a/controlpanel/tests.py b/controlpanel/tests.py new file mode 100644 index 0000000..f50ea00 --- /dev/null +++ b/controlpanel/tests.py @@ -0,0 +1,255 @@ +from decimal import Decimal + +from allauth.mfa.models import Authenticator +from django import forms +from django.contrib.auth import get_user_model +from django.test import TestCase, override_settings +from django.urls import reverse +from django.utils import timezone + +from club.models import Club, ClubMembership, ClubRole, Season +from members.models import Member +from shop.models import Order +from teams.models import Position, Team, TeamMembership + +from .services.admins import grant_club_admin +from .services.statistics import club_statistics, clubs_with_totals, platform_totals +from .templatetags.ui import daisy + +User = get_user_model() + + +def enrol_mfa(user): + return Authenticator.objects.create(user=user, type=Authenticator.Type.TOTP, data={"secret": "JBSWY3DPEHPK3PXP"}) + + +class ControlPanelTestBase(TestCase): + def setUp(self): + self.club = Club.objects.create(name="Ajax United") + self.staff = User.objects.create_user(email="root@example.com", password="pw-secret-123", is_staff=True) + # Staff must hold a second factor, else RequireMFAMiddleware redirects. + enrol_mfa(self.staff) + self.client.force_login(self.staff) + + +class AccessTests(ControlPanelTestBase): + def test_staff_can_reach_the_panel(self): + self.assertEqual(self.client.get(reverse("controlpanel:dashboard")).status_code, 200) + + def test_anonymous_is_sent_to_login(self): + self.client.logout() + + response = self.client.get(reverse("controlpanel:dashboard")) + + self.assertEqual(response.status_code, 302) + self.assertIn(reverse("account_login"), response.url) + + def test_signed_in_non_staff_gets_403(self): + self.client.force_login(User.objects.create_user(email="member@example.com", password="pw-secret-123")) + + self.assertEqual(self.client.get(reverse("controlpanel:dashboard")).status_code, 403) + + def test_superuser_can_reach_the_panel(self): + root = User.objects.create_superuser(email="super@example.com", password="pw-secret-123") + enrol_mfa(root) + self.client.force_login(root) + + self.assertEqual(self.client.get(reverse("controlpanel:dashboard")).status_code, 200) + + @override_settings(CLUBMANAGER_BASE_DOMAIN="clubmanager.app", ALLOWED_HOSTS=[".clubmanager.app"]) + def test_panel_does_not_exist_on_a_club_subdomain(self): + # It manages *all* clubs, so it must not be reachable from inside one. + Club.objects.create(name="Rival FC", slug="rival-fc") + + response = self.client.get(reverse("controlpanel:dashboard"), headers={"host": "rival-fc.clubmanager.app"}) + + self.assertEqual(response.status_code, 404) + + def test_staff_without_a_second_factor_is_sent_to_enrolment(self): + self.client.force_login(User.objects.create_user(email="nomfa@example.com", password="pw-secret-123", is_staff=True)) + + response = self.client.get(reverse("controlpanel:dashboard")) + + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, reverse("mfa_index")) + + +class ClubManagementTests(ControlPanelTestBase): + def test_dashboard_lists_clubs(self): + self.assertContains(self.client.get(reverse("controlpanel:dashboard")), "Ajax United") + + def test_create_club_derives_the_slug(self): + response = self.client.post(reverse("controlpanel:club_create"), {"name": "New Club", "slug": ""}) + + club = Club.objects.get(name="New Club") + self.assertEqual(club.slug, "new-club") + self.assertRedirects(response, reverse("controlpanel:club_detail", args=[club.pk])) + + def test_update_club(self): + self.client.post(reverse("controlpanel:club_update", args=[self.club.pk]), {"name": "Renamed", "slug": self.club.slug}) + + self.club.refresh_from_db() + self.assertEqual(self.club.name, "Renamed") + + def test_club_detail_shows_statistics(self): + response = self.client.get(reverse("controlpanel:club_detail", args=[self.club.pk])) + + self.assertContains(response, "Members") + self.assertContains(response, "Teams & staff") + self.assertContains(response, "Shop") + + def test_archive_then_restore(self): + self.client.post(reverse("controlpanel:club_archive", args=[self.club.pk])) + self.club.refresh_from_db() + self.assertTrue(self.club.is_archived) + + self.client.post(reverse("controlpanel:club_restore", args=[self.club.pk])) + self.club.refresh_from_db() + self.assertFalse(self.club.is_archived) + + def test_list_separates_active_from_archived(self): + Club.objects.create(name="Gone FC").archive() + + active = self.client.get(reverse("controlpanel:club_list")) + self.assertContains(active, "Ajax United") + self.assertNotContains(active, "Gone FC") + + archived = self.client.get(reverse("controlpanel:club_list"), {"archived": "1"}) + self.assertContains(archived, "Gone FC") + self.assertNotContains(archived, "Ajax United") + + def test_list_search(self): + Club.objects.create(name="Rival FC") + + response = self.client.get(reverse("controlpanel:club_list"), {"q": "Ajax"}) + + self.assertContains(response, "Ajax United") + self.assertNotContains(response, "Rival FC") + + +class ClubAdminManagementTests(ControlPanelTestBase): + def add_admin(self, **data): + return self.client.post(reverse("controlpanel:club_admin_add", args=[self.club.pk]), data) + + def test_granting_admin_to_a_new_email_creates_the_account(self): + self.add_admin(email="New.Admin@Example.com", first_name="Ada", last_name="Min") + + user = User.objects.get(email="new.admin@example.com") + self.assertFalse(user.has_usable_password()) # they set one via password reset + self.assertEqual(ClubRole.objects.get(club=self.club, member__user=user).role, ClubRole.Roles.ADMIN) + + def test_a_new_email_must_come_with_a_name(self): + response = self.add_admin(email="nameless@example.com", first_name="", last_name="") + + self.assertEqual(response.status_code, 200) + self.assertFalse(ClubRole.objects.exists()) + self.assertFormError(response.context["form"], "first_name", "Required: this email has no account yet.") + + def test_an_existing_member_is_promoted_rather_than_duplicated(self): + user = User.objects.create_user(email="existing@example.com", password="pw-secret-123") + member = Member.objects.create(user=user, first_name="Ex", last_name="Isting") + ClubRole.objects.create(club=self.club, member=member, role=ClubRole.Roles.MEMBER) + + self.add_admin(email="existing@example.com") + + # One role per member per club, so the MEMBER role is upgraded in place. + self.assertEqual(ClubRole.objects.get(club=self.club, member=member).role, ClubRole.Roles.ADMIN) + self.assertEqual(Member.objects.filter(user=user).count(), 1) + + def test_granting_twice_is_idempotent(self): + self.add_admin(email="ada@example.com", first_name="Ada", last_name="Min") + self.add_admin(email="ada@example.com", first_name="Ada", last_name="Min") + + self.assertEqual(ClubRole.objects.filter(club=self.club).count(), 1) + + def test_remove_admin(self): + role = grant_club_admin(self.club, "ada@example.com", "Ada", "Min") + + self.client.post(reverse("controlpanel:club_admin_remove", args=[self.club.pk, role.pk])) + + self.assertFalse(ClubRole.objects.filter(pk=role.pk).exists()) + + def test_admins_are_listed_on_the_club(self): + grant_club_admin(self.club, "ada@example.com", "Ada", "Min") + + response = self.client.get(reverse("controlpanel:club_detail", args=[self.club.pk])) + + self.assertContains(response, "Ada Min") + self.assertContains(response, "ada@example.com") + + +class StatisticsTests(TestCase): + def setUp(self): + self.club = Club.objects.create(name="Ajax United") + today = timezone.localdate() + self.season = Season.objects.create(club=self.club, start_date=today, end_date=today) + self.member = Member.objects.create(first_name="Jane", last_name="Doe") + + def groups_for(self, club): + return {group["title"]: dict(group["stats"]) for group in club_statistics(club)} + + def test_platform_totals_split_active_and_archived(self): + Club.objects.create(name="Gone FC").archive() + + totals = platform_totals() + + self.assertEqual(totals["clubs"], 1) + self.assertEqual(totals["archived_clubs"], 1) + + def test_club_totals_are_annotated(self): + Team.objects.create(club=self.club, name="First", short_name="1st") + ClubMembership.objects.create(club=self.club, member=self.member, season=self.season) + + club = clubs_with_totals().get(pk=self.club.pk) + + self.assertEqual(club.member_count, 1) + self.assertEqual(club.team_count, 1) + self.assertEqual(club.admin_count, 0) + + def test_club_statistics_count_members_teams_and_money(self): + ClubMembership.objects.create(club=self.club, member=self.member, season=self.season, status=ClubMembership.StatusChoices.ACTIVE) + team = Team.objects.create(club=self.club, name="First", short_name="1st") + position = Position.objects.create(club=self.club, name="Forward", short_name="FW") + TeamMembership.objects.create(team=team, member=self.member, season=self.season, position=position) + Order.objects.create(club=self.club, purchaser=self.member, total=Decimal("50.00"), status=Order.OrderStatus.PAID) + Order.objects.create(club=self.club, purchaser=self.member, total=Decimal("20.00"), status=Order.OrderStatus.PENDING) + + groups = self.groups_for(self.club) + + self.assertEqual(groups["Members"]["Active this season"], 1) + self.assertEqual(groups["Teams & staff"]["Players this season"], 1) + self.assertEqual(groups["Shop"]["Revenue"], Decimal("50.00")) + self.assertEqual(groups["Shop"]["Outstanding"], Decimal("20.00")) + + def test_statistics_cope_with_no_current_season(self): + # A brand-new club has no season covering today; it must not blow up. + groups = self.groups_for(Club.objects.create(name="Seasonless FC")) + + self.assertEqual(groups["Members"]["Active this season"], 0) + self.assertEqual(groups["Teams & staff"]["Players this season"], 0) + self.assertEqual(groups["Events"]["This season"], 0) + self.assertEqual(groups["Shop"]["Revenue"], Decimal("0.00")) + + +class SampleForm(forms.Form): + text = forms.CharField() + choice = forms.ChoiceField(choices=[("a", "A")]) + note = forms.CharField(widget=forms.Textarea) + agree = forms.BooleanField() + + +class DaisyFilterTests(TestCase): + def rendered(self, field_name, data=None): + form = SampleForm(data) + if data is not None: + form.is_valid() + return str(daisy(form[field_name])) + + def test_widgets_get_the_right_daisyui_class(self): + self.assertIn("input input-bordered", self.rendered("text")) + self.assertIn("select select-bordered", self.rendered("choice")) + self.assertIn("textarea textarea-bordered", self.rendered("note")) + self.assertIn("checkbox", self.rendered("agree")) + + def test_invalid_fields_get_an_error_class(self): + self.assertIn("input-error", self.rendered("text", data={})) diff --git a/controlpanel/urls.py b/controlpanel/urls.py new file mode 100644 index 0000000..5662b50 --- /dev/null +++ b/controlpanel/urls.py @@ -0,0 +1,17 @@ +from django.urls import path + +from . import views + +app_name = "controlpanel" + +urlpatterns = [ + path("", views.DashboardView.as_view(), name="dashboard"), + path("clubs/", views.ClubListView.as_view(), name="club_list"), + path("clubs/new/", views.ClubCreateView.as_view(), name="club_create"), + path("clubs//", views.ClubDetailView.as_view(), name="club_detail"), + path("clubs//edit/", views.ClubUpdateView.as_view(), name="club_update"), + path("clubs//archive/", views.ClubArchiveView.as_view(), name="club_archive"), + path("clubs//restore/", views.ClubRestoreView.as_view(), name="club_restore"), + path("clubs//admins/add/", views.ClubAdminAddView.as_view(), name="club_admin_add"), + path("clubs//admins//remove/", views.ClubAdminRemoveView.as_view(), name="club_admin_remove"), +] diff --git a/controlpanel/views.py b/controlpanel/views.py new file mode 100644 index 0000000..54f44db --- /dev/null +++ b/controlpanel/views.py @@ -0,0 +1,128 @@ +from django.contrib import messages +from django.shortcuts import get_object_or_404, redirect +from django.urls import reverse +from django.views.generic import CreateView, DetailView, FormView, ListView, TemplateView, UpdateView, View + +from club.models import Club, ClubRole + +from .forms import ClubAdminForm, ClubForm +from .mixins import PlatformStaffRequiredMixin +from .services.admins import grant_club_admin, revoke_club_admin +from .services.statistics import club_statistics, clubs_with_totals, platform_totals + + +class DashboardView(PlatformStaffRequiredMixin, TemplateView): + template_name = "controlpanel/dashboard.html" + + def get_context_data(self, **kwargs): + return super().get_context_data( + nav="dashboard", + totals=platform_totals(), + clubs=clubs_with_totals(Club.objects.active()), + **kwargs, + ) + + +class ClubListView(PlatformStaffRequiredMixin, ListView): + template_name = "controlpanel/club_list.html" + context_object_name = "clubs" + + @property + def show_archived(self): + return self.request.GET.get("archived") == "1" + + def get_queryset(self): + clubs = Club.objects.archived() if self.show_archived else Club.objects.active() + search = self.request.GET.get("q", "").strip() + if search: + clubs = clubs.filter(name__icontains=search) + return clubs_with_totals(clubs) + + def get_context_data(self, **kwargs): + return super().get_context_data(nav="clubs", show_archived=self.show_archived, search=self.request.GET.get("q", ""), **kwargs) + + +class ClubCreateView(PlatformStaffRequiredMixin, CreateView): + model = Club + form_class = ClubForm + template_name = "controlpanel/club_form.html" + + def form_valid(self, form): + response = super().form_valid(form) + messages.success(self.request, f"Club “{self.object}” created.") + return response + + def get_success_url(self): + return reverse("controlpanel:club_detail", args=[self.object.pk]) + + +class ClubUpdateView(PlatformStaffRequiredMixin, UpdateView): + model = Club + form_class = ClubForm + template_name = "controlpanel/club_form.html" + + def form_valid(self, form): + response = super().form_valid(form) + messages.success(self.request, f"Club “{self.object}” updated.") + return response + + def get_success_url(self): + return reverse("controlpanel:club_detail", args=[self.object.pk]) + + +class ClubDetailView(PlatformStaffRequiredMixin, DetailView): + model = Club + template_name = "controlpanel/club_detail.html" + context_object_name = "club" + + def get_context_data(self, **kwargs): + return super().get_context_data( + nav="clubs", + groups=club_statistics(self.object), + admins=ClubRole.objects.filter(club=self.object, role=ClubRole.Roles.ADMIN).select_related("member", "member__user"), + **kwargs, + ) + + +class ClubArchiveView(PlatformStaffRequiredMixin, View): + """Clubs are archived, never destroyed — their data (and invoices) are kept.""" + + def post(self, request, pk): + club = get_object_or_404(Club, pk=pk) + club.archive() + messages.warning(request, f"Club “{club}” archived. Its subdomain no longer resolves.") + return redirect("controlpanel:club_detail", pk=club.pk) + + +class ClubRestoreView(PlatformStaffRequiredMixin, View): + def post(self, request, pk): + club = get_object_or_404(Club, pk=pk) + club.restore() + messages.success(request, f"Club “{club}” restored.") + return redirect("controlpanel:club_detail", pk=club.pk) + + +class ClubAdminAddView(PlatformStaffRequiredMixin, FormView): + form_class = ClubAdminForm + template_name = "controlpanel/club_admin_form.html" + + @property + def club(self): + return get_object_or_404(Club, pk=self.kwargs["pk"]) + + def get_context_data(self, **kwargs): + return super().get_context_data(nav="clubs", club=self.club, **kwargs) + + def form_valid(self, form): + role = grant_club_admin(self.club, **form.cleaned_data) + messages.success(self.request, f"{role.member} is now an admin of {role.club}. They must set up two-factor authentication before they can sign in.") + return redirect("controlpanel:club_detail", pk=self.kwargs["pk"]) + + +class ClubAdminRemoveView(PlatformStaffRequiredMixin, View): + def post(self, request, pk, role_pk): + role = get_object_or_404(ClubRole, pk=role_pk, club_id=pk, role=ClubRole.Roles.ADMIN) + member = role.member + revoke_club_admin(role) + messages.warning(request, f"{member} is no longer an admin of this club.") + return redirect("controlpanel:club_detail", pk=pk) diff --git a/static/css/app.css b/static/css/app.css new file mode 100644 index 0000000..2e13205 --- /dev/null +++ b/static/css/app.css @@ -0,0 +1,2 @@ +/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-black:#000;--spacing:.25rem;--container-xs:20rem;--container-xl:36rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}@media (prefers-color-scheme:dark){:root:not([data-theme]){color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}}:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=dark]:checked),[data-theme=dark]{color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}@property --radialprogress{syntax:"";inherits:true;initial-value:0%}@property --aura-angle{syntax:"";inherits:false;initial-value:0deg}:root{scrollbar-color:currentColor #0000}@supports (color:color-mix(in lab, red, red)){:root{scrollbar-color:color-mix(in oklch, currentColor 35%, #0000) #0000}}:root{--fx-noise:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.34' numOctaves='4' stitchTiles='stitch'%3E%3C/feTurbulence%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23a)' opacity='0.2'%3E%3C/rect%3E%3C/svg%3E");--page-scroll-lock:initial;--page-overflow:var(--page-scroll-lock) hidden}:root:not(span){overflow:var(--page-overflow)}:root{--page-has-backdrop:var(--page-scroll-lock) 1;--page-scroll-bg:var(--page-scroll-lock) var(--root-bg,#0000)}@supports (color:color-mix(in lab, red, red)){:root{--page-scroll-bg:var(--page-scroll-lock) color-mix(in srgb, var(--root-bg,#0000), oklch(0% 0 0) calc(var(--page-has-backdrop,0) * 40%))}}:root{background-image:var(--page-scroll-lock) linear-gradient(var(--root-bg,#0000), var(--root-bg,#0000));transition:var(--page-scroll-lock) background-color .3s ease-out;animation:var(--page-scroll-lock) set-page-has-scroll forwards;animation-timeline:var(--page-scroll-lock) scroll();--page-has-scroll:initial;scrollbar-gutter:var(--page-has-scroll) var(--page-scroll-lock) stable}@keyframes set-page-has-scroll{0%,to{--page-has-scroll: }}:root,[data-theme]{background-color:var(--root-bg);color:var(--color-base-content)}:root{background-color:var(--page-scroll-bg,var(--root-bg))}:where(:root,[data-theme]){--root-bg:var(--color-base-100)}}@layer components;@layer utilities{@layer daisyui.l1.l2.l3{.tab:is(.tabs>.tab){cursor:pointer;appearance:none;text-align:center;webkit-user-select:none;-webkit-user-select:none;user-select:none;flex-wrap:wrap;justify-content:center;align-items:center;display:inline-flex;position:relative}@media (hover:hover){.tab:is(.tabs>.tab):hover{color:var(--color-base-content)}}.tab:is(.tabs>.tab){--tab-p:.75rem;--tab-bg:var(--color-base-100);--tab-border-color:var(--color-base-300);--tab-radius-ss:0;--tab-radius-se:0;--tab-radius-es:0;--tab-radius-ee:0;--tab-order:0;--tab-radius-min:calc(.75rem - var(--border));--tab-radius-limit:min(var(--radius-field), var(--tab-radius-min));--tab-radius-grad:#0000 calc(69% - var(--border)), var(--tab-border-color) calc(69% - var(--border) + .25px), var(--tab-border-color) 69%, var(--tab-bg) calc(69% + .25px);order:var(--tab-order);height:var(--tab-height);padding-inline:var(--tab-p);border-color:#0000;font-size:.875rem}.tab:is(.tabs>.tab):is(input[type=radio]){min-width:fit-content}.tab:is(.tabs>.tab):is(input[type=radio]):after{--tw-content:attr(aria-label);content:var(--tw-content)}.tab:is(.tabs>.tab):is(label){position:relative}.tab:is(.tabs>.tab):is(label) input{cursor:pointer;appearance:none;opacity:0;position:absolute;inset:0}:is(.tab:is(.tabs>.tab):checked,.tab:is(.tabs>.tab):is(label:has(:checked)),.tab:is(.tabs>.tab):is(.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]))+.tab-content{display:block}.tab:is(.tabs>.tab):not(:checked,label:has(:checked),:hover,.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]){color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.tab:is(.tabs>.tab):not(:checked,label:has(:checked),:hover,.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]){color:color-mix(in oklab, var(--color-base-content) 50%, transparent)}}.tab:is(.tabs>.tab):not(input):empty{cursor:default;flex-grow:1}.tab:is(.tabs>.tab):focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.tab:is(.tabs>.tab):focus{outline-offset:2px;outline:2px solid #0000}}.tab:is(.tabs>.tab):focus-visible,.tab:is(.tabs>.tab):is(label:has(:checked:focus-visible)){outline-offset:-5px;outline:2px solid}.tab:is(.tabs>.tab)[disabled]{pointer-events:none;opacity:.4}.menu{--menu-active-fg:var(--color-neutral-content);--menu-active-bg:var(--color-neutral);flex-flow:column wrap;width:fit-content;padding:.5rem;font-size:.875rem;display:flex}.menu :where(li ul,li menu){white-space:nowrap;margin-inline-start:1rem;padding-inline-start:.5rem;position:relative}.menu :where(li ul,li menu):before{background-color:var(--color-base-content);opacity:.1;width:var(--border);content:"";inset-inline-start:0;position:absolute;top:.75rem;bottom:.75rem}.menu :where(li>.menu-dropdown:not(.menu-dropdown-show)){display:none}.menu :where(li:not(.menu-title)>:not(ul,menu,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--radius-field);text-align:start;-webkit-user-select:none;user-select:none;grid-auto-columns:minmax(auto,max-content) auto max-content;grid-auto-flow:column;align-content:flex-start;align-items:center;gap:.5rem;padding-block:.375rem;padding-inline:.75rem;transition-property:color,background-color,box-shadow;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);display:grid}.menu :where(li>details>summary){--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li>details>summary){outline-offset:2px;outline:2px solid #0000}}.menu :where(li>details>summary)::-webkit-details-marker{display:none}:is(.menu :where(li>details>summary),.menu :where(li>.menu-dropdown-toggle)):after{content:"";transform-origin:50%;pointer-events:none;justify-self:flex-end;width:.375rem;height:.375rem;transition-property:rotate,translate;transition-duration:.2s;display:block;translate:0 -1px;rotate:-135deg;box-shadow:inset 2px 2px}.menu details{interpolate-size:allow-keywords;overflow:hidden}.menu details::details-content{block-size:0}@media (prefers-reduced-motion:no-preference){.menu details::details-content{transition-behavior:allow-discrete;transition-property:block-size,content-visibility;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}}.menu details[open]::details-content{block-size:auto}.menu :where(li>details[open]>summary):after,.menu :where(li>.menu-dropdown-toggle.menu-dropdown-show):after{translate:0 1px;rotate:45deg}.menu :where(li:not(.menu-title,.disabled)>:not(ul,menu,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,menu,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li:not(.menu-title,.disabled)>:not(ul,menu,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,menu,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,menu,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,menu,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{color:var(--color-base-content);--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li:not(.menu-title,.disabled)>:not(ul,menu,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,menu,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{outline-offset:2px;outline:2px solid #0000}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,menu,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li:not(.menu-title,.disabled)>:not(ul,menu,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,menu,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li:not(.menu-title,.disabled)>:not(ul,menu,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){outline-offset:2px;outline:2px solid #0000}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,menu,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){box-shadow:inset 0 1px oklch(0% 0 0/.01),inset 0 -1px oklch(100% 0 0/.01)}.menu :where(li:empty){background-color:var(--color-base-content);opacity:.1;height:1px;margin:.5rem 1rem}.menu :where(li){flex-flow:column wrap;flex-shrink:0;align-items:stretch;display:flex;position:relative}.menu :where(li) .badge{justify-self:flex-end}.menu :where(li)>:not(ul,menu,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,menu,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li)>:not(ul,menu,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,menu,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{outline-offset:2px;outline:2px solid #0000}}.menu :where(li)>:not(ul,menu,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,menu,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{color:var(--menu-active-fg);background-color:var(--menu-active-bg);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise)}:is(.menu :where(li)>:not(ul,menu,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,menu,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active):not(:is(.menu :where(li)>:not(ul,menu,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,menu,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active):active){box-shadow:0 2px calc(var(--depth) * 3px) -2px var(--menu-active-bg)}.menu :where(li).menu-disabled,.menu :where(li) [disabled]{pointer-events:none;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li).menu-disabled,.menu :where(li) [disabled]{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.menu .dropdown:focus-within .menu-dropdown-toggle:after{translate:0 1px;rotate:45deg}.menu .dropdown-content{margin-top:.5rem;padding:.5rem}.menu .dropdown-content:before{display:none}.dropdown{position-area:var(--anchor-v,bottom) var(--anchor-h,span-right);display:inline-block;position:relative}.dropdown>:not(:has(~[class*=dropdown-content])):focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.dropdown>:not(:has(~[class*=dropdown-content])):focus{outline-offset:2px;outline:2px solid #0000}}.dropdown .dropdown-content{position:absolute}.dropdown.dropdown-close .dropdown-content,.dropdown:not(details,.dropdown-open,.dropdown-hover:hover,:focus-within) .dropdown-content,.dropdown.dropdown-hover:not(:hover) [tabindex]:first-child:focus:not(:focus-visible)~.dropdown-content{transform-origin:top;opacity:0;display:none;scale:.95}.dropdown[popover],.dropdown .dropdown-content{z-index:999}@media (prefers-reduced-motion:no-preference){.dropdown[popover],.dropdown .dropdown-content{transition-behavior:allow-discrete;transition-property:opacity,scale,display,overlay;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);animation:.2s dropdown}}@starting-style{.dropdown[popover],.dropdown .dropdown-content{opacity:0;scale:.95}}:is(.dropdown:not(.dropdown-close).dropdown-open,.dropdown:not(.dropdown-close):not(.dropdown-hover):focus,.dropdown:not(.dropdown-close):focus-within)>[tabindex]:first-child{pointer-events:none}:is(.dropdown:not(.dropdown-close).dropdown-open,.dropdown:not(.dropdown-close):not(.dropdown-hover):focus,.dropdown:not(.dropdown-close):focus-within) .dropdown-content,.dropdown:not(.dropdown-close).dropdown-hover:hover .dropdown-content{opacity:1;scale:1}.dropdown:is(details){overflow:revert-layer}.dropdown:is(details) summary::-webkit-details-marker{display:none}.dropdown:where([popover]){background:0 0}.dropdown[popover]{color:inherit;position:fixed}@supports not (position-area:bottom){.dropdown[popover]{margin:auto}.dropdown[popover].dropdown-close{transform-origin:top;opacity:0;display:none;scale:.95}.dropdown[popover].dropdown-open:not(:popover-open){transform-origin:top;opacity:0;display:none;scale:.95}.dropdown[popover]::backdrop{background-color:oklab(0% none none/.3)}}:is(.dropdown[popover].dropdown-close,.dropdown[popover]:not(.dropdown-open,:popover-open)){transform-origin:top;opacity:0;display:none;scale:.95}:where(.btn){width:unset}.btn{--size:calc(var(--size-field,.25rem) * 10);--btn-p:1rem;--btn-fg:var(--color-base-content);cursor:pointer;text-align:center;vertical-align:middle;outline-offset:2px;webkit-user-select:none;-webkit-user-select:none;user-select:none;border-width:var(--border);touch-action:manipulation;--btn-bg:var(--btn-color,var(--color-base-200));--btn-border:var(--btn-color,var(--color-base-200));border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-wrap:nowrap;flex-shrink:0;justify-content:center;align-items:center;gap:.375rem;font-weight:600;transition-property:color,background-color,border-color,box-shadow,transform;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);display:inline-flex}@supports (color:color-mix(in lab, red, red)){.btn{--btn-border:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 calc(var(--depth) * 5%))}}.btn{--btn-soft-bg:initial;--btn-shadow:0 3px 2px -2px var(--btn-bg), 0 4px 3px -2px var(--btn-bg)}@supports (color:color-mix(in lab, red, red)){.btn{--btn-shadow:0 3px 2px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000), 0 4px 3px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000)}}.btn{--btn-inset:0 .5px 0 .5px oklch(100% 0 0 / calc(var(--depth) * 6%));height:var(--size);padding-inline:var(--btn-p);font-size:var(--fontsize,.875rem);background-color:var(--btn-bg);color:var(--btn-fg);border-color:var(--btn-border);border-style:var(--btn-border-style,solid);outline-color:var(--btn-color,var(--color-base-content));--tw-prose-links:var(--btn-fg);background-image:none, var(--fx-noise);background-size:auto, calc(var(--noise,0) * 100%);text-shadow:0 .5px oklch(100% 0 0 / calc(var(--depth) * .15));box-shadow:var(--btn-inset) inset, var(--btn-shadow)}.btn:is([type=checkbox],[type=radio]){appearance:none}.btn:is([type=checkbox],[type=radio])[aria-label]:after{--tw-content:attr(aria-label);content:var(--tw-content)}.collapse{border-radius:var(--radius-box,1rem);isolation:isolate;grid-template-rows:max-content 0fr;grid-template-columns:minmax(0,1fr);width:100%;display:grid;position:relative}@media (prefers-reduced-motion:no-preference){.collapse{transition:grid-template-rows .2s}}.collapse>input:is([type=checkbox],[type=radio]){appearance:none;opacity:0;z-index:1;grid-row-start:1;grid-column-start:1;width:100%;min-height:1lh;padding:1rem;padding-inline-end:3rem;transition:background-color .2s ease-out}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close)),.collapse:not(.collapse-close):has(>input:is([type=checkbox],[type=radio]):checked){grid-template-rows:max-content 1fr}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>.collapse-content,.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){--overflow-delay:.2s;overflow:revert-layer;content-visibility:visible;min-height:fit-content}@supports not (content-visibility:visible){.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>.collapse-content,.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){visibility:visible}}.collapse:focus-visible,.collapse:has(>input:is([type=checkbox],[type=radio]):focus-visible),.collapse:has(summary:focus-visible){outline-color:var(--color-base-content);outline-offset:2px;outline-width:2px;outline-style:solid}.collapse:not(.collapse-close)>input[type=checkbox],.collapse:not(.collapse-close)>input[type=radio]:not(:checked),.collapse:not(.collapse-close)>.collapse-title{cursor:pointer}:is(.collapse[tabindex]:focus:not(.collapse-close,.collapse[open]),.collapse[tabindex]:focus-within:not(.collapse-close,.collapse[open]))>.collapse-title{cursor:unset}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>:where(.collapse-content),.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){padding-bottom:1rem}.collapse:is(details){width:100%}.collapse:is(details)::details-content{--overflow-delay:0s;height:0;overflow:clip}.collapse:is(details):where([open])::details-content{overflow:revert-layer;height:auto}@media (prefers-reduced-motion:no-preference){.collapse:is(details)::details-content{transition:overflow .2s allow-discrete var(--overflow-delay), content-visibility .2s allow-discrete, visibility .2s allow-discrete, min-height .2s ease-out allow-discrete, padding .1s ease-out 20ms, background-color .2s ease-out, height .2s;interpolate-size:allow-keywords}.collapse:is(details):where([open])::details-content{--overflow-delay:.2s}}.collapse:is(details)>summary{outline:none;display:block;position:relative}.collapse:is(details)>summary::-webkit-details-marker{display:none}.collapse-content{--overflow-delay:0s;content-visibility:hidden;min-height:0;cursor:unset;grid-row-start:2;grid-column-start:1;padding-left:1rem;padding-right:1rem;overflow:clip}@supports not (content-visibility:hidden){.collapse-content{visibility:hidden}}@media (prefers-reduced-motion:no-preference){.collapse-content{transition:overflow .2s allow-discrete var(--overflow-delay), content-visibility .2s allow-discrete, visibility .2s allow-discrete, min-height .2s ease-out allow-discrete, padding .1s ease-out 20ms, background-color .2s ease-out}}details>.collapse-content{content-visibility:visible}.filter{flex-wrap:wrap;display:flex}.filter [type=radio]{width:auto}.filter input{opacity:1;transition:visibility .1s allow-discrete, margin .1s, opacity .3s, padding .3s, border-width .1s;overflow:hidden;scale:1}.filter input.filter-reset{aspect-ratio:1}.filter input.filter-reset:after{--tw-content:"×";content:var(--tw-content)}.filter>input:not(:last-child),.filter>:not(:last-child) input{margin-inline-end:.25rem}.list{flex-direction:column;font-size:.875rem;display:flex}.list .list-row{--list-grid-cols:minmax(0, auto) 1fr;border-radius:var(--radius-box);word-break:break-word;grid-auto-flow:column;grid-template-columns:var(--list-grid-cols);gap:1rem;padding:1rem;display:grid;position:relative}:is(.list>:not(:last-child).list-row,.list>:not(:last-child) .list-row):after{content:"";border-bottom:var(--border) solid;inset-inline:var(--radius-box);border-color:var(--color-base-content);position:absolute;bottom:0}@supports (color:color-mix(in lab, red, red)){:is(.list>:not(:last-child).list-row,.list>:not(:last-child) .list-row):after{border-color:color-mix(in oklab, var(--color-base-content) 5%, transparent)}}.toggle{border:var(--border) solid currentColor;color:var(--input-color);cursor:pointer;appearance:none;vertical-align:middle;webkit-user-select:none;-webkit-user-select:none;user-select:none;--radius-selector-max:calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector));border-radius:calc(var(--radius-selector) + min(var(--toggle-p), var(--radius-selector-max)) + min(var(--border), var(--radius-selector-max)));padding:var(--toggle-p);flex-shrink:0;grid-template-columns:0fr 1fr 1fr;place-content:center;display:inline-grid;position:relative;box-shadow:inset 0 1px}@supports (color:color-mix(in lab, red, red)){.toggle{box-shadow:0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000) inset}}.toggle{--input-color:var(--color-base-content);transition:color .3s,grid-template-columns .2s}@supports (color:color-mix(in lab, red, red)){.toggle{--input-color:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}.toggle{--toggle-p:calc(var(--size) * .125);--size:calc(var(--size-selector,.25rem) * 6);width:calc((var(--size) * 2) - (var(--border) + var(--toggle-p)) * 2);height:var(--size)}.toggle>*{z-index:1;cursor:pointer;appearance:none;background-color:#0000;border:none;grid-column:2/span 1;grid-row-start:1;height:100%;padding:.125rem;transition:opacity .2s,rotate .4s}.toggle>:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.toggle>:focus{outline-offset:2px;outline:2px solid #0000}}.toggle>:nth-child(2){color:var(--color-base-100);rotate:0deg}.toggle>:nth-child(3){color:var(--color-base-100);opacity:0;rotate:-15deg}.toggle:has(:checked)>:nth-child(2){opacity:0;rotate:15deg}.toggle:has(:checked)>:nth-child(3){opacity:1;rotate:0deg}.toggle:before{aspect-ratio:1;border-radius:var(--radius-selector);--tw-content:"";content:var(--tw-content);width:100%;height:100%;box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px currentColor;background-color:currentColor;grid-row-start:1;grid-column-start:2;transition:background-color .1s,translate .2s,inset-inline-start .2s;position:relative;inset-inline-start:0;translate:0}@supports (color:color-mix(in lab, red, red)){.toggle:before{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000)}}.toggle:before{background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise)}@media (forced-colors:active){.toggle:before{outline-style:var(--tw-outline-style);outline-offset:calc(1px * -1);outline-width:1px}}@media print{.toggle:before{outline-offset:-1rem;outline:.25rem solid}}.toggle:focus-visible,.toggle:has(:focus-visible){outline-offset:2px;outline:2px solid}.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked){background-color:var(--color-base-100);--input-color:var(--color-base-content);grid-template-columns:1fr 1fr 0fr}:is(.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked)):before{background-color:currentColor}@starting-style{:is(.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked)):before{opacity:0}}.toggle:indeterminate{grid-template-columns:.5fr 1fr .5fr}.toggle:disabled{cursor:not-allowed;opacity:.3}.toggle:disabled:before{border:var(--border) solid currentColor;background-color:#0000}.input{appearance:none;background-color:var(--color-base-100);vertical-align:middle;white-space:nowrap;--size:calc(var(--size-field,.25rem) * var(--in-size-mul));--input-color:var(--color-base-content);flex-shrink:1;align-items:center;gap:.5rem;padding-inline:.75rem;display:inline-flex;position:relative}@supports (color:color-mix(in lab, red, red)){.input{--input-color:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}.input{cursor:text;width:clamp(3rem,20rem,100%);height:var(--size);font-size:max(var(--font-size,0rem), var(--font-size-min));touch-action:manipulation;border:var(--border) solid var(--input-color,#0000);box-shadow:0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field))}@supports (color:color-mix(in lab, red, red)){.input{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset}}.input input{appearance:none;background-color:#0000;border:none;width:100%;height:100%}.input input::placeholder{color:var(--color-base-content);opacity:.5}.input input:focus,.input input:focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.input input:focus,.input input:focus-within{outline-offset:2px;outline:2px solid #0000}}.input input::-webkit-calendar-picker-indicator{inset-inline-end:-.15em}.input::-webkit-inner-spin-button{margin-inline-end:-10px}.input::-webkit-calendar-picker-indicator{inset-inline-end:.75em}input.input,.input input{display:inline-flex;position:relative}:is(input.input,.input input)[type=url],:is(input.input,.input input)[type=email]{text-align:left}:is(input.input,.input input)::-webkit-datetime-edit{min-height:100%;text-align:inherit;align-items:center;display:grid}:is(input.input,.input input)::-webkit-date-and-time-value{min-height:100%;text-align:inherit;align-items:center;display:grid}:is(input.input,.input input)::-webkit-inner-spin-button{margin-block:calc(.25rem * var(--spin-my))}:is(input.input,.input input)::-webkit-calendar-picker-indicator{cursor:pointer;width:1em;height:1em;position:absolute}:is(input.input,.input input)::-webkit-color-swatch-wrapper{padding-block:.25rem}.input:focus,.input:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.input:focus,.input:focus-within{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)}}.input:focus,.input:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}@media (pointer:coarse){@supports (-webkit-touch-callout:none){.input:focus,.input:focus-within{--font-size:1rem}}}.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200)}:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input):is(input),:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input) :is(input){color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input):is(input),:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input) :is(input){color:color-mix(in oklab, var(--color-base-content) 40%, transparent)}}.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{box-shadow:none}:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input)::placeholder,:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input) ::placeholder{color:var(--color-base-content);opacity:.2}.input:has(>input[disabled])>input[disabled]{cursor:not-allowed}.table{border-collapse:separate;--tw-border-spacing-x:0;--tw-border-spacing-y:0;width:100%;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y);border-radius:var(--radius-box);text-align:left;font-size:.875rem;position:relative}.table:where(:dir(rtl),[dir=rtl],[dir=rtl] *){text-align:right}@media (hover:hover){:is(.table tr.row-hover,.table tr.row-hover:nth-child(2n)):hover{background-color:var(--color-base-200)}}.table :where(th,td){vertical-align:middle;padding-block:.75rem;padding-inline:1rem}.table :where(thead,tfoot){white-space:nowrap;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(thead,tfoot){color:color-mix(in oklab, var(--color-base-content) 60%, transparent)}}.table :where(thead,tfoot){font-size:.875rem;font-weight:600}.table :where(tfoot tr:first-child :is(td,th)){border-top:var(--border) solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(tfoot tr:first-child :is(td,th)){border-top:var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000)}}.table :where(.table-pin-rows thead tr){z-index:1;background-color:var(--color-base-100);position:sticky;top:0}.table :where(.table-pin-rows tfoot tr){z-index:1;background-color:var(--color-base-100);position:sticky;bottom:0}.table :where(.table-pin-cols tr th){background-color:var(--color-base-100);position:sticky;left:0;right:0}.table :where(thead tr :is(td,th),tbody tr:not(:last-child) :is(td,th)){border-bottom:var(--border) solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(thead tr :is(td,th),tbody tr:not(:last-child) :is(td,th)){border-bottom:var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000)}}.select{appearance:none;background-color:var(--color-base-100);vertical-align:middle;--size:calc(var(--size-field,.25rem) * var(--sl-size-mul));--input-color:var(--color-base-content);flex-shrink:1;align-items:center;gap:.375rem;padding-inline:.75rem 1.75rem;display:inline-flex;position:relative}@supports (color:color-mix(in lab, red, red)){.select{--input-color:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}.select{width:clamp(3rem,20rem,100%);height:var(--size);font-size:max(var(--font-size,0rem), var(--font-size-min));touch-action:manipulation;white-space:nowrap;text-overflow:ellipsis;border:var(--border) solid var(--input-color,#0000);box-shadow:0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;background-image:linear-gradient(45deg,#0000 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,#0000 50%);background-position:calc(100% - 20px) calc(1px + 50%),calc(100% - 16.1px) calc(1px + 50%);background-repeat:no-repeat;background-size:4px 4px,4px 4px;border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));overflow:hidden}@supports (color:color-mix(in lab, red, red)){.select{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset}}[dir=rtl] .select{background-position:12px calc(1px + 50%),16px calc(1px + 50%)}[dir=rtl] .select::picker(select){translate:.5rem}[dir=rtl] .select select::picker(select){translate:.5rem}.select[multiple]{background-image:none;height:auto;padding-block:.75rem;padding-inline-end:.75rem;overflow:auto}.select select{appearance:none;width:calc(100% + 2.75rem);height:calc(100% - calc(var(--border) * 2));background:inherit;border-radius:inherit;border-style:none;align-items:center;margin-inline:-.75rem -1.75rem;padding-inline:.75rem 1.75rem}.select select::placeholder{color:var(--color-base-content);opacity:.5}.select select:focus,.select select:focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.select select:focus,.select select:focus-within{outline-offset:2px;outline:2px solid #0000}}.select select:not(:last-child){background-image:none;margin-inline-end:-1.375rem}:is(.select:focus,.select:focus-within,.select:open){--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){:is(.select:focus,.select:focus-within,.select:open){box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)}}:is(.select:focus,.select:focus-within,.select:open){outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate;background-image:linear-gradient(135deg,#0000 50%,currentColor 50%),linear-gradient(45deg,currentColor 50%,#0000 50%)}.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select{cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200)}:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select):is(select),:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select) :is(select){color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select):is(select),:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select) :is(select){color:color-mix(in oklab, var(--color-base-content) 40%, transparent)}}.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select{box-shadow:none}:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select)::placeholder,:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select) ::placeholder{color:var(--color-base-content);opacity:.2}.select:has(>select[disabled])>select[disabled]{cursor:not-allowed}@supports (appearance:base-select){:is(.select,.select select){appearance:base-select}:is(.select,.select select)::picker(select){appearance:base-select}}:is(.select,.select select)::picker(select){color:inherit;border:var(--border) solid var(--color-base-200);border-radius:var(--radius-box);background-color:inherit;max-height:min(24rem,70dvh);box-shadow:0 2px calc(var(--depth) * 3px) -2px oklch(0% 0 0/.2);box-shadow:0 20px 25px -5px rgb(0 0 0/calc(var(--depth) * .1)), 0 8px 10px -6px rgb(0 0 0/calc(var(--depth) * .1));margin-block:.5rem;margin-inline:.5rem;padding:.5rem;translate:-.5rem}:is(.select,.select select)::picker-icon{display:none}:is(.select,.select select) selectedcontent{text-overflow:ellipsis;white-space:nowrap;width:100%;overflow:hidden}:is(.select,.select select) optgroup{padding-top:.5em}:is(.select,.select select) optgroup option:first-child{margin-top:.5em}:is(.select,.select select) option{border-radius:var(--radius-field);padding-block:.375rem;padding-inline:calc(.25rem * var(--option-px));white-space:normal;transition-property:color,background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{outline-offset:2px;outline:2px solid #0000}}:is(.select,.select select) option:not(:disabled):active{background-color:var(--color-neutral);color:var(--color-neutral-content);box-shadow:0 2px calc(var(--depth) * 3px) -2px var(--color-neutral)}.collapse-title{grid-row-start:1;grid-column-start:1;width:100%;min-height:1lh;padding:1rem;padding-inline-end:3rem;transition:background-color .2s ease-out;position:relative}.checkbox{border:var(--border) solid var(--input-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.checkbox{border:var(--border) solid var(--input-color,color-mix(in oklab, var(--color-base-content) 20%, #0000))}}.checkbox{cursor:pointer;appearance:none;border-radius:var(--radius-selector);vertical-align:middle;color:var(--color-base-content);box-shadow:0 1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 0 #0000 inset, 0 0 #0000;--size:calc(var(--size-selector,.25rem) * 6);width:var(--size);height:var(--size);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);flex-shrink:0;padding:.25rem;transition:background-color .2s,box-shadow .2s;display:inline-block;position:relative}.checkbox:before{--tw-content:"";content:var(--tw-content);opacity:0;clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 80%,70% 80%,70% 100%);width:100%;height:100%;box-shadow:0px 3px 0 0px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;background-color:currentColor;font-size:1rem;line-height:.75;transition:clip-path .3s .1s,opacity .1s .1s,rotate .3s .1s,translate .3s .1s;display:block;rotate:45deg}.checkbox:focus-visible{outline:2px solid var(--input-color,currentColor);outline-offset:2px}.checkbox:checked,.checkbox[aria-checked=true]{background-color:var(--input-color,#0000);box-shadow:0 0 #0000 inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * .1))}:is(.checkbox:checked,.checkbox[aria-checked=true]):before{clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 0%,70% 0%,70% 100%);opacity:1}@media (forced-colors:active){:is(.checkbox:checked,.checkbox[aria-checked=true]):before{--tw-content:"✔︎";clip-path:none;background-color:#0000;rotate:0deg}}@media print{:is(.checkbox:checked,.checkbox[aria-checked=true]):before{--tw-content:"✔︎";clip-path:none;background-color:#0000;rotate:0deg}}.checkbox:indeterminate{background-color:var(--input-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.checkbox:indeterminate{background-color:var(--input-color,color-mix(in oklab, var(--color-base-content) 20%, #0000))}}.checkbox:indeterminate:before{opacity:1;clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 80%,80% 80%,80% 100%);translate:0 -35%;rotate:0deg}.radio{cursor:pointer;appearance:none;vertical-align:middle;border:var(--border) solid var(--input-color,currentColor);border-radius:3.40282e38px;flex-shrink:0;padding:.25rem;display:inline-block;position:relative}@supports (color:color-mix(in lab, red, red)){.radio{border:var(--border) solid var(--input-color,color-mix(in srgb, currentColor 20%, #0000))}}.radio{box-shadow:0 1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset;--size:calc(var(--size-selector,.25rem) * 6);width:var(--size);height:var(--size);color:var(--input-color,currentColor)}.radio:before{--tw-content:"";content:var(--tw-content);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);border-radius:3.40282e38px;width:100%;height:100%;display:block}.radio:focus-visible{outline:2px solid}.radio:checked,.radio[aria-checked=true]{background-color:var(--color-base-100);border-color:currentColor}@media (prefers-reduced-motion:no-preference){.radio:checked,.radio[aria-checked=true]{animation:.2s ease-out radio}}:is(.radio:checked,.radio[aria-checked=true]):before{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * .1));background-color:currentColor}@media (forced-colors:active){:is(.radio:checked,.radio[aria-checked=true]):before{outline-style:var(--tw-outline-style);outline-offset:calc(1px * -1);outline-width:1px}}@media print{:is(.radio:checked,.radio[aria-checked=true]):before{outline-offset:-1rem;outline:.25rem solid}}.navbar{align-items:center;width:100%;min-height:4rem;padding:.5rem;display:flex}.card{border-radius:var(--radius-box);outline-offset:2px;outline:2px solid #0000;flex-direction:column;transition:outline .2s ease-in-out;display:flex;position:relative}.card:focus-visible,.card[aria-checked=true],.card:has(>:checked,>:is([type=checkbox],[type=radio]):focus-visible){outline-color:currentColor}.card:has(>:checked:focus-visible),.card[aria-checked=true]:focus-visible,.card[aria-checked=true]:has(>:is([type=checkbox],[type=radio]):focus-visible){outline-width:4px}.card:has(>:is([type=checkbox],[type=radio])){cursor:pointer;-webkit-user-select:none;user-select:none}.card>:is([type=checkbox],[type=radio]){appearance:none}.stats{border-radius:var(--radius-box);grid-auto-flow:column;display:inline-grid;position:relative;overflow-x:auto}.textarea{appearance:none;border-radius:var(--radius-field);background-color:var(--color-base-100);vertical-align:middle;--input-color:var(--color-base-content);flex-shrink:1;min-height:5rem;padding-block:.5rem;padding-inline:.75rem}@supports (color:color-mix(in lab, red, red)){.textarea{--input-color:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}.textarea{width:clamp(3rem,20rem,100%);font-size:max(var(--font-size,0rem), var(--font-size-min));touch-action:manipulation;border:var(--border) solid var(--input-color,#0000);box-shadow:0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset}@supports (color:color-mix(in lab, red, red)){.textarea{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset}}.textarea textarea{appearance:none;background-color:#0000;border:none}.textarea textarea::placeholder{color:var(--color-base-content);opacity:.5}.textarea textarea:focus,.textarea textarea:focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.textarea textarea:focus,.textarea textarea:focus-within{outline-offset:2px;outline:2px solid #0000}}.textarea:focus,.textarea:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.textarea:focus,.textarea:focus-within{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)}}.textarea:focus,.textarea:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}@media (pointer:coarse){@supports (-webkit-touch-callout:none){.textarea:focus,.textarea:focus-within{--font-size:1rem}}}.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){color:color-mix(in oklab, var(--color-base-content) 40%, transparent)}}.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){box-shadow:none}:is(.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]))::placeholder,:is(.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled])) ::placeholder{color:var(--color-base-content);opacity:.2}.textarea:has(>textarea[disabled])>textarea[disabled]{cursor:not-allowed}.stack{grid-template-rows:3px 4px 1fr 4px 3px;grid-template-columns:3px 4px 1fr 4px 3px;display:inline-grid}.stack>*{width:100%;height:100%}.stack>:nth-child(n+2){opacity:.7;width:100%}.stack>:nth-child(2){z-index:2;opacity:.9}.stack>:first-child{z-index:3;width:100%}.hero{background-position:50%;background-size:cover;place-items:center;width:100%;display:grid}.hero>*{grid-row-start:1;grid-column-start:1}.stat-value{white-space:nowrap;grid-column-start:1;font-size:2rem;font-weight:800}.stat-title{white-space:nowrap;color:var(--color-base-content);grid-column-start:1}@supports (color:color-mix(in lab, red, red)){.stat-title{color:color-mix(in oklab, var(--color-base-content) 60%, transparent)}}.stat-title{font-size:.75rem}.divider{white-space:nowrap;height:1rem;margin:var(--divider-m,1rem 0);--divider-color:var(--color-base-content);flex-direction:row;align-self:stretch;align-items:center;display:flex}@supports (color:color-mix(in lab, red, red)){.divider{--divider-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.divider:before,.divider:after{content:"";background-color:var(--divider-color);flex-grow:1;width:100%;height:.125rem}@media print{.divider:before,.divider:after{border:.5px solid}}.divider:not(:empty){gap:1rem}.label{white-space:nowrap;color:currentColor;align-items:center;gap:.375rem;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.label{color:color-mix(in oklab, currentcolor 60%, transparent)}}.label:has(input){cursor:pointer}.label:is(.input>*,.select>*){white-space:nowrap;height:calc(100% - .5rem);font-size:inherit;align-items:center;padding-inline:.75rem;display:flex}.label:is(.input>*,.select>*):first-child{border-inline-end:var(--border) solid currentColor;margin-inline:-.75rem .75rem}@supports (color:color-mix(in lab, red, red)){.label:is(.input>*,.select>*):first-child{border-inline-end:var(--border) solid color-mix(in oklab, currentColor 10%, #0000)}}.label:is(.input>*,.select>*):last-child{border-inline-start:var(--border) solid currentColor;margin-inline:.75rem -.75rem}@supports (color:color-mix(in lab, red, red)){.label:is(.input>*,.select>*):last-child{border-inline-start:var(--border) solid color-mix(in oklab, currentColor 10%, #0000)}}.status{aspect-ratio:1;border-radius:var(--radius-selector);background-color:var(--color-base-content);width:.5rem;height:.5rem;display:inline-block}@supports (color:color-mix(in lab, red, red)){.status{background-color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.status{vertical-align:middle;color:#0000004d;background-position:50%;background-repeat:no-repeat}@supports (color:color-mix(in lab, red, red)){.status{color:color-mix(in oklab, var(--color-black) 30%, transparent)}}.status{background-image:radial-gradient(circle at 35% 30%, oklch(1 0 0 / calc(var(--depth) * .5)), #0000);box-shadow:0 2px 3px -1px}@supports (color:color-mix(in lab, red, red)){.status{box-shadow:0 2px 3px -1px color-mix(in oklab, currentColor calc(var(--depth) * 100%), #0000)}}.badge{border-radius:var(--radius-selector);vertical-align:middle;color:var(--badge-fg);border:var(--border) solid var(--badge-color,var(--color-base-200));background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);background-color:var(--badge-bg);--badge-bg:var(--badge-color,var(--color-base-100));--badge-fg:var(--color-base-content);--size:calc(var(--size-selector,.25rem) * 6);width:fit-content;height:var(--size);padding-inline:calc(var(--size) / 2 - var(--border));justify-content:center;align-items:center;gap:.5rem;font-size:.875rem;display:inline-flex}.tabs{--tabs-height:auto;--tabs-direction:row;--tab-height:calc(var(--size-field,.25rem) * 10);height:var(--tabs-height);flex-wrap:wrap;flex-direction:var(--tabs-direction);display:flex}.stat{grid-template-columns:repeat(1,1fr);column-gap:1rem;width:100%;padding-block:1rem;padding-inline:1.5rem;display:inline-grid}.stat:not(:last-child){border-inline-end:var(--border) dashed currentColor}@supports (color:color-mix(in lab, red, red)){.stat:not(:last-child){border-inline-end:var(--border) dashed color-mix(in oklab, currentColor 10%, #0000)}}.stat:not(:last-child){border-block-end:none}.card-body{padding:var(--card-p,1.5rem);font-size:var(--card-fs,.875rem);flex-direction:column;flex:auto;gap:.5rem;display:flex}.alert{--alert-border-color:var(--color-base-200);border-radius:var(--radius-box);color:var(--color-base-content);background-color:var(--alert-color,var(--color-base-200));text-align:start;background-size:auto, calc(var(--noise) * 33%);background-image:none, var(--fx-noise);box-shadow:0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * .08)) inset, 0 1px #000, 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * .08));border-style:solid;grid-template-columns:auto;grid-auto-flow:column;justify-content:start;place-items:center start;gap:1rem;padding-block:.75rem;padding-inline:1rem;font-size:.875rem;line-height:1.25rem;display:grid}@supports (color:color-mix(in lab, red, red)){.alert{box-shadow:0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * .08)) inset, 0 1px color-mix(in oklab, color-mix(in oklab, #000 20%, var(--alert-color,var(--color-base-200))) calc(var(--depth) * 20%), #0000), 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * .08))}}.alert:has(:nth-child(2)){grid-template-columns:auto minmax(auto,1fr)}.card-actions{flex-wrap:wrap;align-items:flex-start;gap:.5rem;display:flex}.card-title{font-size:var(--cardtitle-fs,1.125rem);align-items:center;gap:.5rem;font-weight:600;display:flex}.link{cursor:pointer;text-decoration-line:underline}.link:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.link:focus{outline-offset:2px;outline:2px solid #0000}}.link:focus-visible{outline-offset:2px;outline:2px solid}.btn-ghost{--btn-bg:#0000;color:var(--btn-rest-fg,var(--btn-color,var(--color-base-content,currentColor)));--btn-border:#0000;--btn-inset:0 0 0 0 oklch(0% 0 0/0);--btn-shadow:0 0 0 0 oklch(0% 0 0/0);background-image:none}}@layer daisyui.l1.l2{.collapse-arrow>.collapse-title:after{width:.5rem;height:.5rem;display:block;position:absolute;transform:translateY(-100%)rotate(45deg)}@media (prefers-reduced-motion:no-preference){.collapse-arrow>.collapse-title:after{transition-property:all;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}}.collapse-arrow>.collapse-title:after{content:"";transform-origin:75% 75%;pointer-events:none;top:50%;inset-inline-end:1.4rem;box-shadow:2px 2px}@media (prefers-reduced-motion:no-preference){.collapse[open].collapse-arrow>.collapse-title:after,.collapse.collapse-open.collapse-arrow>.collapse-title:after{transform:translateY(-50%)rotate(225deg)}}.collapse.collapse-open.collapse-plus>.collapse-title:after{--tw-content:"−";content:var(--tw-content)}:is(.collapse[tabindex].collapse-arrow:focus:not(.collapse-close),.collapse.collapse-arrow[tabindex]:focus-within:not(.collapse-close))>.collapse-title:after,.collapse.collapse-arrow:not(.collapse-close)>input:is([type=checkbox],[type=radio]):checked~.collapse-title:after{transform:translateY(-50%)rotate(225deg)}.collapse[open].collapse-plus>.collapse-title:after,.collapse[tabindex].collapse-plus:focus:not(.collapse-close)>.collapse-title:after,.collapse.collapse-plus:not(.collapse-close)>input:is([type=checkbox],[type=radio]):checked~.collapse-title:after{--tw-content:"−";content:var(--tw-content)}.list .list-row:has(.list-col-grow:first-child){--list-grid-cols:1fr}.list .list-row:has(.list-col-grow:nth-child(2)){--list-grid-cols:minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(3)){--list-grid-cols:minmax(0, auto) minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(4)){--list-grid-cols:minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(5)){--list-grid-cols:minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(6)){--list-grid-cols:minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr}.list .list-row>*{grid-row-start:1}.input{--in-size-mul:10;--font-size-min:.875rem;--spin-my:-3}.floating-label:has(.input){--top-mul:5;--font-size:.875rem}.select{--sl-size-mul:10;--font-size-min:.875rem;--option-px:3}.floating-label:has(.select){--top-mul:5;--font-size:.875rem}.checkbox:disabled,.radio:disabled{cursor:not-allowed;opacity:.2}:where(.navbar){position:relative}.dropdown-end{--anchor-h:span-left}.dropdown-end :where(.dropdown-content){inset-inline-end:0;translate:0}[dir=rtl] :is(.dropdown-end :where(.dropdown-content)){translate:0}.dropdown-end.dropdown-left{--anchor-h:left;--anchor-v:span-top}.dropdown-end.dropdown-left .dropdown-content{top:auto;bottom:0}.dropdown-end.dropdown-right{--anchor-h:right;--anchor-v:span-top}.dropdown-end.dropdown-right .dropdown-content{top:auto;bottom:0}.textarea{--font-size-min:.875rem}.floating-label:has(.textarea){--top-mul:5;--font-size:.875rem}:is(.stack,.stack.stack-bottom)>*{grid-area:3/3/6/4}:is(.stack,.stack.stack-bottom)>:nth-child(2){grid-area:2/2/5/5}:is(.stack,.stack.stack-bottom)>:first-child{grid-area:1/1/4/6}.stack.stack-top>*{grid-area:1/3/4/4}.stack.stack-top>:nth-child(2){grid-area:2/2/5/5}.stack.stack-top>:first-child{grid-area:3/1/6/6}.stack.stack-start>*{grid-area:3/1/4/4}.stack.stack-start>:nth-child(2){grid-area:2/2/5/5}.stack.stack-start>:first-child{grid-area:1/3/6/6}.stack.stack-end>*{grid-area:3/3/4/6}.stack.stack-end>:nth-child(2){grid-area:2/2/5/5}.stack.stack-end>:first-child{grid-area:1/1/6/4}.btn-circle{width:var(--size);height:var(--size);border-radius:3.40282e38px;padding-inline:0}.badge-sm{--size:calc(var(--size-selector,.25rem) * 5);font-size:.75rem}.alert-error{color:var(--color-error-content);--alert-border-color:var(--color-error);--alert-color:var(--color-error)}.alert-info{color:var(--color-info-content);--alert-border-color:var(--color-info);--alert-color:var(--color-info)}.alert-success{color:var(--color-success-content);--alert-border-color:var(--color-success);--alert-color:var(--color-success)}.alert-warning{color:var(--color-warning-content);--alert-border-color:var(--color-warning);--alert-color:var(--color-warning)}.link-hover{text-decoration-line:none}@media (hover:hover){.link-hover:hover{text-decoration-line:underline}}.btn-error{--btn-color:var(--color-error);--btn-fg:var(--color-error-content);--btn-soft-bg:initial}.btn-primary{--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content);--btn-soft-bg:initial}.btn-sm{--fontsize:.75rem;--btn-p:.75rem;--size:calc(var(--size-field,.25rem) * 8)}.btn-success{--btn-color:var(--color-success);--btn-fg:var(--color-success-content);--btn-soft-bg:initial}.btn-warning{--btn-color:var(--color-warning);--btn-fg:var(--color-warning-content);--btn-soft-bg:initial}.btn-xs{--fontsize:.6875rem;--btn-p:.5rem;--size:calc(var(--size-field,.25rem) * 6)}.badge-neutral{--badge-color:var(--color-neutral);--badge-fg:var(--color-neutral-content)}.badge-warning{--badge-color:var(--color-warning);--badge-fg:var(--color-warning-content)}}@layer daisyui.l1{.btn:where(:checked:not(.filter [type=radio].btn)){--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content)}@media (hover:hover){.btn:hover{--btn-bg:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn:hover{--btn-bg:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 7%)}}.btn:hover{color:var(--btn-fg);--btn-border:var(--btn-bg)}@supports (color:color-mix(in lab, red, red)){.btn:hover{--btn-border:color-mix(in oklab, var(--btn-bg), #000 calc(var(--depth) * 5%))}}.btn:hover{--btn-border-style:solid;--btn-inset:0 .5px 0 .5px oklch(100% 0 0 / calc(var(--depth) * 6%));--btn-shadow:0 3px 2px -2px var(--btn-bg), 0 4px 3px -2px var(--btn-bg)}@supports (color:color-mix(in lab, red, red)){.btn:hover{--btn-shadow:0 3px 2px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000), 0 4px 3px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000)}}}.btn:active:not(.btn-active){--btn-bg:var(--btn-color,var(--color-base-200));translate:0 .5px}@supports (color:color-mix(in lab, red, red)){.btn:active:not(.btn-active){--btn-bg:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 5%)}}.btn:active:not(.btn-active){color:var(--btn-fg,var(--color-base-content));--btn-border:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn:active:not(.btn-active){--btn-border:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 7%)}}.btn:active:not(.btn-active){--btn-border-style:solid;--btn-inset:0 0 0 0 oklch(0% 0 0/0);--btn-shadow:0 0 0 0 oklch(0% 0 0/0)}.btn:where(:checked:not(.filter [type=radio].btn),:not([type=radio],[type=checkbox]):focus-visible){--btn-bg:var(--btn-color,var(--color-base-200));color:var(--btn-fg,var(--color-base-content));--btn-border:var(--btn-bg)}@supports (color:color-mix(in lab, red, red)){.btn:where(:checked:not(.filter [type=radio].btn),:not([type=radio],[type=checkbox]):focus-visible){--btn-border:color-mix(in oklab, var(--btn-bg), #000 calc(var(--depth) * 5%))}}.btn:where(:checked:not(.filter [type=radio].btn),:not([type=radio],[type=checkbox]):focus-visible){--btn-border-style:solid;--btn-inset:0 .5px 0 .5px oklch(100% 0 0 / calc(var(--depth) * 6%));--btn-shadow:0 3px 2px -2px var(--btn-bg), 0 4px 3px -2px var(--btn-bg)}@supports (color:color-mix(in lab, red, red)){.btn:where(:checked:not(.filter [type=radio].btn),:not([type=radio],[type=checkbox]):focus-visible){--btn-shadow:0 3px 2px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000), 0 4px 3px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000)}}.btn:where(:checked:not(.filter [type=radio].btn),:not([type=radio],[type=checkbox]):focus-visible){isolation:isolate}.btn:focus-visible,.btn:has(:focus-visible){isolation:isolate;outline-width:2px;outline-style:solid}.filter:not(:has(:checked:not(.filter-reset))) :is(.filter-reset,[type=reset]):not(:focus-visible){visibility:hidden}.filter:not(:has(:checked:not(.filter-reset))) :is(.filter-reset,[type=reset]):not(:focus-visible),.filter:not(:has(:focus-visible)):has(:checked:not(.filter-reset,[type=checkbox])) :is(input,button):not(:checked,.filter-reset,[type=reset]){opacity:0;border-width:0;width:0;margin-inline:0;padding-inline:0;scale:0}}@layer daisyui{.btn:is(:disabled,[disabled],[aria-disabled=true]){pointer-events:none;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn:is(:disabled,[disabled],[aria-disabled=true]){color:color-mix(in oklch, var(--color-base-content) 20%, #0000)}}.btn:is(:disabled,[disabled],[aria-disabled=true]){--btn-bg:#0000;--btn-border:#0000;--btn-inset:0 0 0 0 oklch(0% 0 0/0);--btn-shadow:0 0 0 0 oklch(0% 0 0/0);background-image:none}.btn:is(:disabled,[disabled],[aria-disabled=true]):not(.btn-link,.btn-ghost){background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn:is(:disabled,[disabled],[aria-disabled=true]):not(.btn-link,.btn-ghost){background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.btn-link{--btn-bg:#0000;color:var(--btn-color,var(--color-primary));--btn-border:#0000;--btn-inset:0 0 0 0 oklch(0% 0 0/0);--btn-shadow:0 0 0 0 oklch(0% 0 0/0);background-image:none;text-decoration-line:underline}}.collapse:not(td,tr,colgroup){visibility:revert-layer}.collapse{visibility:collapse}.visible{visibility:visible}@layer daisyui.l1.l2.l3.l4{.card figure:first-child{border-start-start-radius:inherit;border-start-end-radius:inherit;border-end-end-radius:unset;border-end-start-radius:unset;overflow:hidden}.card figure:last-child{border-start-start-radius:unset;border-start-end-radius:unset;border-end-end-radius:inherit;border-end-start-radius:inherit;overflow:hidden}.card figure{justify-content:center;align-items:center;display:flex}.card-body p{flex-grow:1}}.static{position:static}.z-10{z-index:10}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-2{margin-block:calc(var(--spacing) * 2)}.my-3{margin-block:calc(var(--spacing) * 3)}.my-4{margin-block:calc(var(--spacing) * 4)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-2{margin-left:calc(var(--spacing) * 2)}.alert{border-width:var(--border);border-color:var(--alert-border-color,var(--color-base-200))}:root .prose{--tw-prose-body:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-body:color-mix(in oklab, var(--color-base-content) 80%, #0000)}}:root .prose{--tw-prose-headings:var(--color-base-content);--tw-prose-lead:var(--color-base-content);--tw-prose-links:var(--color-base-content);--tw-prose-bold:var(--color-base-content);--tw-prose-counters:var(--color-base-content);--tw-prose-bullets:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-bullets:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}:root .prose{--tw-prose-hr:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-hr:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}:root .prose{--tw-prose-quotes:var(--color-base-content);--tw-prose-quote-borders:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-quote-borders:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}:root .prose{--tw-prose-captions:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-captions:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}:root .prose{--tw-prose-code:var(--color-base-content);--tw-prose-pre-code:var(--color-neutral-content);--tw-prose-pre-bg:var(--color-neutral);--tw-prose-th-borders:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-th-borders:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}:root .prose{--tw-prose-td-borders:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-td-borders:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}:root .prose{--tw-prose-kbd:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-kbd:color-mix(in oklab, var(--color-base-content) 80%, #0000)}}:root .prose :where(code):not(pre>code){background-color:var(--color-base-200);border-radius:var(--radius-selector);border:var(--border) solid var(--color-base-300);font-weight:inherit;padding-block:.2em;padding-inline:.5em}:root .prose :where(code):not(pre>code):before,:root .prose :where(code):not(pre>code):after{display:none}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.table{display:table}.h-5{height:calc(var(--spacing) * 5)}.min-h-screen{min-height:100vh}.w-5{width:calc(var(--spacing) * 5)}.w-56{width:calc(var(--spacing) * 56)}.w-fit{width:fit-content}.w-full{width:100%}.max-w-5xl{max-width:var(--container-5xl)}.max-w-none{max-width:none}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.flex-1{flex:1}.flex-none{flex:none}.cursor-pointer{cursor:pointer}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-base-200>:not(:last-child)){border-color:var(--color-base-200)}.overflow-x-auto{overflow-x:auto}.rounded-box{border-radius:var(--radius-box);border-radius:var(--radius-box)}.border{border-style:var(--tw-border-style);border-width:1px}.border-base-300{border-color:var(--color-base-300)}.bg-base-100{background-color:var(--color-base-100)}.bg-base-200{background-color:var(--color-base-200)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-2{padding-block:calc(var(--spacing) * 2)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.text-center{text-align:center}.text-right{text-align:right}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-base-content\/70{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/70{color:color-mix(in oklab, var(--color-base-content) 70%, transparent)}}.text-base-content\/80{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/80{color:color-mix(in oklab, var(--color-base-content) 80%, transparent)}}.text-error{color:var(--color-error)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.prose :where(a.btn:not(.btn-link)):not(:where([class~=not-prose],[class~=not-prose] *)){text-decoration-line:none}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow,.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}@media (min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}}@keyframes dropdown{0%{opacity:0}}@keyframes rotator{89.9999%,to{--first-item-position:0 0%}90%,99.9999%{--first-item-position:0 calc(var(--items) * 100%)}to{translate:0 -100%}}@keyframes radio{0%{padding:5px}50%{padding:3px}}@keyframes toast{0%{opacity:0;scale:.9}to{opacity:1;scale:1}}@keyframes skeleton{0%{background-position:150%}to{background-position:-50%}}@keyframes progress{50%{background-position-x:-115%}}@keyframes rating{0%,40%{filter:brightness(1.05)contrast(1.05);scale:1.1}}@keyframes aura{to{--aura-angle:360deg;transform:translateZ(1px)}}@keyframes aura-glow{20%,80%{opacity:.7;filter:blur(.25rem)}50%{opacity:1;filter:blur(.75rem)}}@keyframes aura-glow-after{20%,80%{opacity:.3;filter:blur(1rem)}50%{opacity:.6;filter:blur(1.5rem)}}@keyframes menu{0%{opacity:0}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false} \ No newline at end of file diff --git a/templates/allauth/controlpanel/services/__init__.py b/templates/allauth/controlpanel/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/templates/allauth/controlpanel/templatetags/__init__.py b/templates/allauth/controlpanel/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/templates/allauth/elements/alert.html b/templates/allauth/elements/alert.html new file mode 100644 index 0000000..e57a824 --- /dev/null +++ b/templates/allauth/elements/alert.html @@ -0,0 +1,4 @@ +{% load allauth %} +
+ {% slot message %}{% endslot %} +
diff --git a/templates/allauth/elements/badge.html b/templates/allauth/elements/badge.html new file mode 100644 index 0000000..432e80c --- /dev/null +++ b/templates/allauth/elements/badge.html @@ -0,0 +1 @@ +{% load allauth %}{% slot %}{% endslot %} diff --git a/templates/allauth/elements/button.html b/templates/allauth/elements/button.html new file mode 100644 index 0000000..e4a04e9 --- /dev/null +++ b/templates/allauth/elements/button.html @@ -0,0 +1,13 @@ +{% load allauth %} +{% comment %} djlint:off {% endcomment %} +<{% if attrs.href %}a href="{{ attrs.href }}"{% else %}button{% endif %} +class="btn {% if attrs.tags and 'danger' in attrs.tags %}btn-error{% elif attrs.tags and 'secondary' in attrs.tags %}btn-ghost{% elif attrs.tags and 'link' in attrs.tags %}btn-link{% else %}btn-primary{% endif %}" +{% if attrs.form %}form="{{ attrs.form }}"{% endif %} +{% if attrs.id %}id="{{ attrs.id }}"{% endif %} +{% if attrs.name %}name="{{ attrs.name }}"{% endif %} +{% if attrs.value %}value="{{ attrs.value }}"{% endif %} +{% if attrs.type %}type="{{ attrs.type }}"{% endif %} +> +{% slot %} +{% endslot %} + diff --git a/templates/allauth/elements/button_group.html b/templates/allauth/elements/button_group.html new file mode 100644 index 0000000..f0ed9cc --- /dev/null +++ b/templates/allauth/elements/button_group.html @@ -0,0 +1,4 @@ +{% load allauth %} +
+ {% slot %}{% endslot %} +
diff --git a/templates/allauth/elements/details.html b/templates/allauth/elements/details.html new file mode 100644 index 0000000..ceab2e8 --- /dev/null +++ b/templates/allauth/elements/details.html @@ -0,0 +1,5 @@ +{% load allauth %} +
+ {% slot summary %}{% endslot %} +
{% slot body %}{% endslot %}
+
diff --git a/templates/allauth/elements/fields.html b/templates/allauth/elements/fields.html new file mode 100644 index 0000000..4ea96bd --- /dev/null +++ b/templates/allauth/elements/fields.html @@ -0,0 +1,24 @@ +{% load ui %} +{% for field in attrs.form.hidden_fields %}{{ field }}{% endfor %} +{% for error in attrs.form.non_field_errors %} +
+ {{ error }} +
+{% endfor %} +{% for field in attrs.form.visible_fields %} +
+ {% if field.field.widget.input_type == "checkbox" %} + + {% else %} + + {{ field|daisy }} + {% endif %} + {% if field.help_text %}{{ field.help_text }}{% endif %} + {% for error in field.errors %}{{ error }}{% endfor %} +
+{% endfor %} diff --git a/templates/allauth/elements/form.html b/templates/allauth/elements/form.html new file mode 100644 index 0000000..38ea765 --- /dev/null +++ b/templates/allauth/elements/form.html @@ -0,0 +1,9 @@ +{% load allauth %} +
+ {% slot body %}{% endslot %} +
+ {% slot actions %}{% endslot %} +
+
diff --git a/templates/allauth/elements/h1.html b/templates/allauth/elements/h1.html new file mode 100644 index 0000000..a3feddd --- /dev/null +++ b/templates/allauth/elements/h1.html @@ -0,0 +1 @@ +{% comment %} djlint:off {% endcomment %}{% load allauth %}

{% slot %}{% endslot %}

diff --git a/templates/allauth/elements/h2.html b/templates/allauth/elements/h2.html new file mode 100644 index 0000000..0018ab0 --- /dev/null +++ b/templates/allauth/elements/h2.html @@ -0,0 +1 @@ +{% comment %} djlint:off {% endcomment %}{% load allauth %}

{% slot %}{% endslot %}

diff --git a/templates/allauth/elements/hr.html b/templates/allauth/elements/hr.html new file mode 100644 index 0000000..31684e3 --- /dev/null +++ b/templates/allauth/elements/hr.html @@ -0,0 +1 @@ +{% comment %} djlint:off {% endcomment %}{% load allauth %}
diff --git a/templates/allauth/elements/p.html b/templates/allauth/elements/p.html new file mode 100644 index 0000000..04cd76c --- /dev/null +++ b/templates/allauth/elements/p.html @@ -0,0 +1 @@ +{% comment %} djlint:off {% endcomment %}{% load allauth %}

{% slot %}{% endslot %}

diff --git a/templates/allauth/elements/panel.html b/templates/allauth/elements/panel.html new file mode 100644 index 0000000..f295669 --- /dev/null +++ b/templates/allauth/elements/panel.html @@ -0,0 +1,8 @@ +{% load allauth %} +
+
+ {% if slots.title %}

{% slot title %}{% endslot %}

{% endif %} +
{% slot body %}{% endslot %}
+ {% if slots.actions %}
{% slot actions %}{% endslot %}
{% endif %} +
+
diff --git a/templates/allauth/elements/table.html b/templates/allauth/elements/table.html new file mode 100644 index 0000000..bf01d82 --- /dev/null +++ b/templates/allauth/elements/table.html @@ -0,0 +1,6 @@ +{% load allauth %} +
+ + {% slot %}{% endslot %} +
+
diff --git a/templates/allauth/layouts/base.html b/templates/allauth/layouts/base.html new file mode 100644 index 0000000..e731b7b --- /dev/null +++ b/templates/allauth/layouts/base.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block title %} + {% block head_title %}{% endblock head_title %} · ClubManager +{% endblock title %} + +{% block main %} +
+
+
+ {% block content %}{% endblock content %} +
+
+
+{% endblock main %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..42602f9 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,86 @@ +{% load static %} + + + + + + + {% block title %}ClubManager{% endblock title %} + + {# Apply the stored theme before first paint, otherwise the page flashes + the wrong colours. With no stored preference we set nothing, so + daisyUI's `dark --prefersdark` follows the OS. #} + + + {% block extra_head %}{% endblock extra_head %} + + + + {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} +
+ {% block main %}{% endblock main %} +
+ + +