commit c5cc2efbac8965f57bc25f9a316898347ffc17f4 Author: Mateusz Gruszczyński Date: Mon Mar 23 15:56:18 2026 +0100 first commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4c55da8 --- /dev/null +++ b/.env.example @@ -0,0 +1,73 @@ +# Backend application +APP_NAME=PV Insight +APP_VERSION=1.3.0 +APP_TIMEZONE=Europe/Warsaw +APP_HOST=0.0.0.0 +APP_PORT=8105 +APP_SECRET_KEY=change-me +APP_SESSION_COOKIE_NAME=pv_insight_session +APP_SQLITE_PATH=./data/pv_insight.sqlite3 + +# Site +SITE_NAME=Domowa instalacja PV +PV_INSTALLED_POWER_KWP=9.99 +CO2_FACTOR_KG_PER_KWH=0.72 + +# InfluxDB +INFLUXDB_SCHEME=http +INFLUXDB_HOST=127.0.0.1 +INFLUXDB_PORT=8086 +INFLUXDB_DATABASE=ha +INFLUXDB_USER= +INFLUXDB_PASSWORD= +INFLUXDB_VERIFY_SSL=false +INFLUXDB_TIMEOUT_SECONDS=15 + +# Auth +AUTH_ENABLED=true +AUTH_USERNAME=admin +AUTH_PASSWORD=change-me +AUTH_DISPLAY_NAME=Operator +AUTH_SESSION_MAX_AGE_SECONDS=43200 +AUTH_COOKIE_SECURE=false +AUTH_COOKIE_SAMESITE=Lax + +CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173,http://localhost:4173,http://127.0.0.1:4173 + +# Frontend defaults +FRONTEND_DEFAULT_TAB=realtime +FRONTEND_THEME=dark +FRONTEND_LANGUAGE=pl + +# PV metric entities used by backend +PV_AC_POWER_ENTITY=sofarsolar_ac_power +PV_AC_POWER_MEASUREMENT=W +PV_TOTAL_ENERGY_ENTITY=sofarsolar_energy_total +PV_TOTAL_ENERGY_MEASUREMENT=kWh +PV_INVERTER_TEMP_ENTITY=sofarsolar_temprature_inverter +PV_INVERTER_TEMP_MEASUREMENT=°C + +# DC strings +PV_STRING_1_LABEL=DC1 +PV_STRING_1_POWER_ENTITY=sofarsolar_dc1_power +PV_STRING_1_POWER_MEASUREMENT=W +PV_STRING_1_VOLTAGE_ENTITY=sofarsolar_dc1_voltage +PV_STRING_1_VOLTAGE_MEASUREMENT=V + +PV_STRING_2_LABEL=DC2 +PV_STRING_2_POWER_ENTITY=sofarsolar_dc2_power +PV_STRING_2_POWER_MEASUREMENT=W +PV_STRING_2_VOLTAGE_ENTITY=sofarsolar_dc2_voltage +PV_STRING_2_VOLTAGE_MEASUREMENT=V + +PV_STRING_3_LABEL=DC3 +PV_STRING_3_POWER_ENTITY= +PV_STRING_3_POWER_MEASUREMENT=W +PV_STRING_3_VOLTAGE_ENTITY= +PV_STRING_3_VOLTAGE_MEASUREMENT=V + +PV_STRING_4_LABEL=DC4 +PV_STRING_4_POWER_ENTITY= +PV_STRING_4_POWER_MEASUREMENT=W +PV_STRING_4_VOLTAGE_ENTITY= +PV_STRING_4_VOLTAGE_MEASUREMENT=V diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549b394 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.venv +node_modules +*.sqlite3 +*.zip +venv +__pycache__ +.env +frontend/dist/* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a1ca86f --- /dev/null +++ b/README.md @@ -0,0 +1,169 @@ +# PV Insight + +Dashboard fotowoltaiki pod InfluxDB, z naciskiem na prosty dev na Pythonie 3.14 i gotowy deploy produkcyjny bez zewnetrznego CDN. + +## Najwazniejsze cechy + +- dev uruchamiany zwyklym `python run.py` +- prod uruchamiany przez `waitress`, bez serwera developerskiego Flask +- frontend `React + TypeScript + Vite + ECharts` +- stylowanie w oparciu o **Tabler 1.4.0**, zwendorowane **offline** w repo +- tryb jasny / ciemny +- interfejs PL / EN +- logowanie sesyjne +- tryb kiosku z recznym doborem widgetow +- lokalny cache historii w SQLite +- import archiwum z InfluxDB chunkami + +## Co aplikacja liczy sama + +Aplikacja nie wymaga encji typu `energy_today`, `energy_monthly` czy `energy_yearly`. +Najpierw liczy energie z `PV_TOTAL_ENERGY_ENTITY` jako licznika calkowitego, a jesli go nie ma, moze estymowac dane z `PV_AC_POWER_ENTITY`. + +## Struktura projektu + +- `backend/config.py` - glowna konfiguracja, encje i moduly +- `backend/run.py` - start developerski +- `backend/run_prod.py` - start produkcyjny przez Waitress +- `backend/backfill.py` - reczny import historii +- `backend/app/routes` - endpointy API +- `backend/app/services` - logika Influx, energii, auth i historii +- `backend/app/storage/sqlite_repository.py` - lokalny cache historii +- `frontend/src` - UI operatora, kiosk, logowanie, i18n +- `frontend/public/vendor/tabler` - lokalny, offline Tabler +- `frontend/nginx.conf` - serwowanie SPA i proxy `/api` w prod +- `scripts/dev_*.sh` - start developerski +- `scripts/prod_*.sh` - build / start produkcyjny + +## Konfiguracja + +1. Skopiuj `.env.example` do `.env` +2. Uzupelnij polaczenie do InfluxDB +3. Ustaw encje PV +4. Ustaw login i haslo do panelu + +Najwazniejsze pola: + +- `INFLUXDB_*` +- `PV_AC_POWER_ENTITY` +- `PV_TOTAL_ENERGY_ENTITY` +- `PV_STRING_1_*`, `PV_STRING_2_*`, `PV_STRING_3_*`, `PV_STRING_4_*` +- `PV_INVERTER_TEMP_ENTITY` opcjonalnie +- `APP_SQLITE_PATH` +- `HISTORY_*` +- `AUTH_USERNAME` +- `AUTH_PASSWORD` lub `AUTH_PASSWORD_HASH` +- `APP_SECRET_KEY` +- `AUTH_COOKIE_SECURE` + +Dla HTTPS w produkcji ustaw `AUTH_COOKIE_SECURE=true`. Flask wyraznie zaznacza, ze w produkcji nie nalezy uzywac wbudowanego dev servera, tylko dedykowany serwer WSGI, a Waitress dobrze pasuje do tego projektu jako prosty, czysto pythonowy serwer WSGI. + +## Dev bez Dockera + +### Backend +```bash +cp .env.example .env +./scripts/dev_backend.sh +``` + +### Frontend +W drugim terminalu: +```bash +./scripts/dev_frontend.sh +``` + +### Razem +```bash +./scripts/dev.sh +``` + +### Demo UI bez backendu +```bash +./scripts/dev_frontend_demo.sh +``` + +## Lokalny smoke-test builda bez Dockera + +### Backend przez Waitress +```bash +./scripts/prod_backend.sh +``` + +### Build frontendu +```bash +./scripts/prod_frontend_build.sh +``` + +### Podglad zbudowanego frontendu +```bash +./scripts/prod_frontend_preview.sh +``` + +Po buildzie frontend statyczny bedzie w `frontend/dist`. To jest dobre do lokalnego sprawdzenia builda. Docelowy deploy produkcyjny jest opisany nizej w sekcji Docker / deploy produkcyjny. + +## Docker / deploy produkcyjny + +Domyslny `docker-compose.yml` jest przygotowany pod prosty deploy produkcyjny: + +- backend: `Flask + Waitress` +- frontend: `nginx` serwujacy build Vite +- `nginx` proxuje `/api/*` do backendu +- Tabler jest lokalny, wiec nie ma zaleznosci od CDN + +Start: +```bash +docker compose up -d --build +``` + +UI bedzie pod adresem: +```text +http://localhost:8080 +``` + +Zatrzymanie: +```bash +./scripts/prod_down.sh +``` + +## Docker dev + +Jesli chcesz kontenery developerskie: +```bash +docker compose -f docker-compose.dev.yml up --build +``` + +## Import historii + +Z UI: zakladka `Ustawienia` -> `Import archiwalny z InfluxDB` + +Z CLI: +```bash +./scripts/import_history.sh --start-date 2022-01-01 --end-date 2025-12-31 --chunk-days 7 +``` + +## Uwagi + +- jesli masz mniej stringow, ustaw tylko istniejace encje +- gdy temperatura falownika nie istnieje, modul temperatury schowa sie automatycznie +- kiosk zapisuje widok lokalnie w `localStorage` +- backend trzyma cache dziennych agregatow w SQLite +- frontend w dev na porcie `5173` automatycznie gada z backendem na tym samym hoscie, port `8105` +- frontend w prod korzysta domyslnie z `/api/v1`, czyli przez proxy nginx + + +## + +cd backend +export FLASK_APP=app.main:app + +python -m flask create-admin --username admin2 --password 'SuperHaslo123' +python -m flask create-user --username user1 --password 'SuperHaslo123' +python -m flask reset-password --username user1 --password 'NoweHaslo123' + +## Produkcja + +Uruchomienie produkcyjne przez reverse proxy: + +```bash +docker compose -f docker-compose.prod.yml up -d --build +``` diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..aad14bb --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,13 @@ +.venv +venv +__pycache__ +*.pyc +*.pyo +*.pyd +*.log +.env +data +*.sqlite3 +*.db +*.db-shm +*.db-wal diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..464cdfc --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.14-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r /app/requirements.txt + +COPY . /app + +EXPOSE 8105 + +CMD ["python", "run_prod.py"] diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev new file mode 100644 index 0000000..774f270 --- /dev/null +++ b/backend/Dockerfile.dev @@ -0,0 +1,12 @@ +FROM python:3.14-slim + +WORKDIR /app + +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r /app/requirements.txt + +COPY . /app + +EXPOSE 8105 + +CMD ["python", "run.py"] diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..4535d67 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1,3 @@ +from app.main import app + +__all__ = ["app"] diff --git a/backend/app/app_factory.py b/backend/app/app_factory.py new file mode 100644 index 0000000..b61b259 --- /dev/null +++ b/backend/app/app_factory.py @@ -0,0 +1,193 @@ +from __future__ import annotations + +import logging +import os +from logging.config import dictConfig + +import click +from flask import Flask, jsonify, make_response, request, session +from werkzeug.exceptions import HTTPException + +from app.core_settings import get_settings +from app.routes import ( + analytics_blueprint, + auth_blueprint, + dashboard_blueprint, + health_blueprint, + historical_blueprint, + realtime_blueprint, +) +from app.services.auth import get_auth_service +from app.services.historical_sync import get_historical_sync_service + + +def configure_logging(debug: bool) -> None: + level = "DEBUG" if debug else "INFO" + dictConfig( + { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": {"format": "%(asctime)s | %(levelname)s | %(name)s | %(message)s"} + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "default", + "level": level, + } + }, + "root": {"handlers": ["console"], "level": level}, + } + ) + + +def create_app() -> Flask: + settings = get_settings() + configure_logging(settings.debug) + + app = Flask(__name__) + app.config["JSON_SORT_KEYS"] = False + get_auth_service().configure_app(app) + + app.register_blueprint(health_blueprint) + app.register_blueprint(auth_blueprint, url_prefix=settings.api_prefix) + app.register_blueprint(dashboard_blueprint, url_prefix=settings.api_prefix) + app.register_blueprint(realtime_blueprint, url_prefix=settings.api_prefix) + app.register_blueprint(analytics_blueprint, url_prefix=settings.api_prefix) + app.register_blueprint(historical_blueprint, url_prefix=settings.api_prefix) + + @app.get("/") + def index(): + return { + "app": settings.app_name, + "version": settings.version, + "api_prefix": settings.api_prefix, + "message": "PV Insight backend is running", + } + + @app.before_request + def handle_preflight_and_auth(): + if request.method == "OPTIONS": + response = make_response("", 204) + return _apply_cors(response) + + if not settings.auth["enabled"]: + return None + + if request.path in {"/", "/health", "/favicon.ico"}: + return None + + if request.path.startswith(f"{settings.api_prefix}/auth/"): + return None + + public_kiosk = request.args.get("publicKiosk") == "1" + public_kiosk_allowed_paths = { + f"{settings.api_prefix}/dashboard/config", + f"{settings.api_prefix}/dashboard/kiosk-settings", + f"{settings.api_prefix}/realtime/snapshot", + f"{settings.api_prefix}/realtime/history", + f"{settings.api_prefix}/analytics/production", + f"{settings.api_prefix}/analytics/distribution", + } + + if public_kiosk and request.method == "GET" and request.path in public_kiosk_allowed_paths: + return None + + if request.path.startswith(settings.api_prefix) and "auth_user" not in session: + return _apply_cors(make_response(jsonify({"detail": "Authentication required"}), 401)) + + return None + + @app.after_request + def append_cors_headers(response): + return _apply_cors(response) + + @app.errorhandler(HTTPException) + def handle_http_exception(exc: HTTPException): + response = jsonify({"detail": exc.description}) + return _apply_cors(make_response(response, exc.code or 500)) + + @app.errorhandler(Exception) + def handle_exception(exc: Exception): + logging.getLogger(__name__).exception("Unhandled application error") + response = {"detail": str(exc) if settings.debug else "Internal server error"} + return _apply_cors(make_response(response, 500)) + + _register_cli_commands(app) + _bootstrap_background_services(settings.debug) + return app + + +def _register_cli_commands(app: Flask) -> None: + auth_service = get_auth_service() + + @app.cli.command("create-admin") + @click.option("--username", required=True, help="Login") + @click.option("--password", required=True, hide_input=True, confirmation_prompt=True, help="Password") + @click.option("--display-name", default=None, help="Name") + def create_admin_command(username: str, password: str, display_name: str | None): + try: + user = auth_service.create_user( + username=username, + password=password, + role="admin", + display_name=display_name, + ) + click.echo(f"Utworzono konto admina: {user.username}") + except ValueError as exc: + raise click.ClickException(str(exc)) from exc + except Exception as exc: # pragma: no cover + raise click.ClickException(f"Cant create admin account: {exc}") from exc + + @app.cli.command("create-user") + @click.option("--username", required=True, help="Login") + @click.option("--password", required=True, hide_input=True, confirmation_prompt=True, help="Password") + @click.option("--display-name", default=None, help="Name") + def create_user_command(username: str, password: str, display_name: str | None): + try: + user = auth_service.create_user( + username=username, + password=password, + role="user", + display_name=display_name, + ) + click.echo(f"Admin created: {user.username}") + except ValueError as exc: + raise click.ClickException(str(exc)) from exc + except Exception as exc: # pragma: no cover + raise click.ClickException(f"Cant create user account: {exc}") from exc + + @app.cli.command("reset-password") + @click.option("--username", required=True, help="Login") + @click.option("--password", required=True, hide_input=True, confirmation_prompt=True, help="Password") + def reset_password_command(username: str, password: str): + try: + user = auth_service.reset_password(username=username, new_password=password) + click.echo(f"Passowrd reseted for: {user.username}") + except ValueError as exc: + raise click.ClickException(str(exc)) from exc + except Exception as exc: # pragma: no cover + raise click.ClickException(f"Can't password reset: {exc}") from exc + + +def _bootstrap_background_services(debug: bool) -> None: + should_run = (not debug) or os.environ.get("WERKZEUG_RUN_MAIN") == "true" + if not should_run: + return + get_historical_sync_service().start_scheduler_if_enabled() + + +def _apply_cors(response): + settings = get_settings() + origin = request.headers.get("Origin") + allowed = settings.cors_origins + if origin and (origin in allowed or "*" in allowed): + response.headers["Access-Control-Allow-Origin"] = origin + response.headers.add("Vary", "Origin") + elif "*" in allowed: + response.headers["Access-Control-Allow-Origin"] = "*" + response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization" + response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT" + response.headers["Access-Control-Allow-Credentials"] = "true" + return response diff --git a/backend/app/core_settings.py b/backend/app/core_settings.py new file mode 100644 index 0000000..b83acdd --- /dev/null +++ b/backend/app/core_settings.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from dataclasses import dataclass +from functools import lru_cache + +import config +from app.models.definitions import MetricDefinition + + +@dataclass(frozen=True) +class AppSettings: + app_name: str + version: str + debug: bool + api_prefix: str + timezone: str + host: str + port: int + site_name: str + installed_power_kwp: float + co2_factor: float + influx: dict + storage: dict + cors_origins: list[str] + modules: dict + realtime: dict + time_ranges: dict + analytics: dict + history: dict + strings: list[dict] + status_metrics: list[str] + visible_entity_table: list[str] + frontend_defaults: dict + auth: dict + i18n: dict + metrics: dict[str, MetricDefinition] + + +@lru_cache(maxsize=1) +def get_settings() -> AppSettings: + metric_catalog = { + metric_id: MetricDefinition(id=metric_id, **payload) + for metric_id, payload in config.METRICS.items() + } + return AppSettings( + app_name=config.APP_CONFIG["name"], + version=config.APP_CONFIG["version"], + debug=config.APP_CONFIG["debug"], + api_prefix=config.APP_CONFIG["api_prefix"], + timezone=config.APP_CONFIG["timezone"], + host=config.APP_CONFIG["host"], + port=config.APP_CONFIG["port"], + site_name=config.SITE_CONFIG["site_name"], + installed_power_kwp=config.SITE_CONFIG["installed_power_kwp"], + co2_factor=config.SITE_CONFIG["co2_factor_kg_per_kwh"], + influx=config.INFLUXDB_CONFIG, + storage=config.STORAGE_CONFIG, + cors_origins=config.CORS_ORIGINS, + modules=config.MODULES, + realtime=config.REALTIME, + time_ranges=config.TIME_RANGES, + analytics=config.ANALYTICS, + history=config.HISTORY, + strings=config.STRINGS, + status_metrics=config.STATUS_METRICS, + visible_entity_table=config.VISIBLE_ENTITY_TABLE, + frontend_defaults=config.FRONTEND_DEFAULTS, + auth=config.AUTH_CONFIG, + i18n=config.I18N, + metrics=metric_catalog, + ) diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..a7a3cab --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from app.app_factory import create_app + +app = create_app() diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..b80271b --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,33 @@ +from .definitions import ( + AnalyticsSummary, + BucketPoint, + DailyEnergyRecord, + HeroCard, + HistoricalActivityEvent, + HistoricalChunkProgress, + HistoricalCoverage, + HistoricalImportStatus, + MetricDefinition, + MetricValue, + SeriesPayload, + SeriesPoint, + SnapshotGroupRow, + SnapshotPayload, +) + +__all__ = [ + "AnalyticsSummary", + "BucketPoint", + "DailyEnergyRecord", + "HeroCard", + "HistoricalActivityEvent", + "HistoricalChunkProgress", + "HistoricalCoverage", + "HistoricalImportStatus", + "MetricDefinition", + "MetricValue", + "SeriesPayload", + "SeriesPoint", + "SnapshotGroupRow", + "SnapshotPayload", +] diff --git a/backend/app/models/definitions.py b/backend/app/models/definitions.py new file mode 100644 index 0000000..2d067f3 --- /dev/null +++ b/backend/app/models/definitions.py @@ -0,0 +1,174 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import date, datetime +from typing import Any, Literal + +MetricKind = Literal["gauge", "counter", "text"] + + +@dataclass(frozen=True) +class MetricDefinition: + id: str + entity_id: str + measurement: str + unit: str = "" + label: str = "" + kind: MetricKind = "gauge" + precision: int = 2 + enabled: bool = True + + +@dataclass +class MetricValue: + metric_id: str + label: str + unit: str = "" + value: float | str | None = None + timestamp: datetime | None = None + precision: int = 2 + kind: MetricKind = "gauge" + status: str = "neutral" + + +@dataclass +class SeriesPoint: + timestamp: datetime + value: float | None = None + + +@dataclass +class SeriesPayload: + metric_id: str + label: str + unit: str + points: list[SeriesPoint] = field(default_factory=list) + + +@dataclass +class SnapshotGroupRow: + id: str + label: str + values: dict[str, MetricValue] = field(default_factory=dict) + meta: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class HeroCard: + metric_id: str + label: str + value: float | str | None = None + unit: str = "" + accent: str = "neutral" + subtitle: str = "" + + +@dataclass +class SnapshotPayload: + updated_at: datetime | None = None + hero_cards: list[HeroCard] = field(default_factory=list) + kpis: dict[str, MetricValue] = field(default_factory=dict) + strings: list[SnapshotGroupRow] = field(default_factory=list) + phases: list[SnapshotGroupRow] = field(default_factory=list) + status: list[MetricValue] = field(default_factory=list) + faults: list[str] = field(default_factory=list) + + +@dataclass +class BucketPoint: + label: str + start: datetime + end: datetime + value: float + + +@dataclass +class AnalyticsSummary: + total: float = 0.0 + unit: str = "kWh" + average_bucket: float = 0.0 + best_bucket_label: str = "" + best_bucket_value: float = 0.0 + co2_saved_kg: float = 0.0 + comparison_total: float | None = None + comparison_delta_pct: float | None = None + + +@dataclass +class DailyEnergyRecord: + day: date + energy_kwh: float + source: str + samples_count: int + imported_at: datetime | None = None + + +@dataclass +class HistoricalCoverage: + imported_days: int = 0 + first_day: date | None = None + last_day: date | None = None + total_energy_kwh: float = 0.0 + available_days: int = 0 + missing_days: int = 0 + coverage_pct: float | None = None + + +@dataclass +class HistoricalChunkProgress: + chunk_index: int + total_chunks: int + start_date: date + end_date: date + processed_days: int = 0 + imported_days: int = 0 + skipped_days: int = 0 + energy_kwh: float = 0.0 + state: str = "pending" + started_at: datetime | None = None + finished_at: datetime | None = None + duration_seconds: float | None = None + note: str = "" + + +@dataclass +class HistoricalActivityEvent: + timestamp: datetime + level: str = "info" + title: str = "" + message: str = "" + day: date | None = None + chunk_index: int | None = None + + +@dataclass +class HistoricalImportStatus: + enabled: bool = True + running: bool = False + state: str = "idle" + job_id: str | None = None + started_at: datetime | None = None + finished_at: datetime | None = None + requested_start_date: date | None = None + requested_end_date: date | None = None + total_days: int = 0 + processed_days: int = 0 + imported_days: int = 0 + skipped_days: int = 0 + chunk_days: int = 1 + total_chunks: int = 0 + active_chunk_index: int = 0 + current_date: date | None = None + current_chunk_start: date | None = None + current_chunk_end: date | None = None + elapsed_seconds: float | None = None + estimated_remaining_seconds: float | None = None + avg_days_per_minute: float | None = None + last_error: str | None = None + message: str = "" + coverage: HistoricalCoverage = field(default_factory=HistoricalCoverage) + available_start_date: date | None = None + available_end_date: date | None = None + default_chunk_days: int = 1 + recent_chunks: list[HistoricalChunkProgress] = field(default_factory=list) + recent_events: list[HistoricalActivityEvent] = field(default_factory=list) diff --git a/backend/app/routes/__init__.py b/backend/app/routes/__init__.py new file mode 100644 index 0000000..d079b77 --- /dev/null +++ b/backend/app/routes/__init__.py @@ -0,0 +1,15 @@ +from .analytics import analytics_blueprint +from .auth import auth_blueprint +from .dashboard import dashboard_blueprint +from .health import health_blueprint +from .historical import historical_blueprint +from .realtime import realtime_blueprint + +__all__ = [ + "auth_blueprint", + "analytics_blueprint", + "dashboard_blueprint", + "health_blueprint", + "historical_blueprint", + "realtime_blueprint", +] diff --git a/backend/app/routes/analytics.py b/backend/app/routes/analytics.py new file mode 100644 index 0000000..43ad393 --- /dev/null +++ b/backend/app/routes/analytics.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import json + +from flask import Blueprint, jsonify, request + +from app.services.analytics import AnalyticsService +from app.utils.serialization import to_plain + +analytics_blueprint = Blueprint("analytics", __name__) +service = AnalyticsService() + + +@analytics_blueprint.get("/analytics/production") +def production_analytics(): + range_key = request.args.get("range", "30d") + bucket = request.args.get("bucket", "day") + compare = request.args.get("compare", "none") + start = request.args.get("start") + end = request.args.get("end") + compare_ranges_raw = request.args.get("compare_ranges", "") + compare_ranges = [] + if compare_ranges_raw: + try: + compare_ranges = json.loads(compare_ranges_raw) + except json.JSONDecodeError as exc: + return jsonify({"detail": f"Invalid compare_ranges payload: {exc}"}), 400 + try: + return jsonify( + to_plain( + service.production( + range_key=range_key, + bucket=bucket, + compare_mode=compare, + start=start, + end=end, + compare_ranges=compare_ranges, + ) + ) + ) + except ValueError as exc: + return jsonify({"detail": str(exc)}), 400 + + +@analytics_blueprint.get("/analytics/distribution") +def production_distribution(): + range_key = request.args.get("range", "30d") + bucket = request.args.get("bucket", "day") + start = request.args.get("start") + end = request.args.get("end") + try: + return jsonify( + to_plain( + service.distribution( + range_key=range_key, + bucket=bucket, + start=start, + end=end, + ) + ) + ) + except ValueError as exc: + return jsonify({"detail": str(exc)}), 400 diff --git a/backend/app/routes/auth.py b/backend/app/routes/auth.py new file mode 100644 index 0000000..23a3ddf --- /dev/null +++ b/backend/app/routes/auth.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from flask import Blueprint, jsonify, request + +from app.services.auth import get_auth_service +from app.utils.serialization import to_plain + + +auth_blueprint = Blueprint("auth", __name__) +service = get_auth_service() + + +@auth_blueprint.get("/auth/status") +def auth_status(): + return jsonify(to_plain(service.status())) + + +@auth_blueprint.post("/auth/login") +def auth_login(): + payload = request.get_json(silent=True) or {} + try: + status = service.login(payload.get("username", ""), payload.get("password", "")) + return jsonify(to_plain(status)) + except ValueError as exc: + return jsonify({"detail": str(exc)}), 401 + + +@auth_blueprint.post("/auth/logout") +def auth_logout(): + return jsonify(to_plain(service.logout())) + + +@auth_blueprint.get("/auth/users") +def list_users(): + try: + service.require_admin() + return jsonify(to_plain({"items": service.list_users()})) + except PermissionError as exc: + return jsonify({"detail": str(exc)}), 403 + + +@auth_blueprint.post("/auth/users") +def create_user(): + payload = request.get_json(silent=True) or {} + try: + service.require_admin() + user = service.create_user( + username=payload.get("username", ""), + password=payload.get("password", ""), + role=payload.get("role", "user"), + display_name=payload.get("display_name") or payload.get("username") or "", + ) + return jsonify(to_plain({ + "username": user.username, + "display_name": user.display_name, + "role": user.role, + "is_active": user.is_active, + })) + except PermissionError as exc: + return jsonify({"detail": str(exc)}), 403 + except ValueError as exc: + return jsonify({"detail": str(exc)}), 400 + + +@auth_blueprint.post("/auth/users//reset-password") +def reset_password(username: str): + payload = request.get_json(silent=True) or {} + try: + service.require_admin() + user = service.reset_password(username=username, new_password=payload.get("password", "")) + return jsonify(to_plain({ + "username": user.username, + "display_name": user.display_name, + "role": user.role, + "is_active": user.is_active, + })) + except PermissionError as exc: + return jsonify({"detail": str(exc)}), 403 + except ValueError as exc: + return jsonify({"detail": str(exc)}), 400 diff --git a/backend/app/routes/dashboard.py b/backend/app/routes/dashboard.py new file mode 100644 index 0000000..534371e --- /dev/null +++ b/backend/app/routes/dashboard.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from flask import Blueprint, jsonify, request + +from app.core_settings import get_settings +from app.services.capabilities import build_capabilities +from app.services.catalog import get_catalog +from app.services.kiosk_settings import get_kiosk_settings_service +from app.services.auth import get_auth_service +from app.utils.serialization import to_plain + + +dashboard_blueprint = Blueprint("dashboard", __name__) + + +@dashboard_blueprint.get("/dashboard/config") +def dashboard_config(): + settings = get_settings() + catalog = get_catalog() + capabilities = build_capabilities(catalog) + + payload = { + "app": { + "name": settings.app_name, + "version": settings.version, + "site_name": settings.site_name, + "timezone": settings.timezone, + "installed_power_kwp": settings.installed_power_kwp, + }, + "defaults": { + "realtime_range": settings.realtime["history_default_range"], + "analytics_range": settings.analytics["default_range"], + "analytics_bucket": settings.analytics["default_bucket"], + "tab": settings.frontend_defaults["tab"], + "theme": settings.frontend_defaults["theme"], + "language": settings.frontend_defaults["language"], + }, + "auth": { + "enabled": settings.auth["enabled"], + }, + "i18n": settings.i18n, + "capabilities": capabilities, + "visible_entities": [ + { + "metric_id": metric.id, + "label": metric.label, + "entity_id": metric.entity_id, + "measurement": metric.measurement, + "unit": metric.unit, + "kind": metric.kind, + } + for metric in catalog.visible_entities() + ], + } + return jsonify(to_plain(payload)) + + +@dashboard_blueprint.get("/dashboard/kiosk-settings") +def dashboard_kiosk_settings(): + requested_mode = request.args.get("mode") or ("public" if request.args.get("publicKiosk") == "1" else "private") + try: + payload = get_kiosk_settings_service().get(requested_mode) + return jsonify(to_plain(payload)) + except ValueError as exc: + return jsonify({"detail": str(exc)}), 400 + + +@dashboard_blueprint.put("/dashboard/kiosk-settings") +def update_dashboard_kiosk_settings(): + payload = request.get_json(silent=True) or {} + mode = payload.get("mode", "private") + auth_service = get_auth_service() + try: + auth_service.require_admin() + updated = get_kiosk_settings_service().update_from_session(mode, payload) + return jsonify(to_plain(updated)) + except PermissionError as exc: + return jsonify({"detail": str(exc)}), 403 + except ValueError as exc: + return jsonify({"detail": str(exc)}), 400 diff --git a/backend/app/routes/health.py b/backend/app/routes/health.py new file mode 100644 index 0000000..4e2751c --- /dev/null +++ b/backend/app/routes/health.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from flask import Blueprint, jsonify + +from app.core_settings import get_settings + +health_blueprint = Blueprint("health", __name__) + + +@health_blueprint.get("/health") +def health(): + settings = get_settings() + return jsonify({ + "status": "ok", + "app": settings.app_name, + "version": settings.version, + }) diff --git a/backend/app/routes/historical.py b/backend/app/routes/historical.py new file mode 100644 index 0000000..de44827 --- /dev/null +++ b/backend/app/routes/historical.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from datetime import date + +from flask import Blueprint, jsonify, request + +from app.services.historical_sync import get_historical_sync_service +from app.utils.serialization import to_plain + +historical_blueprint = Blueprint("historical", __name__) +service = get_historical_sync_service() + + +@historical_blueprint.get("/historical/status") +def historical_status(): + return jsonify(to_plain(service.status())) + + +@historical_blueprint.post("/historical/start") +def historical_start(): + payload = request.get_json(silent=True) or {} + try: + status = service.start( + start_date=_parse_date(payload.get("start_date")), + end_date=_parse_date(payload.get("end_date")), + chunk_days=payload.get("chunk_days"), + force=bool(payload.get("force", False)), + ) + return jsonify(to_plain(status)) + except ValueError as exc: + return jsonify({"detail": str(exc)}), 400 + except RuntimeError as exc: + return jsonify({"detail": str(exc)}), 400 + + +@historical_blueprint.post("/historical/sync-now") +def historical_sync_now(): + try: + status = service.start(auto=True) + return jsonify(to_plain(status)) + except RuntimeError as exc: + return jsonify({"detail": str(exc)}), 400 + + +@historical_blueprint.post("/historical/cancel") +def historical_cancel(): + return jsonify(to_plain(service.cancel())) + + + +def _parse_date(value: str | None) -> date | None: + if not value: + return None + return date.fromisoformat(value) diff --git a/backend/app/routes/realtime.py b/backend/app/routes/realtime.py new file mode 100644 index 0000000..706e11a --- /dev/null +++ b/backend/app/routes/realtime.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from flask import Blueprint, jsonify, request + +from app.services.realtime import RealtimeService +from app.utils.serialization import to_plain + +realtime_blueprint = Blueprint("realtime", __name__) +service = RealtimeService() + + +@realtime_blueprint.get("/realtime/snapshot") +def realtime_snapshot(): + return jsonify(to_plain(service.snapshot())) + + +@realtime_blueprint.get("/realtime/history") +def realtime_history(): + range_key = request.args.get("range", "6h") + start = request.args.get("start") + end = request.args.get("end") + metrics = [item.strip() for item in request.args.get("metrics", "").split(",") if item.strip()] + try: + return jsonify(to_plain(service.history(range_key=range_key, start=start, end=end, metric_ids=metrics or None))) + except ValueError as exc: + return jsonify({"detail": str(exc)}), 400 diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..08b9ccc --- /dev/null +++ b/backend/app/services/__init__.py @@ -0,0 +1,5 @@ +from .analytics import AnalyticsService +from .historical_sync import HistoricalSyncService +from .realtime import RealtimeService + +__all__ = ["AnalyticsService", "HistoricalSyncService", "RealtimeService"] diff --git a/backend/app/services/analytics.py b/backend/app/services/analytics.py new file mode 100644 index 0000000..275c725 --- /dev/null +++ b/backend/app/services/analytics.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +from app.core_settings import AppSettings, get_settings +from app.services.catalog import MetricCatalog, get_catalog +from app.services.energy import EnergyService +from app.services.influx_http import InfluxHTTPService +from app.services.metrics import compare_delta_pct +from app.utils.time import resolve_window, shift_window + + +class AnalyticsService: + def __init__( + self, + settings: AppSettings | None = None, + catalog: MetricCatalog | None = None, + influx: InfluxHTTPService | None = None, + energy: EnergyService | None = None, + ) -> None: + self.settings = settings or get_settings() + self.catalog = catalog or get_catalog() + self.influx = influx or InfluxHTTPService(self.settings) + self.energy = energy or EnergyService(self.settings, self.catalog, self.influx) + + def production( + self, + range_key: str | None = None, + bucket: str | None = None, + compare_mode: str = "none", + start: str | None = None, + end: str | None = None, + compare_ranges: list[dict] | None = None, + ) -> dict: + bucket = bucket or self.settings.analytics["default_bucket"] + if bucket not in self.settings.analytics["bucket_labels"]: + raise ValueError(f"Unsupported bucket: {bucket}") + + window = resolve_window(range_key=range_key, start=start, end=end) + current_days = self.energy.daily_records_for_window(window.start, window.end, persist_missing=True) + current = self.energy.bucketize_daily(current_days, bucket) + total = round(sum(item.value for item in current), 2) + + comparison = [] + comparison_total = None + comparison_delta_pct = None + comparisons = [] + if compare_mode == "custom_multi": + for index, item in enumerate(compare_ranges or []): + compare_start = item.get("start") + compare_end = item.get("end") + if not compare_start or not compare_end: + continue + compare_window = resolve_window(start=compare_start, end=compare_end) + comparison_days = self.energy.daily_records_for_window(compare_window.start, compare_window.end, persist_missing=True) + comparison_series = self.energy.bucketize_daily(comparison_days, bucket) + comparison_total_value = round(sum(point.value for point in comparison_series), 2) + comparisons.append({ + "key": item.get("key") or f"custom_{index + 1}", + "label": item.get("label") or f"Custom {index + 1}", + "start": compare_window.start, + "end": compare_window.end, + "total": comparison_total_value, + "delta_pct": compare_delta_pct(total, comparison_total_value), + "points": comparison_series, + }) + if comparisons: + comparison = comparisons[0]["points"] + comparison_total = comparisons[0]["total"] + comparison_delta_pct = comparisons[0]["delta_pct"] + elif compare_mode != "none": + compare_window = shift_window(window, compare_mode) + comparison_days = self.energy.daily_records_for_window(compare_window.start, compare_window.end, persist_missing=True) + comparison = self.energy.bucketize_daily(comparison_days, bucket) + comparison_total = round(sum(item.value for item in comparison), 2) + comparison_delta_pct = compare_delta_pct(total, comparison_total) + comparisons.append({ + "key": compare_mode, + "label": compare_mode, + "start": compare_window.start, + "end": compare_window.end, + "total": comparison_total, + "delta_pct": comparison_delta_pct, + "points": comparison, + }) + + average_bucket = round(total / len(current), 2) if current else 0.0 + best_bucket = max(current, key=lambda item: item.value, default=None) + + return { + "unit": "kWh", + "bucket": bucket, + "compare_mode": compare_mode, + "current": current, + "comparison": comparison, + "comparisons": comparisons, + "summary": { + "total": total, + "unit": "kWh", + "average_bucket": average_bucket, + "best_bucket_label": best_bucket.label if best_bucket else "", + "best_bucket_value": best_bucket.value if best_bucket else 0.0, + "co2_saved_kg": round(total * self.settings.co2_factor, 2), + "comparison_total": comparison_total, + "comparison_delta_pct": comparison_delta_pct, + }, + "meta": { + "window": { + "start": window.start, + "end": window.end, + "range_key": window.key, + }, + "source": "sqlite_cache_plus_live_influx", + }, + } + + def distribution( + self, + range_key: str | None = None, + bucket: str | None = None, + start: str | None = None, + end: str | None = None, + ) -> dict: + payload = self.production(range_key=range_key, bucket=bucket, compare_mode="none", start=start, end=end) + current = payload["current"] + total = round(sum(item.value for item in current), 2) + denominator = total or 1.0 + return { + "unit": payload["unit"], + "bucket": payload["bucket"], + "total": total, + "slices": [ + { + "label": item.label, + "value": item.value, + "share": round((item.value / denominator) * 100.0, 2), + } + for item in current + if item.value > 0 + ], + "meta": payload["meta"], + } diff --git a/backend/app/services/auth.py b/backend/app/services/auth.py new file mode 100644 index 0000000..99dd53e --- /dev/null +++ b/backend/app/services/auth.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from flask import session +from werkzeug.security import check_password_hash, generate_password_hash + +from app.core_settings import AppSettings, get_settings +from app.storage.auth_users import AuthUser, SQLiteAuthUserRepository + + +SESSION_USER_KEY = "auth_user" +SESSION_DISPLAY_NAME_KEY = "auth_display_name" +SESSION_ROLE_KEY = "auth_role" +VALID_ROLES = {"admin", "user"} + + +class AuthService: + def __init__(self, settings: AppSettings | None = None) -> None: + self.settings = settings or get_settings() + self.user_repository = SQLiteAuthUserRepository(self.settings.storage["sqlite_path"]) + + @property + def enabled(self) -> bool: + return bool(self.settings.auth["enabled"]) + + def status(self) -> dict[str, Any]: + if not self.enabled: + return { + "enabled": False, + "authenticated": True, + "user": None, + "display_name": None, + "role": None, + } + + return { + "enabled": True, + "authenticated": SESSION_USER_KEY in session, + "user": session.get(SESSION_USER_KEY), + "display_name": session.get(SESSION_DISPLAY_NAME_KEY), + "role": session.get(SESSION_ROLE_KEY), + } + + def login(self, username: str, password: str) -> dict[str, Any]: + if not self.enabled: + return self.status() + + username = (username or "").strip() + password = password or "" + user = self.user_repository.get_by_username(username) + + if user is None: + self._login_legacy_user(username, password) + else: + if not user.is_active: + raise ValueError("Konto jest nieaktywne") + if not check_password_hash(user.password_hash, password): + raise ValueError("Niepoprawny login lub haslo") + self._set_session(user.username, user.display_name, user.role) + + return self.status() + + def logout(self) -> dict[str, Any]: + session.clear() + return self.status() + + def list_users(self) -> list[dict[str, Any]]: + users = self.user_repository.list_users() + return [ + { + "username": user.username, + "display_name": user.display_name, + "role": user.role, + "is_active": user.is_active, + "created_at": user.created_at, + "updated_at": user.updated_at, + } + for user in users + ] + + def require_admin(self) -> None: + if not self.enabled: + return + if session.get(SESSION_ROLE_KEY) != "admin": + raise PermissionError("Brak uprawnien administratora") + + def configure_app(self, app) -> None: + max_age = int(self.settings.auth["session_max_age_seconds"]) + app.secret_key = self.settings.auth["secret_key"] + app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(seconds=max_age) + app.config["SESSION_COOKIE_NAME"] = self.settings.auth["session_cookie_name"] + app.config["SESSION_COOKIE_HTTPONLY"] = True + app.config["SESSION_COOKIE_SAMESITE"] = self.settings.auth.get("cookie_samesite", "Lax") + app.config["SESSION_COOKIE_SECURE"] = bool(self.settings.auth.get("cookie_secure", False)) + + def create_user(self, *, username: str, password: str, role: str, display_name: str | None = None) -> AuthUser: + normalized_username = self._normalize_username(username) + normalized_role = self._normalize_role(role) + clean_password = self._validate_password(password) + resolved_display_name = (display_name or normalized_username).strip() + if not resolved_display_name: + raise ValueError("Display name nie moze byc pusty") + return self.user_repository.upsert_user( + username=normalized_username, + password_hash=generate_password_hash(clean_password), + role=normalized_role, + display_name=resolved_display_name, + is_active=True, + ) + + def reset_password(self, *, username: str, new_password: str) -> AuthUser: + normalized_username = self._normalize_username(username) + clean_password = self._validate_password(new_password) + user = self.user_repository.update_password( + normalized_username, + generate_password_hash(clean_password), + ) + if user is None: + raise ValueError(f"Uzytkownik '{normalized_username}' nie istnieje") + return user + + def _login_legacy_user(self, username: str, password: str) -> None: + expected_username = self.settings.auth["username"] + expected_password = self.settings.auth["password"] + expected_password_hash = self.settings.auth.get("password_hash") + + if username != expected_username: + raise ValueError("Niepoprawny login lub haslo") + + if expected_password_hash: + password_ok = check_password_hash(expected_password_hash, password) + else: + password_ok = password == expected_password + + if not password_ok: + raise ValueError("Niepoprawny login lub haslo") + + self._set_session( + expected_username, + self.settings.auth.get("display_name") or expected_username, + self.settings.auth.get("role", "admin"), + ) + + def _set_session(self, username: str, display_name: str, role: str) -> None: + session.clear() + session.permanent = True + session[SESSION_USER_KEY] = username + session[SESSION_DISPLAY_NAME_KEY] = display_name + session[SESSION_ROLE_KEY] = role + + def _normalize_username(self, username: str) -> str: + normalized = (username or "").strip() + if not normalized: + raise ValueError("Username nie moze byc pusty") + return normalized + + def _normalize_role(self, role: str) -> str: + normalized = (role or "").strip().lower() + if normalized not in VALID_ROLES: + raise ValueError("Rola musi byc jedna z: admin, user") + return normalized + + def _validate_password(self, password: str) -> str: + clean_password = password or "" + if len(clean_password) < 8: + raise ValueError("Haslo musi miec co najmniej 8 znakow") + return clean_password + + +_auth_service: AuthService | None = None + + +def get_auth_service() -> AuthService: + global _auth_service + if _auth_service is None: + _auth_service = AuthService() + return _auth_service diff --git a/backend/app/services/capabilities.py b/backend/app/services/capabilities.py new file mode 100644 index 0000000..a2c6645 --- /dev/null +++ b/backend/app/services/capabilities.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from app.services.catalog import MetricCatalog, get_catalog + + + +def build_capabilities(catalog: MetricCatalog | None = None) -> dict: + catalog = catalog or get_catalog() + settings = catalog.settings + + string_rows = [] + for item in settings.strings: + metric_ids = list(item.get("metrics", {}).values()) + if any(catalog.safe_get(metric_id) for metric_id in metric_ids): + string_rows.append(item) + + analytics_enabled = settings.modules.get("analytics", False) + + return { + "modules": settings.modules, + "strings_enabled": settings.modules.get("strings", False) and len(string_rows) > 0, + "strings_count": len(string_rows), + "phases_enabled": False, + "phases_count": 0, + "analytics_enabled": analytics_enabled, + "realtime_enabled": settings.modules.get("realtime_overview", False), + "comparison_modes": list(settings.analytics["compare_modes"].keys()), + "ranges": [ + {"key": key, "label": definition["label"]} + for key, definition in settings.time_ranges.items() + ], + "buckets": [ + {"key": key, "label": label} + for key, label in settings.analytics["bucket_labels"].items() + ], + "historical_import_enabled": settings.modules.get("historical_import", False), + "history": { + "enabled": settings.history.get("enabled", True), + "default_chunk_days": settings.history.get("default_chunk_days", 7), + "auto_sync_enabled": settings.history.get("auto_sync_enabled", False), + "auto_sync_interval_minutes": settings.history.get("auto_sync_interval_minutes", 30), + }, + } diff --git a/backend/app/services/catalog.py b/backend/app/services/catalog.py new file mode 100644 index 0000000..206974a --- /dev/null +++ b/backend/app/services/catalog.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from app.core_settings import AppSettings, get_settings +from app.models.definitions import MetricDefinition + + +@dataclass +class MetricCatalog: + settings: AppSettings + + def get(self, metric_id: str) -> MetricDefinition: + if metric_id not in self.settings.metrics: + raise KeyError(f"Unknown metric: {metric_id}") + return self.settings.metrics[metric_id] + + def safe_get(self, metric_id: str) -> MetricDefinition | None: + return self.settings.metrics.get(metric_id) + + def visible_entities(self) -> list[MetricDefinition]: + return [self.get(metric_id) for metric_id in self.settings.visible_entity_table if metric_id in self.settings.metrics] + + + +def get_catalog() -> MetricCatalog: + return MetricCatalog(get_settings()) diff --git a/backend/app/services/energy.py b/backend/app/services/energy.py new file mode 100644 index 0000000..cefffb2 --- /dev/null +++ b/backend/app/services/energy.py @@ -0,0 +1,220 @@ +from __future__ import annotations + +from collections import defaultdict +from dataclasses import dataclass +from datetime import date, datetime, time, timedelta +from zoneinfo import ZoneInfo + +from app.core_settings import AppSettings, get_settings +from app.models.definitions import BucketPoint, DailyEnergyRecord, MetricDefinition, SeriesPoint +from app.services.catalog import MetricCatalog, get_catalog +from app.services.influx_http import InfluxHTTPService +from app.services.metrics import to_float +from app.storage import SQLiteEnergyRepository +from app.utils.time import ( + choose_counter_interval, + choose_power_interval, + duration_to_seconds, + now_local, +) + + +@dataclass +class EnergySample: + timestamp: datetime + delta_kwh: float + + +class EnergyService: + def __init__( + self, + settings: AppSettings | None = None, + catalog: MetricCatalog | None = None, + influx: InfluxHTTPService | None = None, + repository: SQLiteEnergyRepository | None = None, + ) -> None: + self.settings = settings or get_settings() + self.catalog = catalog or get_catalog() + self.influx = influx or InfluxHTTPService(self.settings) + self.repository = repository or SQLiteEnergyRepository(self.settings.storage["sqlite_path"]) + self.tz = ZoneInfo(self.settings.timezone) + + def total_for_window(self, start: datetime, end: datetime) -> float: + total, _, _ = self.window_total_with_meta(start, end) + return total + + def window_total_with_meta(self, start: datetime, end: datetime) -> tuple[float, str, int]: + samples, source, observations_count = self._samples_for_window(start, end) + return round(sum(sample.delta_kwh for sample in samples), 2), source, observations_count + + def total_for_full_day(self, day: date) -> tuple[float, str, int]: + start = datetime.combine(day, time.min, tzinfo=self.tz) + end = start + timedelta(days=1) + return self.window_total_with_meta(start, end) + + def samples(self, start: datetime, end: datetime) -> list[EnergySample]: + samples, _, _ = self._samples_for_window(start, end) + return samples + + def daily_records_for_window( + self, + start: datetime, + end: datetime, + *, + persist_missing: bool = True, + ) -> list[DailyEnergyRecord]: + start_local = start.astimezone(self.tz) + end_local = end.astimezone(self.tz) + if end_local <= start_local: + return [] + + start_day = start_local.date() + end_day = end_local.date() + cached = self.repository.fetch_daily_energy(start_day, end_day) + today_local = now_local().date() + rows: list[DailyEnergyRecord] = [] + + current = start_day + while current <= end_day: + day_start = datetime.combine(current, time.min, tzinfo=self.tz) + day_end = day_start + timedelta(days=1) + segment_start = max(start_local, day_start) + segment_end = min(end_local, day_end) + if segment_end <= segment_start: + current = current + timedelta(days=1) + continue + + is_full_day = segment_start == day_start and segment_end == day_end + cached_row = cached.get(current) + if is_full_day and cached_row is not None: + rows.append(cached_row) + else: + total, source, observations_count = self.window_total_with_meta(segment_start, segment_end) + record = DailyEnergyRecord( + day=current, + energy_kwh=total, + source=source, + samples_count=observations_count, + ) + rows.append(record) + if is_full_day and persist_missing and current < today_local and observations_count > 0: + self.repository.upsert_daily_energy(record) + current = current + timedelta(days=1) + + return rows + + def bucketize_daily(self, records: list[DailyEnergyRecord], bucket: str) -> list[BucketPoint]: + grouped: dict[str, dict] = defaultdict(lambda: {"value": 0.0, "start": None, "end": None, "label": ""}) + + for record in records: + start = datetime.combine(record.day, time.min, tzinfo=self.tz) + if bucket == "day": + bucket_start = start + bucket_end = bucket_start + timedelta(days=1) + key = bucket_start.strftime("%Y-%m-%d") + label = bucket_start.strftime("%d.%m") + elif bucket == "week": + bucket_start = start - timedelta(days=start.weekday()) + bucket_end = bucket_start + timedelta(days=7) + iso = bucket_start.isocalendar() + key = f"{iso.year}-W{iso.week:02d}" + label = key + elif bucket == "month": + bucket_start = start.replace(day=1) + if bucket_start.month == 12: + bucket_end = bucket_start.replace(year=bucket_start.year + 1, month=1) + else: + bucket_end = bucket_start.replace(month=bucket_start.month + 1) + key = bucket_start.strftime("%Y-%m") + label = key + elif bucket == "year": + bucket_start = start.replace(month=1, day=1) + bucket_end = bucket_start.replace(year=bucket_start.year + 1) + key = bucket_start.strftime("%Y") + label = key + else: + raise ValueError(f"Unsupported bucket: {bucket}") + + current = grouped[key] + current["label"] = label + current["value"] += record.energy_kwh + current["start"] = bucket_start if current["start"] is None else min(current["start"], bucket_start) + current["end"] = bucket_end if current["end"] is None else max(current["end"], bucket_end) + + rows = [] + for key in sorted(grouped.keys()): + item = grouped[key] + rows.append( + BucketPoint( + label=item["label"], + start=item["start"], + end=item["end"], + value=round(item["value"], 2), + ) + ) + return rows + + def _samples_for_window(self, start: datetime, end: datetime) -> tuple[list[EnergySample], str, int]: + counter_metric = self.catalog.safe_get(self.settings.analytics["production_metric_id"]) + if counter_metric is not None: + samples, observations_count = self._samples_from_counter(counter_metric, start, end) + return samples, "counter", observations_count + + power_metric = self.catalog.safe_get(self.settings.analytics["fallback_power_metric_id"]) + if power_metric is not None: + samples, observations_count = self._samples_from_power(power_metric, start, end) + return samples, "power_estimated", observations_count + + return [], "unavailable", 0 + + def _samples_from_counter(self, metric: MetricDefinition, start: datetime, end: datetime) -> tuple[list[EnergySample], int]: + interval = choose_counter_interval(start, end) + baseline = self.influx.last_before(metric, start) + series = self.influx.grouped_last_series(metric, start, end, interval) + + points: list[SeriesPoint] = [] + if baseline and baseline.value is not None: + points.append(SeriesPoint(timestamp=start, value=baseline.value)) + else: + first_value = next((point.value for point in series if point.value is not None), None) + if first_value is not None: + points.append(SeriesPoint(timestamp=start, value=first_value)) + points.extend(series) + + samples: list[EnergySample] = [] + previous_value = None + for point in points: + current_value = to_float(point.value) + if current_value is None: + continue + if previous_value is None: + previous_value = current_value + continue + + delta = current_value - previous_value + previous_value = current_value + if delta <= 0: + continue + if point.timestamp < start or point.timestamp > end: + continue + samples.append(EnergySample(timestamp=point.timestamp, delta_kwh=round(delta, 6))) + + observations_count = sum(1 for point in series if to_float(point.value) is not None) + return samples, observations_count + + def _samples_from_power(self, metric: MetricDefinition, start: datetime, end: datetime) -> tuple[list[EnergySample], int]: + interval = choose_power_interval(start, end) + interval_seconds = duration_to_seconds(interval) + points = self.influx.gauge_history(metric, start, end, interval, aggregate="mean") + samples: list[EnergySample] = [] + observations_count = 0 + for point in points: + watts = to_float(point.value) + if watts is None: + continue + observations_count += 1 + if watts <= 0: + continue + delta_kwh = watts * (interval_seconds / 3600.0) / 1000.0 + samples.append(EnergySample(timestamp=point.timestamp, delta_kwh=round(delta_kwh, 6))) + return samples, observations_count diff --git a/backend/app/services/historical_sync.py b/backend/app/services/historical_sync.py new file mode 100644 index 0000000..762810e --- /dev/null +++ b/backend/app/services/historical_sync.py @@ -0,0 +1,605 @@ +from __future__ import annotations + +import copy +import logging +import threading +import uuid +from datetime import date, datetime, timedelta +from functools import lru_cache +from math import ceil +from typing import Iterable + +from app.core_settings import AppSettings, get_settings +from app.models import ( + DailyEnergyRecord, + HistoricalActivityEvent, + HistoricalChunkProgress, + HistoricalImportStatus, +) +from app.services.catalog import MetricCatalog, get_catalog +from app.services.energy import EnergyService +from app.services.influx_http import InfluxHTTPService +from app.storage import SQLiteEnergyRepository +from app.utils.time import now_local + +logger = logging.getLogger(__name__) + + +class HistoricalSyncService: + MAX_RECENT_CHUNKS = 18 + MAX_RECENT_EVENTS = 40 + + def __init__( + self, + settings: AppSettings | None = None, + catalog: MetricCatalog | None = None, + influx: InfluxHTTPService | None = None, + energy: EnergyService | None = None, + repository: SQLiteEnergyRepository | None = None, + ) -> None: + self.settings = settings or get_settings() + self.catalog = catalog or get_catalog() + self.influx = influx or InfluxHTTPService(self.settings) + self.energy = energy or EnergyService(self.settings, self.catalog, self.influx) + self.repository = repository or SQLiteEnergyRepository(self.settings.storage["sqlite_path"]) + self._state_lock = threading.Lock() + self._worker: threading.Thread | None = None + self._cancel_event = threading.Event() + self._scheduler_stop = threading.Event() + self._scheduler: threading.Thread | None = None + self._available_bounds_cache: tuple[datetime, date | None, date | None] | None = None + self._state = HistoricalImportStatus( + enabled=self.settings.history.get("enabled", True), + state="idle", + default_chunk_days=self.settings.history.get("default_chunk_days", 7), + ) + self._refresh_coverage() + self._refresh_available_bounds() + self._refresh_runtime_metrics() + + def status(self) -> HistoricalImportStatus: + with self._state_lock: + self._refresh_coverage(lock_held=True) + self._refresh_available_bounds(lock_held=True) + self._refresh_runtime_metrics(lock_held=True) + return copy.deepcopy(self._state) + + def start( + self, + *, + start_date: date | None = None, + end_date: date | None = None, + chunk_days: int | None = None, + force: bool = False, + auto: bool = False, + ) -> HistoricalImportStatus: + if not self.settings.history.get("enabled", True): + raise RuntimeError("Historical import is disabled") + + chunk_days = max(int(chunk_days or self.settings.history.get("default_chunk_days", 7)), 1) + resolved = self._resolve_range(start_date=start_date, end_date=end_date) + if resolved is None: + with self._state_lock: + self._state.running = False + self._state.state = "idle" + self._state.message = "Brak brakujacych dni do importu." + self._state.finished_at = datetime.utcnow() + self._refresh_coverage(lock_held=True) + self._refresh_available_bounds(lock_held=True) + self._refresh_runtime_metrics(lock_held=True) + return copy.deepcopy(self._state) + + resolved_start, resolved_end = resolved + total_days = (resolved_end - resolved_start).days + 1 + total_chunks = max(ceil(total_days / chunk_days), 1) + start_message = "Start importu archiwalnego" if not auto else "Start automatycznej synchronizacji archiwum" + + with self._state_lock: + if self._worker and self._worker.is_alive(): + return copy.deepcopy(self._state) + + self._cancel_event = threading.Event() + self._state = HistoricalImportStatus( + enabled=True, + running=True, + state="running", + job_id=uuid.uuid4().hex[:12], + started_at=datetime.utcnow(), + requested_start_date=resolved_start, + requested_end_date=resolved_end, + total_days=total_days, + chunk_days=chunk_days, + total_chunks=total_chunks, + active_chunk_index=1, + current_chunk_start=resolved_start, + current_chunk_end=min(resolved_start + timedelta(days=chunk_days - 1), resolved_end), + message=start_message, + default_chunk_days=self.settings.history.get("default_chunk_days", 7), + recent_chunks=[], + recent_events=[], + ) + self._refresh_coverage(lock_held=True) + self._refresh_available_bounds(lock_held=True) + self._refresh_runtime_metrics(lock_held=True) + + self._worker = threading.Thread( + target=self._run_worker, + kwargs={ + "start_date": resolved_start, + "end_date": resolved_end, + "chunk_days": chunk_days, + "force": force, + "auto": auto, + }, + name="pv-historical-backfill", + daemon=True, + ) + self._worker.start() + + self._record_event( + level="info", + title="Uruchomiono zadanie", + message=f"Zakres {resolved_start.isoformat()} -> {resolved_end.isoformat()}, chunk {chunk_days} dni", + ) + return self.status() + + def cancel(self) -> HistoricalImportStatus: + self._cancel_event.set() + with self._state_lock: + self._state.message = "Anulowanie zadania..." + self._refresh_runtime_metrics(lock_held=True) + snapshot = copy.deepcopy(self._state) + self._record_event(level="warn", title="Anulowanie", message="Uzytkownik poprosil o zatrzymanie zadania.") + return snapshot + + def run_blocking( + self, + *, + start_date: date | None = None, + end_date: date | None = None, + chunk_days: int | None = None, + force: bool = False, + ) -> HistoricalImportStatus: + resolved = self._resolve_range(start_date=start_date, end_date=end_date) + if resolved is None: + return self.status() + resolved_start, resolved_end = resolved + chunk_days = max(int(chunk_days or self.settings.history.get("default_chunk_days", 7)), 1) + total_days = (resolved_end - resolved_start).days + 1 + total_chunks = max(ceil(total_days / chunk_days), 1) + with self._state_lock: + self._state = HistoricalImportStatus( + enabled=True, + running=True, + state="running", + job_id=uuid.uuid4().hex[:12], + started_at=datetime.utcnow(), + requested_start_date=resolved_start, + requested_end_date=resolved_end, + total_days=total_days, + chunk_days=chunk_days, + total_chunks=total_chunks, + default_chunk_days=self.settings.history.get("default_chunk_days", 7), + recent_chunks=[], + recent_events=[], + ) + self._record_event( + level="info", + title="Uruchomiono zadanie", + message=f"Zakres {resolved_start.isoformat()} -> {resolved_end.isoformat()}, chunk {chunk_days} dni", + ) + self._run_worker( + start_date=resolved_start, + end_date=resolved_end, + chunk_days=chunk_days, + force=force, + auto=False, + ) + return self.status() + + def start_scheduler_if_enabled(self) -> None: + if not self.settings.history.get("enabled", True): + return + if not self.settings.history.get("auto_sync_enabled", False): + return + if self._scheduler and self._scheduler.is_alive(): + return + self._scheduler_stop.clear() + self._scheduler = threading.Thread(target=self._scheduler_loop, name="pv-history-scheduler", daemon=True) + self._scheduler.start() + + def _scheduler_loop(self) -> None: + interval_seconds = max(int(self.settings.history.get("auto_sync_interval_minutes", 30)), 1) * 60 + if self.settings.history.get("auto_sync_on_start", False): + try: + self.start(auto=True) + except Exception as exc: + logger.warning("Unable to auto-start historical sync: %s", exc) + + while not self._scheduler_stop.wait(interval_seconds): + try: + if self._worker and self._worker.is_alive(): + continue + self.start(auto=True) + except Exception as exc: + logger.warning("Historical scheduler cycle failed: %s", exc) + + def _run_worker( + self, + *, + start_date: date, + end_date: date, + chunk_days: int, + force: bool, + auto: bool, + ) -> None: + total_chunks = max(ceil(((end_date - start_date).days + 1) / chunk_days), 1) + try: + chunk_index = 0 + chunk_start = start_date + while chunk_start <= end_date: + if self._cancel_event.is_set(): + self._record_event(level="warn", title="Anulowano", message="Import archiwalny anulowany przez uzytkownika.") + self._finish("cancelled", running=False, message="Import archiwalny anulowany przez uzytkownika.") + return + + chunk_index += 1 + chunk_end = min(chunk_start + timedelta(days=chunk_days - 1), end_date) + self._update_chunk(chunk_index, total_chunks, chunk_start, chunk_end) + imported, skipped, energy_kwh, cancelled = self._process_chunk( + chunk_index=chunk_index, + start_day=chunk_start, + end_day=chunk_end, + force=force, + ) + if cancelled: + self._close_chunk( + chunk_index, + imported_days=imported, + skipped_days=skipped, + energy_kwh=energy_kwh, + state="cancelled", + note="Chunk zatrzymany podczas przetwarzania", + ) + self._record_event(level="warn", title="Anulowano", message="Import archiwalny anulowany przez uzytkownika.") + self._finish("cancelled", running=False, message="Import archiwalny anulowany przez uzytkownika.") + return + + self._close_chunk( + chunk_index, + imported_days=imported, + skipped_days=skipped, + energy_kwh=energy_kwh, + state="completed", + note=f"Chunk zakonczony: import {imported}, pominiete {skipped}", + ) + self._record_event( + level="success", + title=f"Chunk {chunk_index}/{total_chunks} zakonczony", + message=f"Zakres {chunk_start.isoformat()} -> {chunk_end.isoformat()}, import {imported}, pominiete {skipped}, energia {energy_kwh:.2f} kWh", + chunk_index=chunk_index, + ) + chunk_start = chunk_end + timedelta(days=1) + + final_message = "Synchronizacja archiwalna zakonczona" if auto else "Import archiwalny zakonczony" + self._record_event(level="success", title="Zakonczono", message=final_message) + self._finish("completed", running=False, message=final_message) + except Exception as exc: + logger.exception("Historical import failed") + self._record_event(level="error", title="Blad importu", message=str(exc)) + self._finish("failed", running=False, message="Import archiwalny zakonczyl sie bledem.", last_error=str(exc)) + + def _process_chunk(self, *, chunk_index: int, start_day: date, end_day: date, force: bool) -> tuple[int, int, float, bool]: + imported_days = 0 + skipped_days = 0 + energy_kwh = 0.0 + + for day in self._date_range(start_day, end_day): + if self._cancel_event.is_set(): + return imported_days, skipped_days, energy_kwh, True + + if not force and self.repository.has_day(day): + skipped_days += 1 + self._advance_day( + day, + imported=False, + message=f"Pominieto {day.isoformat()} - dzien juz istnieje w cache", + level="warn", + title="Pominieto dzien", + chunk_index=chunk_index, + ) + continue + + total, source, samples_count = self.energy.total_for_full_day(day) + if samples_count <= 0: + skipped_days += 1 + self._advance_day( + day, + imported=False, + message=f"Pominieto {day.isoformat()} - brak probek w InfluxDB", + level="warn", + title="Brak probek", + chunk_index=chunk_index, + ) + continue + + self.repository.upsert_daily_energy( + DailyEnergyRecord( + day=day, + energy_kwh=total, + source=source, + samples_count=samples_count, + ) + ) + imported_days += 1 + energy_kwh += total + self._advance_day( + day, + imported=True, + message=f"Zaimportowano {day.isoformat()} ({total:.2f} kWh)", + level="success", + title="Zaimportowano dzien", + chunk_index=chunk_index, + energy_kwh=total, + ) + + return imported_days, skipped_days, round(energy_kwh, 3), False + + def _advance_day( + self, + day: date, + *, + imported: bool, + message: str, + level: str, + title: str, + chunk_index: int, + energy_kwh: float | None = None, + ) -> None: + with self._state_lock: + self._state.processed_days += 1 + if imported: + self._state.imported_days += 1 + else: + self._state.skipped_days += 1 + self._state.current_date = day + self._state.message = message + self._refresh_coverage(lock_held=True) + self._refresh_runtime_metrics(lock_held=True) + suffix = f" Energia: {energy_kwh:.2f} kWh." if imported and energy_kwh is not None else "" + self._record_event( + level=level, + title=title, + message=f"{message}.{suffix}" if not message.endswith(".") else f"{message}{suffix}", + day=day, + chunk_index=chunk_index, + ) + + def _update_chunk(self, chunk_index: int, total_chunks: int, chunk_start: date, chunk_end: date) -> None: + chunk = HistoricalChunkProgress( + chunk_index=chunk_index, + total_chunks=total_chunks, + start_date=chunk_start, + end_date=chunk_end, + state="running", + started_at=datetime.utcnow(), + note=f"Aktywny chunk {chunk_start.isoformat()} -> {chunk_end.isoformat()}", + ) + with self._state_lock: + self._state.current_chunk_start = chunk_start + self._state.current_chunk_end = chunk_end + self._state.active_chunk_index = chunk_index + self._state.message = f"Przetwarzanie zakresu {chunk_start.isoformat()} -> {chunk_end.isoformat()}" + self._upsert_chunk_locked(chunk) + self._refresh_runtime_metrics(lock_held=True) + self._record_event( + level="info", + title=f"Chunk {chunk_index}/{total_chunks}", + message=f"Start zakresu {chunk_start.isoformat()} -> {chunk_end.isoformat()}", + chunk_index=chunk_index, + ) + + def _close_chunk( + self, + chunk_index: int, + *, + imported_days: int, + skipped_days: int, + energy_kwh: float, + state: str, + note: str, + ) -> None: + with self._state_lock: + existing = self._find_chunk_locked(chunk_index) + started_at = existing.started_at if existing and existing.started_at else datetime.utcnow() + finished_at = datetime.utcnow() + processed_days = imported_days + skipped_days + duration_seconds = max((finished_at - started_at).total_seconds(), 0.0) + chunk = HistoricalChunkProgress( + chunk_index=chunk_index, + total_chunks=self._state.total_chunks, + start_date=existing.start_date if existing else self._state.current_chunk_start or self._state.requested_start_date or date.today(), + end_date=existing.end_date if existing else self._state.current_chunk_end or self._state.requested_end_date or date.today(), + processed_days=processed_days, + imported_days=imported_days, + skipped_days=skipped_days, + energy_kwh=round(energy_kwh, 3), + state=state, + started_at=started_at, + finished_at=finished_at, + duration_seconds=round(duration_seconds, 2), + note=note, + ) + self._upsert_chunk_locked(chunk) + if state != "running": + self._state.message = note + self._refresh_runtime_metrics(lock_held=True) + + def _finish( + self, + state: str, + *, + running: bool, + message: str, + last_error: str | None = None, + ) -> None: + with self._state_lock: + self._state.running = running + self._state.state = state + self._state.finished_at = datetime.utcnow() + self._state.last_error = last_error + self._state.message = message + self._state.active_chunk_index = 0 + self._refresh_coverage(lock_held=True) + self._refresh_available_bounds(lock_held=True) + self._refresh_runtime_metrics(lock_held=True) + + def _resolve_range(self, *, start_date: date | None, end_date: date | None) -> tuple[date, date] | None: + today = now_local().date() + include_today = self.settings.history.get("include_today_in_sync", False) + default_end = today if include_today else today - timedelta(days=1) + resolved_end = end_date or default_end + + if start_date is None: + coverage = self.repository.coverage() + if coverage.last_day: + resolved_start = coverage.last_day + timedelta(days=1) + else: + bootstrap_start = self.settings.history.get("bootstrap_start_date") + if bootstrap_start: + resolved_start = date.fromisoformat(bootstrap_start) + else: + available_start, _ = self._available_bounds() + resolved_start = available_start or resolved_end + else: + resolved_start = start_date + + if resolved_start > resolved_end: + return None + return resolved_start, resolved_end + + def _available_bounds(self) -> tuple[date | None, date | None]: + now_utc = datetime.utcnow() + cached = self._available_bounds_cache + if cached and (now_utc - cached[0]).total_seconds() < 300: + return cached[1], cached[2] + + available_start: date | None = None + available_end: date | None = None + metric = self.catalog.safe_get(self.settings.analytics.get("production_metric_id", "energy_total")) + fallback = self.catalog.safe_get(self.settings.analytics.get("fallback_power_metric_id", "ac_power")) + source_metric = metric or fallback + if source_metric is not None: + first_point = self.influx.first_value(source_metric) + last_point = self.influx.last_value(source_metric) + available_start = first_point.timestamp.astimezone(self.energy.tz).date() if first_point else None + available_end = last_point.timestamp.astimezone(self.energy.tz).date() if last_point else None + self._available_bounds_cache = (now_utc, available_start, available_end) + return available_start, available_end + + def _refresh_coverage(self, *, lock_held: bool = False) -> None: + coverage = self.repository.coverage() + available_start, available_end = self._available_bounds() + if available_start and available_end and available_start <= available_end: + available_days = (available_end - available_start).days + 1 + missing_days = self.repository.count_missing_days(available_start, available_end) + coverage.available_days = available_days + coverage.missing_days = missing_days + imported_in_range = max(available_days - missing_days, 0) + coverage.coverage_pct = round((imported_in_range / available_days) * 100, 1) if available_days > 0 else None + else: + coverage.available_days = 0 + coverage.missing_days = 0 + coverage.coverage_pct = None + + if lock_held: + self._state.coverage = coverage + else: + with self._state_lock: + self._state.coverage = coverage + + def _refresh_available_bounds(self, *, lock_held: bool = False) -> None: + available_start, available_end = self._available_bounds() + if lock_held: + self._state.available_start_date = available_start + self._state.available_end_date = available_end + else: + with self._state_lock: + self._state.available_start_date = available_start + self._state.available_end_date = available_end + + def _refresh_runtime_metrics(self, *, lock_held: bool = False) -> None: + def apply() -> None: + if self._state.started_at is None: + self._state.elapsed_seconds = None + self._state.estimated_remaining_seconds = None + self._state.avg_days_per_minute = None + return + + end_reference = datetime.utcnow() if self._state.running or self._state.finished_at is None else self._state.finished_at + elapsed_seconds = max((end_reference - self._state.started_at).total_seconds(), 0.0) + self._state.elapsed_seconds = round(elapsed_seconds, 1) + + if self._state.processed_days > 0 and elapsed_seconds > 0: + avg_days_per_minute = (self._state.processed_days / elapsed_seconds) * 60 + remaining_days = max(self._state.total_days - self._state.processed_days, 0) + estimated_remaining = (remaining_days / self._state.processed_days) * elapsed_seconds + self._state.avg_days_per_minute = round(avg_days_per_minute, 2) + self._state.estimated_remaining_seconds = round(estimated_remaining, 1) if self._state.running else 0.0 + else: + self._state.avg_days_per_minute = None + self._state.estimated_remaining_seconds = None if self._state.running else 0.0 + + if lock_held: + apply() + else: + with self._state_lock: + apply() + + def _record_event( + self, + *, + level: str, + title: str, + message: str, + day: date | None = None, + chunk_index: int | None = None, + ) -> None: + event = HistoricalActivityEvent( + timestamp=datetime.utcnow(), + level=level, + title=title, + message=message, + day=day, + chunk_index=chunk_index, + ) + with self._state_lock: + self._state.recent_events.append(event) + self._state.recent_events = self._state.recent_events[-self.MAX_RECENT_EVENTS :] + + def _find_chunk_locked(self, chunk_index: int) -> HistoricalChunkProgress | None: + for chunk in self._state.recent_chunks: + if chunk.chunk_index == chunk_index: + return chunk + return None + + def _upsert_chunk_locked(self, chunk: HistoricalChunkProgress) -> None: + for index, existing in enumerate(self._state.recent_chunks): + if existing.chunk_index == chunk.chunk_index: + self._state.recent_chunks[index] = chunk + break + else: + self._state.recent_chunks.append(chunk) + self._state.recent_chunks = self._state.recent_chunks[-self.MAX_RECENT_CHUNKS :] + + @staticmethod + def _date_range(start_day: date, end_day: date) -> Iterable[date]: + current = start_day + while current <= end_day: + yield current + current = current + timedelta(days=1) + + +@lru_cache(maxsize=1) +def get_historical_sync_service() -> HistoricalSyncService: + return HistoricalSyncService() diff --git a/backend/app/services/influx_http.py b/backend/app/services/influx_http.py new file mode 100644 index 0000000..6b750eb --- /dev/null +++ b/backend/app/services/influx_http.py @@ -0,0 +1,241 @@ +from __future__ import annotations + +import base64 +import json +import logging +import ssl +import urllib.error +import urllib.parse +import urllib.request +from collections import defaultdict +from datetime import datetime +from typing import Iterable + +from app.core_settings import AppSettings, get_settings +from app.models.definitions import MetricDefinition, SeriesPoint +from app.services.metrics import to_float +from app.utils.time import to_utc_iso + +logger = logging.getLogger(__name__) + + + +def _quote_identifier(value: str) -> str: + return '"' + value.replace('"', '\\"') + '"' + + + +def _quote_literal(value: str) -> str: + return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'" + + +class InfluxHTTPService: + def __init__(self, settings: AppSettings | None = None) -> None: + self.settings = settings or get_settings() + + @property + def base_url(self) -> str: + config = self.settings.influx + return f"{config['scheme']}://{config['host']}:{config['port']}" + + def latest_values(self, metrics: Iterable[MetricDefinition]) -> dict[str, dict]: + grouped: dict[str, list[MetricDefinition]] = defaultdict(list) + for metric in metrics: + grouped[metric.measurement].append(metric) + + payload: dict[str, dict] = {} + for measurement, measurement_metrics in grouped.items(): + conditions = " OR ".join( + f'("entity_id" = {_quote_literal(metric.entity_id)})' + for metric in measurement_metrics + ) + query = ( + f'SELECT LAST("value") AS value ' + f'FROM {_quote_identifier(measurement)} ' + f'WHERE {conditions} ' + f'GROUP BY "entity_id"' + ) + try: + for series in self._execute(query): + entity_id = (series.get("tags") or {}).get("entity_id") + if not entity_id: + continue + metric = next((item for item in measurement_metrics if item.entity_id == entity_id), None) + if metric is None: + continue + row = self._row_from_series(series) + payload[metric.id] = { + "value": row.get("value"), + "timestamp": _parse_time(row.get("time")), + } + except Exception as exc: + logger.warning("Influx latest_values error for %s: %s", measurement, exc) + return payload + + def latest_value(self, metric: MetricDefinition) -> SeriesPoint | None: + return self._single_value( + f'SELECT LAST("value") AS value ' + f'FROM {_quote_identifier(metric.measurement)} ' + f'WHERE "entity_id" = {_quote_literal(metric.entity_id)}' + ) + + def first_value(self, metric: MetricDefinition) -> SeriesPoint | None: + return self._single_value( + f'SELECT FIRST("value") AS value ' + f'FROM {_quote_identifier(metric.measurement)} ' + f'WHERE "entity_id" = {_quote_literal(metric.entity_id)}' + ) + + def last_value(self, metric: MetricDefinition) -> SeriesPoint | None: + return self.latest_value(metric) + + def gauge_history( + self, + metric: MetricDefinition, + start: datetime, + end: datetime, + interval: str, + aggregate: str = "mean", + ) -> list[SeriesPoint]: + query = ( + f'SELECT {aggregate}("value") AS value ' + f'FROM {_quote_identifier(metric.measurement)} ' + f'WHERE "entity_id" = {_quote_literal(metric.entity_id)} ' + f'AND time >= {_quote_literal(to_utc_iso(start))} ' + f'AND time <= {_quote_literal(to_utc_iso(end))} ' + f'GROUP BY time({interval}) fill(null)' + ) + points: list[SeriesPoint] = [] + try: + for series in self._execute(query): + for row in self._rows_from_series(series): + timestamp = _parse_time(row.get("time")) + if timestamp is None: + continue + points.append(SeriesPoint(timestamp=timestamp, value=to_float(row.get("value")))) + except Exception as exc: + logger.warning("Influx gauge_history error for %s: %s", metric.id, exc) + return points + + def grouped_last_series( + self, + metric: MetricDefinition, + start: datetime, + end: datetime, + interval: str, + ) -> list[SeriesPoint]: + query = ( + f'SELECT LAST("value") AS value ' + f'FROM {_quote_identifier(metric.measurement)} ' + f'WHERE "entity_id" = {_quote_literal(metric.entity_id)} ' + f'AND time >= {_quote_literal(to_utc_iso(start))} ' + f'AND time <= {_quote_literal(to_utc_iso(end))} ' + f'GROUP BY time({interval}) fill(null)' + ) + points: list[SeriesPoint] = [] + try: + for series in self._execute(query): + for row in self._rows_from_series(series): + timestamp = _parse_time(row.get("time")) + if timestamp is None: + continue + points.append(SeriesPoint(timestamp=timestamp, value=to_float(row.get("value")))) + except Exception as exc: + logger.warning("Influx grouped_last_series error for %s: %s", metric.id, exc) + return points + + def last_before(self, metric: MetricDefinition, moment: datetime) -> SeriesPoint | None: + query = ( + f'SELECT LAST("value") AS value ' + f'FROM {_quote_identifier(metric.measurement)} ' + f'WHERE "entity_id" = {_quote_literal(metric.entity_id)} ' + f'AND time < {_quote_literal(to_utc_iso(moment))}' + ) + try: + series = self._execute(query) + if not series: + return None + row = self._row_from_series(series[0]) + timestamp = _parse_time(row.get("time")) + value = to_float(row.get("value")) + if timestamp is None or value is None: + return None + return SeriesPoint(timestamp=timestamp, value=value) + except Exception as exc: + logger.warning("Influx last_before error for %s: %s", metric.id, exc) + return None + + def _single_value(self, query: str) -> SeriesPoint | None: + try: + series = self._execute(query) + if not series: + return None + row = self._row_from_series(series[0]) + timestamp = _parse_time(row.get("time")) + value = to_float(row.get("value")) + if timestamp is None or value is None: + return None + return SeriesPoint(timestamp=timestamp, value=value) + except Exception as exc: + logger.warning("Influx single value query error: %s", exc) + return None + + def _execute(self, query: str) -> list[dict]: + params = { + "db": self.settings.influx["database"], + "q": query, + } + url = f"{self.base_url}/query?{urllib.parse.urlencode(params)}" + headers = {"Accept": "application/json"} + username = self.settings.influx.get("username") or "" + password = self.settings.influx.get("password") or "" + if username: + token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii") + headers["Authorization"] = f"Basic {token}" + + request = urllib.request.Request(url, headers=headers, method="GET") + verify_ssl = self.settings.influx.get("verify_ssl", False) + timeout = self.settings.influx.get("timeout_seconds", 15) + context = None + if self.settings.influx.get("scheme") == "https" and not verify_ssl: + context = ssl._create_unverified_context() + + try: + with urllib.request.urlopen(request, timeout=timeout, context=context) as response: + payload = json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + body = exc.read().decode("utf-8", errors="ignore") + raise RuntimeError(f"Influx HTTP {exc.code}: {body}") from exc + except urllib.error.URLError as exc: + raise RuntimeError(f"Influx connection error: {exc}") from exc + + results = payload.get("results") or [] + if not results: + return [] + result = results[0] + if "error" in result: + raise RuntimeError(result["error"]) + return result.get("series") or [] + + @staticmethod + def _rows_from_series(series: dict) -> list[dict]: + columns = series.get("columns") or [] + rows = [] + for values in series.get("values") or []: + rows.append(dict(zip(columns, values))) + return rows + + @classmethod + def _row_from_series(cls, series: dict) -> dict: + rows = cls._rows_from_series(series) + return rows[0] if rows else {} + + + +def _parse_time(value: str | None) -> datetime | None: + if not value: + return None + try: + return datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError: + return None diff --git a/backend/app/services/kiosk_settings.py b/backend/app/services/kiosk_settings.py new file mode 100644 index 0000000..650728f --- /dev/null +++ b/backend/app/services/kiosk_settings.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +from typing import Any + +from flask import session + +from app.core_settings import AppSettings, get_settings +from app.storage.kiosk_settings import SQLiteKioskSettingsRepository + + +VALID_MODES = {"public", "private"} +DEFAULT_WIDGETS = ["hero", "history", "strings", "status", "production", "comparison", "importStatus"] +VALID_WIDGETS = {"hero", "quickMetrics", "history", "status", "strings", "production", "comparison", "distribution", "importStatus"} +VALID_REALTIME_RANGES = {"today", "yesterday", "6h", "12h", "24h", "48h", "7d"} +VALID_ANALYTICS_RANGES = {"today", "yesterday", "7d", "30d", "90d", "365d", "custom"} + + +class KioskSettingsService: + def __init__(self, settings: AppSettings | None = None) -> None: + self.settings = settings or get_settings() + self.repository = SQLiteKioskSettingsRepository(self.settings.storage["sqlite_path"]) + + def get(self, mode: str) -> dict[str, Any]: + normalized_mode = self._normalize_mode(mode) + stored = self.repository.get(normalized_mode) + if stored is None: + return self._default_payload(normalized_mode) + return self._sanitize_payload(normalized_mode, stored, persist_if_changed=False) + + def update(self, mode: str, payload: dict[str, Any], updated_by: str | None = None) -> dict[str, Any]: + normalized_mode = self._normalize_mode(mode) + merged = {**self.get(normalized_mode), **(payload or {})} + cleaned = self._sanitize_payload(normalized_mode, merged, persist_if_changed=False) + return self.repository.upsert(normalized_mode, cleaned, updated_by=updated_by) + + def update_from_session(self, mode: str, payload: dict[str, Any]) -> dict[str, Any]: + updated_by = session.get("auth_user") + return self.update(mode, payload, updated_by=updated_by) + + def _default_payload(self, mode: str) -> dict[str, Any]: + return { + "mode": mode, + "widgets": list(DEFAULT_WIDGETS), + "realtime_range": self._default_realtime_range(), + "analytics_range": self._default_analytics_range(), + "analytics_bucket": self._default_analytics_bucket(), + "compare_mode": self._default_compare_mode(), + "updated_at": None, + "updated_by": None, + } + + def _sanitize_payload(self, mode: str, payload: dict[str, Any], persist_if_changed: bool = False) -> dict[str, Any]: + cleaned = { + "mode": mode, + "widgets": self._normalize_widgets(payload.get("widgets")), + "realtime_range": self._normalize_realtime_range(payload.get("realtime_range")), + "analytics_range": self._normalize_analytics_range(payload.get("analytics_range")), + "analytics_bucket": self._normalize_bucket(payload.get("analytics_bucket")), + "compare_mode": self._normalize_compare_mode(payload.get("compare_mode")), + "updated_at": payload.get("updated_at"), + "updated_by": payload.get("updated_by"), + } + if persist_if_changed: + return self.repository.upsert(mode, cleaned, updated_by=cleaned.get("updated_by")) + return cleaned + + def _normalize_mode(self, mode: str) -> str: + normalized = (mode or "").strip().lower() + if normalized not in VALID_MODES: + raise ValueError("Mode musi byc jednym z: public, private") + return normalized + + def _normalize_widgets(self, widgets: Any) -> list[str]: + if not isinstance(widgets, list): + return list(DEFAULT_WIDGETS) + normalized: list[str] = [] + for item in widgets: + widget = str(item or "").strip() + if widget in VALID_WIDGETS and widget not in normalized: + normalized.append(widget) + return normalized or list(DEFAULT_WIDGETS) + + def _normalize_realtime_range(self, value: Any) -> str: + normalized = str(value or self._default_realtime_range()).strip() + return normalized if normalized in VALID_REALTIME_RANGES else self._default_realtime_range() + + def _normalize_analytics_range(self, value: Any) -> str: + normalized = str(value or self._default_analytics_range()).strip() + return normalized if normalized in VALID_ANALYTICS_RANGES else self._default_analytics_range() + + def _normalize_bucket(self, value: Any) -> str: + normalized = str(value or self._default_analytics_bucket()).strip() + return normalized if normalized in self.settings.analytics["bucket_labels"] else self._default_analytics_bucket() + + def _normalize_compare_mode(self, value: Any) -> str: + normalized = str(value or self._default_compare_mode()).strip() + return normalized if normalized in self.settings.analytics["compare_modes"] else self._default_compare_mode() + + def _default_realtime_range(self) -> str: + raw = str(self.settings.realtime.get("history_default_range", "12h")) + return raw if raw in VALID_REALTIME_RANGES else "12h" + + def _default_analytics_range(self) -> str: + raw = str(self.settings.analytics.get("default_range", "30d")) + return raw if raw in VALID_ANALYTICS_RANGES else "30d" + + def _default_analytics_bucket(self) -> str: + raw = str(self.settings.analytics.get("default_bucket", "day")) + return raw if raw in self.settings.analytics["bucket_labels"] else "day" + + def _default_compare_mode(self) -> str: + raw = str(self.settings.analytics.get("default_compare", "none")) + return raw if raw in self.settings.analytics["compare_modes"] else "none" + + +_kiosk_settings_service: KioskSettingsService | None = None + + +def get_kiosk_settings_service() -> KioskSettingsService: + global _kiosk_settings_service + if _kiosk_settings_service is None: + _kiosk_settings_service = KioskSettingsService() + return _kiosk_settings_service diff --git a/backend/app/services/metrics.py b/backend/app/services/metrics.py new file mode 100644 index 0000000..507cad8 --- /dev/null +++ b/backend/app/services/metrics.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +from datetime import datetime + +from app.models.definitions import MetricDefinition, MetricValue + + + +def to_float(value: float | str | None) -> float | None: + if value is None: + return None + if isinstance(value, (float, int)): + return float(value) + try: + return float(str(value).replace(",", ".")) + except (TypeError, ValueError): + return None + + + +def round_value(value: float | None, precision: int) -> float | None: + if value is None: + return None + return round(value, precision) + + + +def compare_delta_pct(current: float | None, previous: float | None) -> float | None: + if current is None or previous in (None, 0): + return None + return round(((current - previous) / previous) * 100.0, 2) + + + +def build_status(metric_id: str, numeric: float | None) -> str: + if numeric is None: + return "neutral" + + if metric_id == "inverter_temp": + if numeric < 55: + return "ok" + if numeric < 70: + return "warn" + return "critical" + + return "ok" + + + +def metric_value( + metric: MetricDefinition, + value: float | str | None, + *, + timestamp: datetime | None = None, +) -> MetricValue: + rendered = value + numeric = None + if metric.kind != "text": + numeric = to_float(value) + rendered = round_value(numeric, metric.precision) + + return MetricValue( + metric_id=metric.id, + label=metric.label, + unit=metric.unit, + value=rendered, + timestamp=timestamp, + precision=metric.precision, + kind=metric.kind, + status=build_status(metric.id, numeric), + ) + + + +def custom_metric_value( + metric_id: str, + label: str, + value: float | str | None, + *, + unit: str = "", + precision: int = 2, + timestamp: datetime | None = None, + status: str = "neutral", + kind: str = "gauge", +) -> MetricValue: + rendered = value + if kind != "text": + numeric = to_float(value) + rendered = round_value(numeric, precision) + return MetricValue( + metric_id=metric_id, + label=label, + unit=unit, + value=rendered, + timestamp=timestamp, + precision=precision, + kind=kind, + status=status, + ) diff --git a/backend/app/services/realtime.py b/backend/app/services/realtime.py new file mode 100644 index 0000000..8c04d9d --- /dev/null +++ b/backend/app/services/realtime.py @@ -0,0 +1,231 @@ +from __future__ import annotations + +from datetime import datetime, timedelta + +from app.core_settings import AppSettings, get_settings +from app.models.definitions import HeroCard, SnapshotGroupRow, SnapshotPayload +from app.services.catalog import MetricCatalog, get_catalog +from app.services.energy import EnergyService +from app.services.influx_http import InfluxHTTPService +from app.services.metrics import compare_delta_pct, custom_metric_value, metric_value, to_float +from app.utils.time import choose_power_interval, now_local, resolve_window, start_of_local_day + + +class RealtimeService: + def __init__( + self, + settings: AppSettings | None = None, + catalog: MetricCatalog | None = None, + influx: InfluxHTTPService | None = None, + energy: EnergyService | None = None, + ) -> None: + self.settings = settings or get_settings() + self.catalog = catalog or get_catalog() + self.influx = influx or InfluxHTTPService(self.settings) + self.energy = energy or EnergyService(self.settings, self.catalog, self.influx) + + def snapshot(self) -> SnapshotPayload: + now = now_local() + today_start = start_of_local_day(now) + yesterday_start = today_start - timedelta(days=1) + + metric_ids = {"ac_power", "energy_total", "inverter_temp"} + for group in self.settings.strings: + metric_ids.update(group.get("metrics", {}).values()) + + metrics = [self.catalog.get(metric_id) for metric_id in metric_ids if self.catalog.safe_get(metric_id)] + latest = self.influx.latest_values(metrics) + + ac_power = to_float(_value(latest, "ac_power")) + total_dc_power = round( + sum( + to_float(_value(latest, group.get("metrics", {}).get("power", ""))) or 0.0 + for group in self.settings.strings + ), + 0, + ) + energy_today = self.energy.total_for_window(today_start, now) + energy_yesterday = self.energy.total_for_window(yesterday_start, today_start) + total_energy = to_float(_value(latest, "energy_total")) + inverter_temp = to_float(_value(latest, "inverter_temp")) + + hero_cards = [ + self._hero_card("ac_power", ac_power, subtitle="Aktualna moc AC"), + self._hero_card("dc_power_total", total_dc_power, label="Moc DC laczna", unit="W", subtitle="Suma stringow DC"), + self._hero_card("energy_today", energy_today, label="Energia dzis", unit="kWh", subtitle="Liczona z danych Influx"), + self._hero_card("energy_total", total_energy, label="Energia laczna", unit="kWh", subtitle="Licznik calkowity"), + ] + if inverter_temp is not None: + hero_cards.append(self._hero_card("inverter_temp", inverter_temp, label="Temp. falownika", unit="°C", subtitle="Sensor opcjonalny")) + + kpis = { + "energy_today": custom_metric_value("energy_today", "Energia dzis", energy_today, unit="kWh", precision=2, status="ok"), + "energy_yesterday": custom_metric_value("energy_yesterday", "Energia wczoraj", energy_yesterday, unit="kWh", precision=2, status="ok"), + "energy_total": custom_metric_value( + "energy_total", + "Energia laczna", + total_energy, + unit="kWh", + precision=2, + timestamp=_timestamp(latest, "energy_total"), + status="ok", + ), + "dc_power_total": custom_metric_value("dc_power_total", "Moc DC laczna", total_dc_power, unit="W", precision=0, status="ok"), + } + + comparison = compare_delta_pct(energy_today, energy_yesterday) + if comparison is not None: + kpis["today_vs_yesterday"] = custom_metric_value( + "today_vs_yesterday", + "Dzis vs wczoraj", + comparison, + unit="%", + precision=2, + status="ok" if comparison >= 0 else "warn", + ) + + strings = self._build_string_rows(latest) + status = [] + if self.catalog.safe_get("inverter_temp"): + status.append( + metric_value( + self.catalog.get("inverter_temp"), + inverter_temp, + timestamp=_timestamp(latest, "inverter_temp"), + ) + ) + status.append( + custom_metric_value( + "data_refresh", + "Ostatni odczyt energii", + _timestamp(latest, "energy_total").isoformat() if _timestamp(latest, "energy_total") else None, + status="ok" if _timestamp(latest, "energy_total") else "neutral", + kind="text", + ) + ) + + updated_at = _max_timestamp(latest.values()) + return SnapshotPayload( + updated_at=updated_at, + hero_cards=hero_cards, + kpis=kpis, + strings=strings, + phases=[], + status=status, + faults=[], + ) + + def history(self, range_key: str | None = None, start: str | None = None, end: str | None = None, metric_ids: list[str] | None = None) -> dict: + window = resolve_window(range_key=range_key or self.settings.realtime["history_default_range"], start=start, end=end) + interval = choose_power_interval(window.start, window.end) + series = [] + + selected = set(metric_ids or []) + + def include(metric_id: str) -> bool: + return not selected or metric_id in selected + + ac_metric = self.catalog.safe_get("ac_power") + if ac_metric is not None and include("ac_power"): + series.append( + { + "metric_id": ac_metric.id, + "label": ac_metric.label, + "unit": ac_metric.unit, + "points": self.influx.gauge_history(ac_metric, window.start, window.end, interval=interval, aggregate="mean"), + } + ) + + for group in self.settings.strings: + for slot, metric_id in group.get("metrics", {}).items(): + if not metric_id or not self.catalog.safe_get(metric_id) or not include(metric_id): + continue + metric = self.catalog.get(metric_id) + series.append( + { + "metric_id": metric.id, + "label": metric.label if slot != "power" else group["label"], + "unit": metric.unit, + "points": self.influx.gauge_history(metric, window.start, window.end, interval=interval, aggregate="mean"), + } + ) + + temp_metric = self.catalog.safe_get("inverter_temp") + if temp_metric is not None and include("inverter_temp"): + temp_points = self.influx.gauge_history(temp_metric, window.start, window.end, interval=interval, aggregate="mean") + last_value = None + filled = [] + for point in temp_points: + value = point.value if point.value is not None else last_value + if point.value is not None: + last_value = point.value + filled.append({"timestamp": point.timestamp, "value": value}) + series.append( + { + "metric_id": temp_metric.id, + "label": temp_metric.label, + "unit": temp_metric.unit, + "points": filled, + } + ) + + return { + "range_key": window.key, + "start": window.start, + "end": window.end, + "series": series, + } + + def _hero_card(self, metric_id: str, value, *, label: str | None = None, unit: str | None = None, subtitle: str = "") -> HeroCard: + accent = "slate" + numeric = to_float(value) + if metric_id == "inverter_temp": + if numeric is not None and numeric < 55: + accent = "emerald" + elif numeric is not None and numeric < 70: + accent = "amber" + elif numeric is not None: + accent = "rose" + else: + accent = "emerald" if numeric not in (None, 0) else "slate" + + resolved_label = label or (self.catalog.get(metric_id).label if self.catalog.safe_get(metric_id) else metric_id) + resolved_unit = unit or (self.catalog.get(metric_id).unit if self.catalog.safe_get(metric_id) else "") + return HeroCard( + metric_id=metric_id, + label=resolved_label, + value=value, + unit=resolved_unit, + accent=accent, + subtitle=subtitle, + ) + + def _build_string_rows(self, latest: dict) -> list[SnapshotGroupRow]: + rows = [] + for group in self.settings.strings: + values = {} + for slot, metric_id in group.get("metrics", {}).items(): + metric = self.catalog.safe_get(metric_id) + if metric is None: + continue + values[slot] = metric_value(metric, _value(latest, metric_id), timestamp=_timestamp(latest, metric_id)) + rows.append(SnapshotGroupRow(id=group["id"], label=group["label"], values=values, meta={})) + return rows + + + +def _value(latest: dict, metric_id: str): + payload = latest.get(metric_id) or {} + return payload.get("value") + + + +def _timestamp(latest: dict, metric_id: str): + payload = latest.get(metric_id) or {} + return payload.get("timestamp") + + + +def _max_timestamp(items) -> datetime | None: + timestamps = [item.get("timestamp") for item in items if item.get("timestamp") is not None] + return max(timestamps) if timestamps else None diff --git a/backend/app/storage/__init__.py b/backend/app/storage/__init__.py new file mode 100644 index 0000000..5d0793a --- /dev/null +++ b/backend/app/storage/__init__.py @@ -0,0 +1,4 @@ +from .sqlite_repository import SQLiteEnergyRepository +from .auth_users import AuthUser, SQLiteAuthUserRepository + +__all__ = ["SQLiteEnergyRepository", "AuthUser", "SQLiteAuthUserRepository"] diff --git a/backend/app/storage/auth_users.py b/backend/app/storage/auth_users.py new file mode 100644 index 0000000..8800706 --- /dev/null +++ b/backend/app/storage/auth_users.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +import sqlite3 +from contextlib import contextmanager +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Iterator + + +@dataclass(frozen=True) +class AuthUser: + username: str + password_hash: str + role: str + display_name: str + is_active: bool = True + created_at: datetime | None = None + updated_at: datetime | None = None + + +class SQLiteAuthUserRepository: + def __init__(self, db_path: str) -> None: + self.db_path = Path(db_path) + self.db_path.parent.mkdir(parents=True, exist_ok=True) + self.ensure_schema() + + @contextmanager + def connect(self) -> Iterator[sqlite3.Connection]: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + try: + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA synchronous=NORMAL") + yield conn + conn.commit() + finally: + conn.close() + + def ensure_schema(self) -> None: + with self.connect() as conn: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS auth_users ( + username TEXT PRIMARY KEY, + password_hash TEXT NOT NULL, + role TEXT NOT NULL, + display_name TEXT NOT NULL, + is_active INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ) + """ + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_auth_users_role ON auth_users(role)" + ) + + def get_by_username(self, username: str) -> AuthUser | None: + with self.connect() as conn: + row = conn.execute( + """ + SELECT username, password_hash, role, display_name, is_active, created_at, updated_at + FROM auth_users + WHERE username = ? + LIMIT 1 + """, + (username,), + ).fetchone() + if row is None: + return None + return AuthUser( + username=row["username"], + password_hash=row["password_hash"], + role=row["role"], + display_name=row["display_name"], + is_active=bool(row["is_active"]), + created_at=datetime.fromisoformat(row["created_at"]), + updated_at=datetime.fromisoformat(row["updated_at"]), + ) + + def upsert_user(self, *, username: str, password_hash: str, role: str, display_name: str, is_active: bool = True) -> AuthUser: + now = datetime.utcnow().isoformat() + with self.connect() as conn: + conn.execute( + """ + INSERT INTO auth_users (username, password_hash, role, display_name, is_active, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(username) DO UPDATE SET + password_hash = excluded.password_hash, + role = excluded.role, + display_name = excluded.display_name, + is_active = excluded.is_active, + updated_at = excluded.updated_at + """, + (username, password_hash, role, display_name, 1 if is_active else 0, now, now), + ) + return self.get_by_username(username) # type: ignore[return-value] + + def update_password(self, username: str, password_hash: str) -> AuthUser | None: + now = datetime.utcnow().isoformat() + with self.connect() as conn: + cursor = conn.execute( + "UPDATE auth_users SET password_hash = ?, updated_at = ? WHERE username = ?", + (password_hash, now, username), + ) + if cursor.rowcount == 0: + return None + return self.get_by_username(username) + + + def list_users(self) -> list[AuthUser]: + with self.connect() as conn: + rows = conn.execute( + """ + SELECT username, password_hash, role, display_name, is_active, created_at, updated_at + FROM auth_users + ORDER BY role DESC, username ASC + """ + ).fetchall() + return [ + AuthUser( + username=row["username"], + password_hash=row["password_hash"], + role=row["role"], + display_name=row["display_name"], + is_active=bool(row["is_active"]), + created_at=datetime.fromisoformat(row["created_at"]), + updated_at=datetime.fromisoformat(row["updated_at"]), + ) + for row in rows + ] diff --git a/backend/app/storage/kiosk_settings.py b/backend/app/storage/kiosk_settings.py new file mode 100644 index 0000000..38c820e --- /dev/null +++ b/backend/app/storage/kiosk_settings.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import json +import sqlite3 +from contextlib import contextmanager +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Any, Iterator + + +@dataclass +class KioskSettingsRecord: + mode: str + widgets: list[str] + realtime_range: str + analytics_range: str + analytics_bucket: str + compare_mode: str + updated_at: datetime | None = None + updated_by: str | None = None + + +class SQLiteKioskSettingsRepository: + def __init__(self, db_path: str) -> None: + self.db_path = Path(db_path) + self.db_path.parent.mkdir(parents=True, exist_ok=True) + self.ensure_schema() + + @contextmanager + def connect(self) -> Iterator[sqlite3.Connection]: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + try: + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA synchronous=NORMAL") + yield conn + conn.commit() + finally: + conn.close() + + def ensure_schema(self) -> None: + with self.connect() as conn: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS kiosk_settings ( + mode TEXT PRIMARY KEY, + payload_json TEXT NOT NULL, + updated_at TEXT NOT NULL, + updated_by TEXT + ) + """ + ) + + def get(self, mode: str) -> dict[str, Any] | None: + with self.connect() as conn: + row = conn.execute( + "SELECT mode, payload_json, updated_at, updated_by FROM kiosk_settings WHERE mode = ? LIMIT 1", + (mode,), + ).fetchone() + if row is None: + return None + payload = json.loads(row["payload_json"]) + payload["mode"] = row["mode"] + payload["updated_at"] = row["updated_at"] + payload["updated_by"] = row["updated_by"] + return payload + + def upsert(self, mode: str, payload: dict[str, Any], updated_by: str | None = None) -> dict[str, Any]: + now = datetime.utcnow().isoformat() + stored_payload = dict(payload) + stored_payload.pop("mode", None) + stored_payload.pop("updated_at", None) + stored_payload.pop("updated_by", None) + with self.connect() as conn: + conn.execute( + """ + INSERT INTO kiosk_settings (mode, payload_json, updated_at, updated_by) + VALUES (?, ?, ?, ?) + ON CONFLICT(mode) DO UPDATE SET + payload_json = excluded.payload_json, + updated_at = excluded.updated_at, + updated_by = excluded.updated_by + """, + (mode, json.dumps(stored_payload, ensure_ascii=False), now, updated_by), + ) + return self.get(mode) or {"mode": mode, **stored_payload, "updated_at": now, "updated_by": updated_by} diff --git a/backend/app/storage/sqlite_repository.py b/backend/app/storage/sqlite_repository.py new file mode 100644 index 0000000..9e8da10 --- /dev/null +++ b/backend/app/storage/sqlite_repository.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +import sqlite3 +from contextlib import contextmanager +from datetime import datetime, date +from pathlib import Path +from typing import Iterator + +from app.models import DailyEnergyRecord, HistoricalCoverage + + +class SQLiteEnergyRepository: + def __init__(self, db_path: str) -> None: + self.db_path = Path(db_path) + self.db_path.parent.mkdir(parents=True, exist_ok=True) + self.ensure_schema() + + @contextmanager + def connect(self) -> Iterator[sqlite3.Connection]: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + try: + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA synchronous=NORMAL") + yield conn + conn.commit() + finally: + conn.close() + + def ensure_schema(self) -> None: + with self.connect() as conn: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS daily_energy ( + day TEXT PRIMARY KEY, + energy_kwh REAL NOT NULL, + source TEXT NOT NULL, + samples_count INTEGER NOT NULL DEFAULT 0, + imported_at TEXT NOT NULL + ) + """ + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_daily_energy_imported_at ON daily_energy(imported_at)" + ) + + def has_day(self, day: date) -> bool: + with self.connect() as conn: + row = conn.execute("SELECT 1 FROM daily_energy WHERE day = ? LIMIT 1", (day.isoformat(),)).fetchone() + return row is not None + + def upsert_daily_energy(self, record: DailyEnergyRecord) -> None: + imported_at = record.imported_at or datetime.utcnow() + with self.connect() as conn: + conn.execute( + """ + INSERT INTO daily_energy (day, energy_kwh, source, samples_count, imported_at) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(day) DO UPDATE SET + energy_kwh = excluded.energy_kwh, + source = excluded.source, + samples_count = excluded.samples_count, + imported_at = excluded.imported_at + """, + ( + record.day.isoformat(), + float(record.energy_kwh), + record.source, + int(record.samples_count), + imported_at.isoformat(), + ), + ) + + def fetch_daily_energy(self, start_day: date, end_day: date) -> dict[date, DailyEnergyRecord]: + with self.connect() as conn: + rows = conn.execute( + """ + SELECT day, energy_kwh, source, samples_count, imported_at + FROM daily_energy + WHERE day >= ? AND day <= ? + ORDER BY day ASC + """, + (start_day.isoformat(), end_day.isoformat()), + ).fetchall() + + payload: dict[date, DailyEnergyRecord] = {} + for row in rows: + payload[date.fromisoformat(row["day"])] = DailyEnergyRecord( + day=date.fromisoformat(row["day"]), + energy_kwh=float(row["energy_kwh"]), + source=row["source"], + samples_count=int(row["samples_count"]), + imported_at=datetime.fromisoformat(row["imported_at"]), + ) + return payload + + def coverage(self) -> HistoricalCoverage: + with self.connect() as conn: + row = conn.execute( + """ + SELECT + COUNT(*) AS imported_days, + MIN(day) AS first_day, + MAX(day) AS last_day, + COALESCE(SUM(energy_kwh), 0) AS total_energy_kwh + FROM daily_energy + """ + ).fetchone() + + if row is None: + return HistoricalCoverage() + + return HistoricalCoverage( + imported_days=int(row["imported_days"] or 0), + first_day=date.fromisoformat(row["first_day"]) if row["first_day"] else None, + last_day=date.fromisoformat(row["last_day"]) if row["last_day"] else None, + total_energy_kwh=round(float(row["total_energy_kwh"] or 0.0), 2), + ) + + def latest_day(self) -> date | None: + return self.coverage().last_day + + def count_missing_days(self, start_day: date, end_day: date) -> int: + existing = self.fetch_daily_energy(start_day, end_day) + current = start_day + missing = 0 + while current <= end_day: + if current not in existing: + missing += 1 + current = current.fromordinal(current.toordinal() + 1) + return missing diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000..e2eeae5 --- /dev/null +++ b/backend/app/utils/__init__.py @@ -0,0 +1,25 @@ +from .serialization import to_plain +from .time import ( + TimeWindow, + choose_counter_interval, + choose_power_interval, + duration_to_seconds, + now_local, + resolve_window, + shift_window, + start_of_local_day, + to_utc_iso, +) + +__all__ = [ + "TimeWindow", + "choose_counter_interval", + "choose_power_interval", + "duration_to_seconds", + "now_local", + "resolve_window", + "shift_window", + "start_of_local_day", + "to_plain", + "to_utc_iso", +] diff --git a/backend/app/utils/serialization.py b/backend/app/utils/serialization.py new file mode 100644 index 0000000..7c49203 --- /dev/null +++ b/backend/app/utils/serialization.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import asdict, is_dataclass +from datetime import date, datetime +from typing import Any + + +def to_plain(value: Any) -> Any: + if is_dataclass(value): + return to_plain(asdict(value)) + if isinstance(value, datetime): + return value.isoformat() + if isinstance(value, date): + return value.isoformat() + if isinstance(value, dict): + return {key: to_plain(item) for key, item in value.items()} + if isinstance(value, (list, tuple, set)): + return [to_plain(item) for item in value] + return value diff --git a/backend/app/utils/time.py b/backend/app/utils/time.py new file mode 100644 index 0000000..f199803 --- /dev/null +++ b/backend/app/utils/time.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timedelta +from zoneinfo import ZoneInfo + +from config import TIME_RANGES +from app.core_settings import get_settings + + +@dataclass +class TimeWindow: + start: datetime + end: datetime + label: str + key: str + + +def now_local() -> datetime: + settings = get_settings() + return datetime.now(ZoneInfo(settings.timezone)) + + +def start_of_local_day(moment: datetime | None = None) -> datetime: + current = moment or now_local() + return current.replace(hour=0, minute=0, second=0, microsecond=0) + + +def resolve_window(range_key: str | None = None, start: str | None = None, end: str | None = None) -> TimeWindow: + settings = get_settings() + tz = ZoneInfo(settings.timezone) + + if (start and not end) or (end and not start): + raise ValueError("Provide both start and end for custom range") + + if start and end: + start_dt = _parse_iso(start, tz) + end_dt = _parse_iso(end, tz) + return TimeWindow(start=start_dt, end=end_dt, label="Custom", key="custom") + + key = range_key or settings.analytics["default_range"] + definition = settings.time_ranges.get(key) + if not definition: + raise ValueError(f"Unsupported range: {key}") + + now_dt = datetime.now(tz) + end_dt = now_dt + special = definition.get("special") + if special == "ytd": + start_dt = now_dt.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0) + elif special == "today": + start_dt = now_dt.replace(hour=0, minute=0, second=0, microsecond=0) + end_dt = now_dt + elif special == "yesterday": + end_dt = now_dt.replace(hour=0, minute=0, second=0, microsecond=0) + start_dt = end_dt - timedelta(days=1) + else: + start_dt = now_dt - timedelta(seconds=int(definition["seconds"])) + + return TimeWindow(start=start_dt, end=end_dt, label=definition["label"], key=key) + + +def shift_window(window: TimeWindow, mode: str) -> TimeWindow: + if mode == "previous_period": + span = window.end - window.start + return TimeWindow( + start=window.start - span, + end=window.start, + label="Previous period", + key=f"{window.key}:previous_period", + ) + + if mode in {"previous_year", "previous_year_2", "previous_year_3"}: + years = {"previous_year": 1, "previous_year_2": 2, "previous_year_3": 3}[mode] + return TimeWindow( + start=_safe_replace_year(window.start, window.start.year - years), + end=_safe_replace_year(window.end, window.end.year - years), + label=f"Previous {years} year", + key=f"{window.key}:{mode}", + ) + + if mode in {"previous_month_12", "previous_month_24"}: + months = {"previous_month_12": 12, "previous_month_24": 24}[mode] + return TimeWindow( + start=_shift_months(window.start, -months), + end=_shift_months(window.end, -months), + label=f"Previous {months} months", + key=f"{window.key}:{mode}", + ) + + raise ValueError(f"Unsupported compare mode: {mode}") + + +def choose_counter_interval(start: datetime, end: datetime) -> str: + span_seconds = max((end - start).total_seconds(), 0) + if span_seconds <= 3 * 86400: + return "5m" + if span_seconds <= 14 * 86400: + return "15m" + if span_seconds <= 93 * 86400: + return "30m" + if span_seconds <= 366 * 86400: + return "1h" + return "3h" + + +def choose_power_interval(start: datetime, end: datetime) -> str: + span_seconds = max((end - start).total_seconds(), 0) + if span_seconds <= 24 * 3600: + return "5m" + if span_seconds <= 7 * 86400: + return "15m" + if span_seconds <= 31 * 86400: + return "30m" + if span_seconds <= 366 * 86400: + return "1h" + return "3h" + + +def duration_to_seconds(interval: str) -> int: + suffix = interval[-1] + amount = int(interval[:-1]) + if suffix == "s": + return amount + if suffix == "m": + return amount * 60 + if suffix == "h": + return amount * 3600 + if suffix == "d": + return amount * 86400 + raise ValueError(f"Unsupported duration format: {interval}") + + +def to_utc_iso(dt: datetime) -> str: + return dt.astimezone(ZoneInfo("UTC")).isoformat() + + +def _parse_iso(value: str, tz: ZoneInfo) -> datetime: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + if parsed.tzinfo is None: + return parsed.replace(tzinfo=tz) + return parsed.astimezone(tz) + + +def _safe_replace_year(value: datetime, year: int) -> datetime: + try: + return value.replace(year=year) + except ValueError: + return value.replace(year=year, day=28) + + +def _shift_months(value: datetime, months: int) -> datetime: + year = value.year + ((value.month - 1 + months) // 12) + month = ((value.month - 1 + months) % 12) + 1 + day = min(value.day, [31, 29 if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1]) + return value.replace(year=year, month=month, day=day) diff --git a/backend/backfill.py b/backend/backfill.py new file mode 100644 index 0000000..48c86d2 --- /dev/null +++ b/backend/backfill.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +import argparse +from datetime import date + +from app.services.historical_sync import get_historical_sync_service +from app.utils.serialization import to_plain + + +parser = argparse.ArgumentParser(description="Import dziennych agregatow PV z InfluxDB do lokalnego cache SQLite") +parser.add_argument("--start-date", dest="start_date", help="Data startowa YYYY-MM-DD") +parser.add_argument("--end-date", dest="end_date", help="Data koncowa YYYY-MM-DD") +parser.add_argument("--chunk-days", dest="chunk_days", type=int, default=7, help="Liczba dni na chunk") +parser.add_argument("--force", action="store_true", help="Nadpisz dni juz zapisane w cache") +args = parser.parse_args() + +service = get_historical_sync_service() +status = service.run_blocking( + start_date=date.fromisoformat(args.start_date) if args.start_date else None, + end_date=date.fromisoformat(args.end_date) if args.end_date else None, + chunk_days=args.chunk_days, + force=args.force, +) +print(to_plain(status)) diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..be95513 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,289 @@ +from __future__ import annotations + +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent +PROJECT_DIR = BASE_DIR.parent +DATA_DIR = PROJECT_DIR / "data" +DATA_DIR.mkdir(exist_ok=True) + + +def _load_dotenv(path: Path) -> None: + if not path.exists(): + return + + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + + key, value = line.split("=", 1) + key = key.strip() + if not key or key in os.environ: + continue + + cleaned = value.strip().strip('"').strip("'") + os.environ[key] = cleaned + + +_load_dotenv(PROJECT_DIR / ".env") + + +def env_bool(name: str, default: bool = False) -> bool: + return os.getenv(name, str(default)).strip().lower() in {"1", "true", "yes", "on"} + + +def env_int(name: str, default: int) -> int: + try: + return int(os.getenv(name, str(default))) + except (TypeError, ValueError): + return default + + +def env_float(name: str, default: float) -> float: + try: + return float(os.getenv(name, str(default)).replace(",", ".")) + except (TypeError, ValueError): + return default + + +APP_CONFIG = { + "name": os.getenv("APP_NAME", "PV Insight"), + "version": os.getenv("APP_VERSION", "1.3.0"), + "debug": env_bool("APP_DEBUG", False), + "api_prefix": "/api/v1", + "timezone": os.getenv("APP_TIMEZONE", "Europe/Warsaw"), + "host": os.getenv("APP_HOST", "0.0.0.0"), + "port": env_int("APP_PORT", 8105), +} + +SITE_CONFIG = { + "site_name": os.getenv("SITE_NAME", "Domowa instalacja PV"), + "timezone": APP_CONFIG["timezone"], + "installed_power_kwp": env_float("PV_INSTALLED_POWER_KWP", 9.99), + "currency": os.getenv("SITE_CURRENCY", "PLN"), + "co2_factor_kg_per_kwh": env_float("CO2_FACTOR_KG_PER_KWH", 0.72), +} + +INFLUXDB_CONFIG = { + "scheme": os.getenv("INFLUXDB_SCHEME", "http"), + "host": os.getenv("INFLUXDB_HOST", "127.0.0.1"), + "port": env_int("INFLUXDB_PORT", 8086), + "database": os.getenv("INFLUXDB_DATABASE", "ha"), + "username": os.getenv("INFLUXDB_USER", ""), + "password": os.getenv("INFLUXDB_PASSWORD", ""), + "verify_ssl": env_bool("INFLUXDB_VERIFY_SSL", False), + "timeout_seconds": env_int("INFLUXDB_TIMEOUT_SECONDS", 15), +} + +STORAGE_CONFIG = { + "sqlite_path": os.getenv("APP_SQLITE_PATH", str(DATA_DIR / "pv_insight.sqlite3")), +} + +CORS_ORIGINS = [ + value.strip() + for value in os.getenv( + "CORS_ORIGINS", + "http://localhost:5173,http://127.0.0.1:5173,http://localhost:4173,http://127.0.0.1:4173", + ).split(",") + if value.strip() +] + +TIME_RANGES = { + "today": {"label": "Dzis", "special": "today"}, + "yesterday": {"label": "Wczoraj", "special": "yesterday"}, + "6h": {"label": "6h", "seconds": 6 * 3600}, + "12h": {"label": "12h", "seconds": 12 * 3600}, + "24h": {"label": "24h", "seconds": 24 * 3600}, + "48h": {"label": "48h", "seconds": 48 * 3600}, + "1d": {"label": "1 dzien", "seconds": 1 * 24 * 3600}, + "3d": {"label": "3 dni", "seconds": 3 * 24 * 3600}, + "7d": {"label": "7 dni", "seconds": 7 * 24 * 3600}, + "14d": {"label": "14 dni", "seconds": 14 * 24 * 3600}, + "30d": {"label": "30 dni", "seconds": 30 * 24 * 3600}, + "60d": {"label": "60 dni", "seconds": 60 * 24 * 3600}, + "90d": {"label": "90 dni", "seconds": 90 * 24 * 3600}, + "365d": {"label": "365 dni", "seconds": 365 * 24 * 3600}, + "ytd": {"label": "YTD", "special": "ytd"}, +} + +REALTIME = { + "refresh_seconds": env_int("REALTIME_REFRESH_SECONDS", 8), + "history_default_range": os.getenv("REALTIME_HISTORY_DEFAULT_RANGE", "6h"), +} + +ANALYTICS = { + "production_metric_id": "energy_total", + "fallback_power_metric_id": "ac_power", + "default_range": os.getenv("ANALYTICS_DEFAULT_RANGE", "30d"), + "default_bucket": os.getenv("ANALYTICS_DEFAULT_BUCKET", "day"), + "bucket_labels": { + "day": "Dzien", + "week": "Tydzien", + "month": "Miesiac", + "year": "Rok", + }, + "compare_modes": { + "none": "Porownanie", + "previous_period": "Poprzedni okres", + "previous_year": "Poprzedni rok", + "previous_year_2": "2 lata wstecz", + "previous_year_3": "3 lata wstecz", + "previous_month_12": "12 miesiecy wstecz", + "previous_month_24": "24 miesiace wstecz", + "custom_multi": "Wlasne zakresy", + }, +} + +HISTORY = { + "enabled": env_bool("HISTORY_ENABLED", True), + "chunk_days": env_int("HISTORY_CHUNK_DAYS", 7), + "default_chunk_days": env_int("HISTORY_DEFAULT_CHUNK_DAYS", 7), + "auto_sync_enabled": env_bool("HISTORY_AUTO_SYNC_ENABLED", True), + "auto_sync_on_start": env_bool("HISTORY_AUTO_SYNC_ON_START", False), + "auto_sync_interval_minutes": env_int("HISTORY_AUTO_SYNC_INTERVAL_MINUTES", 30), + "include_today_in_sync": env_bool("HISTORY_INCLUDE_TODAY_IN_SYNC", False), + "bootstrap_start_date": os.getenv("HISTORY_BOOTSTRAP_START_DATE", "").strip(), +} + + +AUTH_CONFIG = { + "enabled": env_bool("AUTH_ENABLED", True), + "username": os.getenv("AUTH_USERNAME", "admin"), + "password": os.getenv("AUTH_PASSWORD", "change-me"), + "password_hash": os.getenv("AUTH_PASSWORD_HASH", "").strip(), + "display_name": os.getenv("AUTH_DISPLAY_NAME", "Operator"), + "secret_key": os.getenv("APP_SECRET_KEY", "pv-insight-dev-secret-change-me"), + "session_cookie_name": os.getenv("APP_SESSION_COOKIE_NAME", "pv_insight_session"), + "session_max_age_seconds": env_int("AUTH_SESSION_MAX_AGE_SECONDS", 60 * 60 * 12), + "cookie_secure": env_bool("AUTH_COOKIE_SECURE", False), + "cookie_samesite": os.getenv("AUTH_COOKIE_SAMESITE", "Lax"), +} + +I18N = { + "default_language": os.getenv("APP_DEFAULT_LANGUAGE", "pl"), + "supported_languages": ["pl", "en"], +} + +FRONTEND_DEFAULTS = { + "tab": os.getenv("FRONTEND_DEFAULT_TAB", "realtime"), + "theme": os.getenv("FRONTEND_THEME", "dark"), + "language": os.getenv("FRONTEND_LANGUAGE", I18N["default_language"]), +} + +METRICS: dict[str, dict] = {} +STRINGS: list[dict] = [] + + +def register_metric( + metric_id: str, + *, + entity_id: str, + measurement: str, + unit: str, + label: str, + kind: str = "gauge", + precision: int = 2, +) -> str | None: + entity_id = (entity_id or "").strip() + measurement = (measurement or "").strip() + if not entity_id or not measurement: + return None + + METRICS[metric_id] = { + "entity_id": entity_id, + "measurement": measurement, + "unit": unit, + "label": label, + "kind": kind, + "precision": precision, + "enabled": True, + } + return metric_id + + +register_metric( + "ac_power", + entity_id=os.getenv("PV_AC_POWER_ENTITY", "sofarsolar_ac_power"), + measurement=os.getenv("PV_AC_POWER_MEASUREMENT", "W"), + unit="W", + label="Moc AC", + precision=0, +) +register_metric( + "energy_total", + entity_id=os.getenv("PV_TOTAL_ENERGY_ENTITY", "sofarsolar_energy_total"), + measurement=os.getenv("PV_TOTAL_ENERGY_MEASUREMENT", "kWh"), + unit="kWh", + label="Energia laczna", + kind="counter", + precision=2, +) +register_metric( + "inverter_temp", + entity_id=os.getenv("PV_INVERTER_TEMP_ENTITY", "sofarsolar_temprature_inverter"), + measurement=os.getenv("PV_INVERTER_TEMP_MEASUREMENT", "°C"), + unit="°C", + label="Temperatura falownika", + precision=1, +) + +STRING_DEFAULTS = { + 1: {"label": "DC1", "power": "sofarsolar_dc1_power", "voltage": "sofarsolar_dc1_voltage"}, + 2: {"label": "DC2", "power": "sofarsolar_dc2_power", "voltage": "sofarsolar_dc2_voltage"}, + 3: {"label": "DC3", "power": "", "voltage": ""}, + 4: {"label": "DC4", "power": "", "voltage": ""}, +} + +for index, defaults in STRING_DEFAULTS.items(): + label = os.getenv(f"PV_STRING_{index}_LABEL", defaults["label"]).strip() or defaults["label"] + power_metric_id = register_metric( + f"string_{index}_power", + entity_id=os.getenv(f"PV_STRING_{index}_POWER_ENTITY", defaults["power"]), + measurement=os.getenv(f"PV_STRING_{index}_POWER_MEASUREMENT", "W"), + unit="W", + label=f"{label} moc", + precision=0, + ) + voltage_metric_id = register_metric( + f"string_{index}_voltage", + entity_id=os.getenv(f"PV_STRING_{index}_VOLTAGE_ENTITY", defaults["voltage"]), + measurement=os.getenv(f"PV_STRING_{index}_VOLTAGE_MEASUREMENT", "V"), + unit="V", + label=f"{label} napiecie", + precision=1, + ) + + if power_metric_id or voltage_metric_id: + STRINGS.append( + { + "id": f"string_{index}", + "label": label, + "metrics": { + key: value + for key, value in { + "power": power_metric_id, + "voltage": voltage_metric_id, + }.items() + if value + }, + } + ) + +MODULES = { + "realtime_overview": True, + "realtime_history": True, + "analytics": "energy_total" in METRICS or "ac_power" in METRICS, + "comparison": "energy_total" in METRICS or "ac_power" in METRICS, + "distribution_pie": "energy_total" in METRICS or "ac_power" in METRICS, + "strings": len(STRINGS) > 0, + "temperatures": "inverter_temp" in METRICS, + "historical_import": HISTORY["enabled"], + "phases": False, + "faults": False, + "settings_panel": True, +} + +STATUS_METRICS = [metric_id for metric_id in ["inverter_temp"] if metric_id in METRICS] +VISIBLE_ENTITY_TABLE = list(METRICS.keys()) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..f2826e1 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,2 @@ +Flask>=3.1,<4 +waitress>=3.0.2,<4 diff --git a/backend/run.py b/backend/run.py new file mode 100644 index 0000000..3332142 --- /dev/null +++ b/backend/run.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from app.main import app +from app.core_settings import get_settings + + +if __name__ == "__main__": + settings = get_settings() + app.run( + host=settings.host, + port=settings.port, + debug=settings.debug, + use_reloader=settings.debug, + threaded=True, + ) diff --git a/backend/run_prod.py b/backend/run_prod.py new file mode 100644 index 0000000..75b8d1d --- /dev/null +++ b/backend/run_prod.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +import os + +from waitress import serve + +from app.main import app +from app.core_settings import get_settings + + +if __name__ == "__main__": + settings = get_settings() + try: + threads = int(os.getenv("WAITRESS_THREADS", "8")) + except (TypeError, ValueError): + threads = 8 + + serve(app, host=settings.host, port=settings.port, threads=threads) diff --git a/deploy/nginx/default.conf b/deploy/nginx/default.conf new file mode 100644 index 0000000..2bada72 --- /dev/null +++ b/deploy/nginx/default.conf @@ -0,0 +1,23 @@ +server { + listen 80; + server_name _; + server_tokens off; + + location /api/ { + proxy_pass http://backend:8105; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://frontend:80; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..f34d503 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,27 @@ +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile.dev + env_file: + - ./.env + environment: + APP_HOST: 0.0.0.0 + APP_PORT: 8105 + APP_DEBUG: "true" + CORS_ORIGINS: http://localhost:5173,http://127.0.0.1:5173,http://localhost:4173,http://127.0.0.1:4173 + volumes: + - ./data:/app/data + ports: + - "8105:8105" + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile.dev + environment: + VITE_DEMO_MODE: "false" + ports: + - "5173:5173" + depends_on: + - backend diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..32ebed7 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,36 @@ +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + env_file: + - ./.env + environment: + APP_HOST: 0.0.0.0 + APP_PORT: 8105 + volumes: + - ./data:/app/data + expose: + - "8105" + restart: unless-stopped + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + depends_on: + - backend + expose: + - "80" + restart: unless-stopped + + reverse-proxy: + image: nginx:1.28.2-alpine + depends_on: + - frontend + - backend + volumes: + - ./deploy/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + ports: + - "8787:80" + restart: unless-stopped diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f7519f5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + env_file: + - ./.env + environment: + APP_HOST: 0.0.0.0 + APP_PORT: 8105 + volumes: + - ./data:/app/data + ports: + - "8105:8105" + restart: unless-stopped + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "8080:80" + restart: unless-stopped + depends_on: + - backend diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..e537e12 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,4 @@ +node_modules +dist +*.log +.env diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..0e93249 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,18 @@ +FROM node:22-alpine AS build + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +FROM nginx:1.28-alpine + +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev new file mode 100644 index 0000000..bfd0a73 --- /dev/null +++ b/frontend/Dockerfile.dev @@ -0,0 +1,13 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package.json package-lock.json tsconfig.json vite.config.ts index.html /app/ +COPY public /app/public +COPY src /app/src + +RUN npm ci + +EXPOSE 5173 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..2cf70b6 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + PV Insight + + +
+ + + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..21ae07d --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,30 @@ +server { + listen 80; + server_name _; + server_tokens off; + + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://backend:8105; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + } + + location = /health { + proxy_pass http://backend:8105/health; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + } + + location / { + try_files $uri $uri/ /index.html; + } +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..e08b912 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2553 @@ +{ + "name": "pv-insight-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pv-insight-frontend", + "version": "1.0.0", + "dependencies": { + "@tanstack/react-query": "^5.0.0", + "clsx": "^2.1.1", + "echarts": "^6.0.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.2.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.0.0", + "tailwindcss": "^4.2.0", + "typescript": "^5.0.0", + "vite": "^7.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.95.0.tgz", + "integrity": "sha512-H1/CWCe8tGL3YIVeo770Z6kPbt0B3M1d/iQXIIK1qlFiFt6G2neYdkHgLapOC8uMYNt9DmHjmGukEKgdMk1P+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.95.0.tgz", + "integrity": "sha512-EMP8B+BK9zvnAemT8M/y3z/WO0NjZ7fIUY3T3wnHYK6AA3qK/k33i7tPgCXCejhX0cd4I6bJIXN2GmjrHjDBzg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.95.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..4f2d1e0 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "pv-insight-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview --host 0.0.0.0 --port 4173", + "dev:demo": "VITE_DEMO_MODE=true vite", + "build:demo": "VITE_DEMO_MODE=true tsc --noEmit && vite build" + }, + "dependencies": { + "@tanstack/react-query": "^5.0.0", + "clsx": "^2.1.1", + "echarts": "^6.0.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.2.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.0.0", + "tailwindcss": "^4.2.0", + "typescript": "^5.0.0", + "vite": "^7.0.0" + } +} \ No newline at end of file diff --git a/frontend/public/vendor/tabler/LICENSE.txt b/frontend/public/vendor/tabler/LICENSE.txt new file mode 100644 index 0000000..1999fea --- /dev/null +++ b/frontend/public/vendor/tabler/LICENSE.txt @@ -0,0 +1,183 @@ +Tabler core 1.4.0 +Source package: @tabler/core +License metadata: MIT +Upstream package.json follows: + +{ + "name": "@tabler/core", + "version": "1.4.0", + "description": "Premium and Open Source dashboard template with responsive and high quality UI.", + "homepage": "https://tabler.io", + "repository": { + "type": "git", + "url": "git+https://github.com/tabler/tabler.git" + }, + "keywords": [ + "css", + "sass", + "mobile-first", + "responsive", + "front-end", + "framework", + "web" + ], + "author": "codecalm", + "license": "MIT", + "bugs": { + "url": "https://github.com/tabler/tabler/issues" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "engines": { + "node": ">=20" + }, + "files": [ + "docs/**/*", + "dist/**/*", + "js/**/*.{js,map}", + "img/**/*.{svg}", + "scss/**/*.scss", + "libs.json" + ], + "style": "dist/css/tabler.css", + "sass": "scss/tabler.scss", + "unpkg": "dist/js/tabler.min.js", + "umd:main": "dist/js/tabler.min.js", + "module": "dist/js/tabler.esm.js", + "main": "dist/js/tabler.js", + "bundlewatch": { + "files": [ + { + "path": "./dist/css/tabler.css", + "maxSize": "75 kB" + }, + { + "path": "./dist/css/tabler.min.css", + "maxSize": "70 kB" + }, + { + "path": "./dist/css/tabler.rtl.css", + "maxSize": "75 kB" + }, + { + "path": "./dist/css/tabler.rtl.min.css", + "maxSize": "70 kB" + }, + { + "path": "./dist/css/tabler-flags.css", + "maxSize": "2.5 kB" + }, + { + "path": "./dist/css/tabler-flags.min.css", + "maxSize": "2 kB" + }, + { + "path": "./dist/css/tabler-payments.css", + "maxSize": "2.2 kB" + }, + { + "path": "./dist/css/tabler-payments.min.css", + "maxSize": "2 kB" + }, + { + "path": "./dist/css/tabler-socials.css", + "maxSize": "2 kB" + }, + { + "path": "./dist/css/tabler-socials.min.css", + "maxSize": "2 kB" + }, + { + "path": "./dist/css/tabler-vendors.css", + "maxSize": "7.5 kB" + }, + { + "path": "./dist/css/tabler-vendors.min.css", + "maxSize": "6.5 kB" + }, + { + "path": "./dist/js/tabler.js", + "maxSize": "60 kB" + }, + { + "path": "./dist/js/tabler.min.js", + "maxSize": "45 kB" + }, + { + "path": "./dist/js/tabler.esm.js", + "maxSize": "60 kB" + }, + { + "path": "./dist/js/tabler.esm.min.js", + "maxSize": "45 kB" + } + ] + }, + "dependencies": { + "@popperjs/core": "^2.11.8", + "bootstrap": "5.3.7" + }, + "devDependencies": { + "@hotwired/turbo": "^8.0.13", + "@melloware/coloris": "^0.25.0", + "apexcharts": "3.54.1", + "autosize": "^6.0.1", + "choices.js": "^11.1.0", + "clipboard": "^2.0.11", + "countup.js": "^2.9.0", + "dropzone": "^6.0.0-beta.2", + "flatpickr": "^4.6.13", + "fslightbox": "^3.6.1", + "fullcalendar": "^6.1.18", + "hugerte": "^1.0.9", + "imask": "^7.6.1", + "jsvectormap": "^1.7.0", + "list.js": "^2.3.1", + "litepicker": "^2.0.12", + "nouislider": "^15.8.1", + "plyr": "^3.7.8", + "signature_pad": "^5.0.10", + "star-rating.js": "^4.3.1", + "tom-select": "^2.4.3", + "typed.js": "^2.1.0" + }, + "directories": { + "doc": "docs" + }, + "scripts": { + "dev": "pnpm run clean && pnpm run copy && pnpm run watch", + "build": "pnpm run clean && pnpm run css && pnpm run js && pnpm run copy && pnpm run generate-sri", + "clean": "shx rm -rf dist demo", + "css": "pnpm run css-compile && pnpm run css-prefix && pnpm run css-rtl && pnpm run css-minify && pnpm run css-banner", + "css-compile": "sass --no-source-map --load-path=node_modules --style expanded scss/:dist/css/", + "css-banner": "node .build/add-banner.mjs", + "css-prefix": "postcss --config .build/postcss.config.mjs --replace \"dist/css/*.css\" \"!dist/css/*.rtl*.css\" \"!dist/css/*.min.css\"", + "css-rtl": "cross-env NODE_ENV=RTL postcss --config .build/postcss.config.mjs --dir \"dist/css\" --ext \".rtl.css\" \"dist/css/*.css\" \"!dist/css/*.min.css\" \"!dist/css/*.rtl.css\"", + "css-minify": "pnpm run css-minify-main && pnpm run css-minify-rtl", + "css-minify-main": "cleancss -O1 --format breakWith=lf --with-rebase --source-map --source-map-inline-sources --output dist/css/ --batch --batch-suffix \".min\" \"dist/css/*.css\" \"!dist/css/*.min.css\" \"!dist/css/*rtl*.css\"", + "css-minify-rtl": "cleancss -O1 --format breakWith=lf --with-rebase --source-map --source-map-inline-sources --output dist/css/ --batch --batch-suffix \".min\" \"dist/css/*rtl.css\" \"!dist/css/*.min.css\"", + "js": "pnpm run js-compile && pnpm run js-minify", + "js-compile": "pnpm run js-compile-standalone && pnpm run js-compile-standalone-esm && pnpm run js-compile-theme && pnpm run js-compile-theme-esm", + "js-compile-theme-esm": "rollup --environment THEME:true --environment ESM:true --config .build/rollup.config.mjs --sourcemap", + "js-compile-theme": "rollup --environment THEME:true --config .build/rollup.config.mjs --sourcemap", + "js-compile-standalone": "rollup --config .build/rollup.config.mjs --sourcemap", + "js-compile-standalone-esm": "rollup --environment ESM:true --config .build/rollup.config.mjs --sourcemap", + "js-minify": "pnpm run js-minify-standalone && pnpm run js-minify-standalone-esm && pnpm run js-minify-theme && pnpm run js-minify-theme-esm", + "js-minify-standalone": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/tabler.js.map,includeSources,url=tabler.min.js.map\" --output dist/js/tabler.min.js dist/js/tabler.js", + "js-minify-standalone-esm": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/tabler.esm.js.map,includeSources,url=tabler.esm.min.js.map\" --output dist/js/tabler.esm.min.js dist/js/tabler.esm.js", + "js-minify-theme": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/tabler-theme.js.map,includeSources,url=tabler-theme.min.js.map\" --output dist/js/tabler-theme.min.js dist/js/tabler-theme.js", + "js-minify-theme-esm": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/tabler-theme.esm.js.map,includeSources,url=tabler-theme.esm.min.js.map\" --output dist/js/tabler-theme.esm.min.js dist/js/tabler-theme.esm.js", + "copy": "pnpm run copy-img && pnpm run copy-libs", + "copy-img": "shx mkdir -p dist/img && shx cp -rf img/* dist/img", + "copy-libs": "node .build/copy-libs.mjs", + "watch": "concurrently \"pnpm run watch-css\" \"pnpm run watch-js\"", + "watch-css": "nodemon --watch scss/ --ext scss --exec \"pnpm run css-compile && pnpm run css-prefix\"", + "watch-js": "nodemon --watch js/ --ext js --exec \"pnpm run js-compile\"", + "bundlewatch": "bundlewatch", + "generate-sri": "node .build/generate-sri.js", + "format:check": "prettier --check src/**/*.{js,scss} --cache", + "format:write": "prettier --write src/**/*.{js,scss} --cache" + } +} \ No newline at end of file diff --git a/frontend/public/vendor/tabler/tabler.min.css b/frontend/public/vendor/tabler/tabler.min.css new file mode 100644 index 0000000..082cf9e --- /dev/null +++ b/frontend/public/vendor/tabler/tabler.min.css @@ -0,0 +1,9 @@ +@charset "UTF-8"; +/*! + * Tabler v1.4.0 (https://tabler.io) + * Copyright 2018-2025 The Tabler Authors + * Copyright 2018-2025 codecalm.net Paweł Kuna + * Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE) + */ +:root,[data-bs-theme=light]{--tblr-blue:#066fd1;--tblr-indigo:#4263eb;--tblr-purple:#ae3ec9;--tblr-pink:#d6336c;--tblr-red:#d63939;--tblr-orange:#f76707;--tblr-yellow:#f59f00;--tblr-green:#2fb344;--tblr-teal:#0ca678;--tblr-cyan:#17a2b8;--tblr-black:#000000;--tblr-white:#ffffff;--tblr-gray:#4b5563;--tblr-gray-dark:#1f2937;--tblr-gray-100:#f3f4f6;--tblr-gray-200:#e5e7eb;--tblr-gray-300:#d1d5db;--tblr-gray-400:#9ca3af;--tblr-gray-500:#6b7280;--tblr-gray-600:#4b5563;--tblr-gray-700:#374151;--tblr-gray-800:#1f2937;--tblr-gray-900:#111827;--tblr-primary:#066fd1;--tblr-secondary:#6b7280;--tblr-success:#2fb344;--tblr-info:#4299e1;--tblr-warning:#f59f00;--tblr-danger:#d63939;--tblr-light:#f9fafb;--tblr-dark:#1f2937;--tblr-muted:#6b7280;--tblr-blue:#066fd1;--tblr-azure:#4299e1;--tblr-indigo:#4263eb;--tblr-purple:#ae3ec9;--tblr-pink:#d6336c;--tblr-red:#d63939;--tblr-orange:#f76707;--tblr-yellow:#f59f00;--tblr-lime:#74b816;--tblr-green:#2fb344;--tblr-teal:#0ca678;--tblr-cyan:#17a2b8;--tblr-primary-rgb:6,111,209;--tblr-secondary-rgb:107,114,128;--tblr-success-rgb:47,179,68;--tblr-info-rgb:66,153,225;--tblr-warning-rgb:245,159,0;--tblr-danger-rgb:214,57,57;--tblr-light-rgb:249,250,251;--tblr-dark-rgb:31,41,55;--tblr-muted-rgb:107,114,128;--tblr-blue-rgb:6,111,209;--tblr-azure-rgb:66,153,225;--tblr-indigo-rgb:66,99,235;--tblr-purple-rgb:174,62,201;--tblr-pink-rgb:214,51,108;--tblr-red-rgb:214,57,57;--tblr-orange-rgb:247,103,7;--tblr-yellow-rgb:245,159,0;--tblr-lime-rgb:116,184,22;--tblr-green-rgb:47,179,68;--tblr-teal-rgb:12,166,120;--tblr-cyan-rgb:23,162,184;--tblr-primary-text-emphasis:rgb(2.4, 44.4, 83.6);--tblr-secondary-text-emphasis:rgb(42.8, 45.6, 51.2);--tblr-success-text-emphasis:rgb(18.8, 71.6, 27.2);--tblr-info-text-emphasis:rgb(26.4, 61.2, 90);--tblr-warning-text-emphasis:rgb(98, 63.6, 0);--tblr-danger-text-emphasis:rgb(85.6, 22.8, 22.8);--tblr-light-text-emphasis:#374151;--tblr-dark-text-emphasis:#374151;--tblr-primary-bg-subtle:rgb(205.2, 226.2, 245.8);--tblr-secondary-bg-subtle:rgb(225.4, 226.8, 229.6);--tblr-success-bg-subtle:rgb(213.4, 239.8, 217.6);--tblr-info-bg-subtle:rgb(217.2, 234.6, 249);--tblr-warning-bg-subtle:rgb(253, 235.8, 204);--tblr-danger-bg-subtle:rgb(246.8, 215.4, 215.4);--tblr-light-bg-subtle:rgb(249, 249.5, 250.5);--tblr-dark-bg-subtle:#9ca3af;--tblr-primary-border-subtle:rgb(155.4, 197.4, 236.6);--tblr-secondary-border-subtle:rgb(195.8, 198.6, 204.2);--tblr-success-border-subtle:rgb(171.8, 224.6, 180.2);--tblr-info-border-subtle:rgb(179.4, 214.2, 243);--tblr-warning-border-subtle:rgb(251, 216.6, 153);--tblr-danger-border-subtle:rgb(238.6, 175.8, 175.8);--tblr-light-border-subtle:#e5e7eb;--tblr-dark-border-subtle:#6b7280;--tblr-white-rgb:255,255,255;--tblr-black-rgb:0,0,0;--tblr-font-sans-serif:"Inter Var",Inter,-apple-system,BlinkMacSystemFont,San Francisco,Segoe UI,Roboto,Helvetica Neue,sans-serif;--tblr-font-monospace:Monaco,Consolas,Liberation Mono,Courier New,monospace;--tblr-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--tblr-body-font-family:var(--tblr-font-sans-serif);--tblr-body-font-size:0.875rem;--tblr-body-font-weight:400;--tblr-body-line-height:1.4285714286;--tblr-body-color:#1f2937;--tblr-body-color-rgb:31,41,55;--tblr-body-bg:#f9fafb;--tblr-body-bg-rgb:249,250,251;--tblr-emphasis-color:#374151;--tblr-emphasis-color-rgb:55,65,81;--tblr-secondary-color:rgba(31, 41, 55, 0.75);--tblr-secondary-color-rgb:31,41,55;--tblr-secondary-bg:#e5e7eb;--tblr-secondary-bg-rgb:229,231,235;--tblr-tertiary-color:rgba(31, 41, 55, 0.5);--tblr-tertiary-color-rgb:31,41,55;--tblr-tertiary-bg:#f3f4f6;--tblr-tertiary-bg-rgb:243,244,246;--tblr-heading-color:inherit;--tblr-link-color:#066fd1;--tblr-link-color-rgb:6,111,209;--tblr-link-decoration:none;--tblr-link-hover-color:rgb(4.8, 88.8, 167.2);--tblr-link-hover-color-rgb:5,89,167;--tblr-link-hover-decoration:underline;--tblr-code-color:light-dark(var(--tblr-gray-600), var(--tblr-gray-400));--tblr-highlight-color:#1f2937;--tblr-highlight-bg:rgb(253, 235.8, 204);--tblr-border-width:1px;--tblr-border-style:solid;--tblr-border-color:#e5e7eb;--tblr-border-color-translucent:rgba(4, 32, 69, 0.1);--tblr-border-radius:6px;--tblr-border-radius-sm:4px;--tblr-border-radius-lg:8px;--tblr-border-radius-xl:1rem;--tblr-border-radius-xxl:2rem;--tblr-border-radius-2xl:var(--tblr-border-radius-xxl);--tblr-border-radius-pill:100rem;--tblr-box-shadow:rgba(var(--tblr-body-color-rgb), 0.04) 0 2px 4px 0;--tblr-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--tblr-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--tblr-box-shadow-inset:0 0 transparent;--tblr-focus-ring-width:0.25rem;--tblr-focus-ring-opacity:0.25;--tblr-focus-ring-color:rgba(var(--tblr-primary-rgb), 0.25);--tblr-form-valid-color:#2fb344;--tblr-form-valid-border-color:#2fb344;--tblr-form-invalid-color:#d63939;--tblr-form-invalid-border-color:#d63939}[data-bs-theme=dark],body[data-bs-theme=dark] [data-bs-theme=light]{color-scheme:dark;--tblr-body-color:#e5e7eb;--tblr-body-color-rgb:229,231,235;--tblr-body-bg:#111827;--tblr-body-bg-rgb:17,24,39;--tblr-emphasis-color:#ffffff;--tblr-emphasis-color-rgb:255,255,255;--tblr-secondary-color:rgba(229, 231, 235, 0.75);--tblr-secondary-color-rgb:229,231,235;--tblr-secondary-bg:#1f2937;--tblr-secondary-bg-rgb:31,41,55;--tblr-tertiary-color:rgba(229, 231, 235, 0.5);--tblr-tertiary-color-rgb:229,231,235;--tblr-tertiary-bg:rgb(24, 32.5, 47);--tblr-tertiary-bg-rgb:24,33,47;--tblr-primary-text-emphasis:rgb(105.6, 168.6, 227.4);--tblr-secondary-text-emphasis:rgb(166.2, 170.4, 178.8);--tblr-success-text-emphasis:rgb(130.2, 209.4, 142.8);--tblr-info-text-emphasis:rgb(141.6, 193.8, 237);--tblr-warning-text-emphasis:rgb(249, 197.4, 102);--tblr-danger-text-emphasis:rgb(230.4, 136.2, 136.2);--tblr-light-text-emphasis:#f3f4f6;--tblr-dark-text-emphasis:#d1d5db;--tblr-primary-bg-subtle:rgb(1.2, 22.2, 41.8);--tblr-secondary-bg-subtle:rgb(21.4, 22.8, 25.6);--tblr-success-bg-subtle:rgb(9.4, 35.8, 13.6);--tblr-info-bg-subtle:rgb(13.2, 30.6, 45);--tblr-warning-bg-subtle:rgb(49, 31.8, 0);--tblr-danger-bg-subtle:rgb(42.8, 11.4, 11.4);--tblr-light-bg-subtle:#1f2937;--tblr-dark-bg-subtle:rgb(15.5, 20.5, 27.5);--tblr-primary-border-subtle:rgb(3.6, 66.6, 125.4);--tblr-secondary-border-subtle:rgb(64.2, 68.4, 76.8);--tblr-success-border-subtle:rgb(28.2, 107.4, 40.8);--tblr-info-border-subtle:rgb(39.6, 91.8, 135);--tblr-warning-border-subtle:rgb(147, 95.4, 0);--tblr-danger-border-subtle:rgb(128.4, 34.2, 34.2);--tblr-light-border-subtle:#374151;--tblr-dark-border-subtle:#1f2937;--tblr-heading-color:inherit;--tblr-link-color:rgb(105.6, 168.6, 227.4);--tblr-link-hover-color:rgb(135.48, 185.88, 232.92);--tblr-link-color-rgb:106,169,227;--tblr-link-hover-color-rgb:135,186,233;--tblr-code-color:var(--tblr-gray-300);--tblr-highlight-color:#e5e7eb;--tblr-highlight-bg:rgb(98, 63.6, 0);--tblr-border-color:rgb(45.7069767442, 60.4511627907, 81.0930232558);--tblr-border-color-translucent:rgba(72, 110, 149, 0.14);--tblr-form-valid-color:rgb(130.2, 209.4, 142.8);--tblr-form-valid-border-color:rgb(130.2, 209.4, 142.8);--tblr-form-invalid-color:rgb(230.4, 136.2, 136.2);--tblr-form-invalid-border-color:rgb(230.4, 136.2, 136.2)}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--tblr-body-font-family);font-size:var(--tblr-body-font-size);font-weight:var(--tblr-body-font-weight);line-height:var(--tblr-body-line-height);color:var(--tblr-body-color);text-align:var(--tblr-body-text-align);background-color:var(--tblr-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}.hr,hr{margin:2rem 0;color:inherit;border:0;border-top:var(--tblr-border-width) solid;opacity:.16}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:var(--tblr-spacer);font-weight:var(--tblr-font-weight-bold);line-height:1.2;color:var(--tblr-heading-color)}.h1,h1{font-size:1.5rem}.h2,h2{font-size:1.25rem}.h3,h3{font-size:1rem}.h4,h4{font-size:.875rem}.h5,h5{font-size:.75rem}.h6,h6{font-size:.625rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:600}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;color:var(--tblr-highlight-color);background-color:var(--tblr-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--tblr-link-color-rgb),var(--tblr-link-opacity,1));text-decoration:none}a:hover{--tblr-link-color-rgb:var(--tblr-link-hover-color-rgb);text-decoration:underline}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--tblr-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.85714285em;color:var(--tblr-light)}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.85714285em;color:var(--tblr-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.25rem .5rem;font-size:var(--tblr-font-size-h5);color:var(--tblr-text-secondary-dark);background-color:var(--tblr-code-bg);border-radius:4px}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:var(--tblr-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;line-height:inherit;font-size:1.5rem}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:.875rem;font-weight:var(--tblr-font-weight-normal)}.display-1{font-weight:300;line-height:1.2;font-size:5rem}.display-2{font-weight:300;line-height:1.2;font-size:4.5rem}.display-3{font-weight:300;line-height:1.2;font-size:4rem}.display-4{font-weight:300;line-height:1.2;font-size:3.5rem}.display-5{font-weight:300;line-height:1.2;font-size:3rem}.display-6{font-weight:300;line-height:1.2;font-size:2rem}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:.875rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#4b5563}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--tblr-body-bg);border:var(--tblr-border-width) solid var(--tblr-border-color);border-radius:var(--tblr-border-radius);box-shadow:var(--tblr-box-shadow-sm);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--tblr-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--tblr-gutter-x:calc(var(--tblr-page-padding) * 2);--tblr-gutter-y:0;width:100%;padding-right:calc(var(--tblr-gutter-x) * .5);padding-left:calc(var(--tblr-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--tblr-breakpoint-xs:0;--tblr-breakpoint-sm:576px;--tblr-breakpoint-md:768px;--tblr-breakpoint-lg:992px;--tblr-breakpoint-xl:1200px;--tblr-breakpoint-xxl:1400px}.row{--tblr-gutter-x:var(--tblr-page-padding);--tblr-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--tblr-gutter-y));margin-right:calc(-.5 * var(--tblr-gutter-x));margin-left:calc(-.5 * var(--tblr-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--tblr-gutter-x) * .5);padding-left:calc(var(--tblr-gutter-x) * .5);margin-top:var(--tblr-gutter-y)}.grid{display:grid;grid-template-rows:repeat(var(--tblr-rows,1),1fr);grid-template-columns:repeat(var(--tblr-columns,12),1fr);gap:var(--tblr-gap,var(--tblr-page-padding))}.grid .g-col-1{grid-column:auto/span 1}.grid .g-col-2{grid-column:auto/span 2}.grid .g-col-3{grid-column:auto/span 3}.grid .g-col-4{grid-column:auto/span 4}.grid .g-col-5{grid-column:auto/span 5}.grid .g-col-6{grid-column:auto/span 6}.grid .g-col-7{grid-column:auto/span 7}.grid .g-col-8{grid-column:auto/span 8}.grid .g-col-9{grid-column:auto/span 9}.grid .g-col-10{grid-column:auto/span 10}.grid .g-col-11{grid-column:auto/span 11}.grid .g-col-12{grid-column:auto/span 12}.grid .g-start-1{grid-column-start:1}.grid .g-start-2{grid-column-start:2}.grid .g-start-3{grid-column-start:3}.grid .g-start-4{grid-column-start:4}.grid .g-start-5{grid-column-start:5}.grid .g-start-6{grid-column-start:6}.grid .g-start-7{grid-column-start:7}.grid .g-start-8{grid-column-start:8}.grid .g-start-9{grid-column-start:9}.grid .g-start-10{grid-column-start:10}.grid .g-start-11{grid-column-start:11}@media (min-width:576px){.grid .g-col-sm-1{grid-column:auto/span 1}.grid .g-col-sm-2{grid-column:auto/span 2}.grid .g-col-sm-3{grid-column:auto/span 3}.grid .g-col-sm-4{grid-column:auto/span 4}.grid .g-col-sm-5{grid-column:auto/span 5}.grid .g-col-sm-6{grid-column:auto/span 6}.grid .g-col-sm-7{grid-column:auto/span 7}.grid .g-col-sm-8{grid-column:auto/span 8}.grid .g-col-sm-9{grid-column:auto/span 9}.grid .g-col-sm-10{grid-column:auto/span 10}.grid .g-col-sm-11{grid-column:auto/span 11}.grid .g-col-sm-12{grid-column:auto/span 12}.grid .g-start-sm-1{grid-column-start:1}.grid .g-start-sm-2{grid-column-start:2}.grid .g-start-sm-3{grid-column-start:3}.grid .g-start-sm-4{grid-column-start:4}.grid .g-start-sm-5{grid-column-start:5}.grid .g-start-sm-6{grid-column-start:6}.grid .g-start-sm-7{grid-column-start:7}.grid .g-start-sm-8{grid-column-start:8}.grid .g-start-sm-9{grid-column-start:9}.grid .g-start-sm-10{grid-column-start:10}.grid .g-start-sm-11{grid-column-start:11}}@media (min-width:768px){.grid .g-col-md-1{grid-column:auto/span 1}.grid .g-col-md-2{grid-column:auto/span 2}.grid .g-col-md-3{grid-column:auto/span 3}.grid .g-col-md-4{grid-column:auto/span 4}.grid .g-col-md-5{grid-column:auto/span 5}.grid .g-col-md-6{grid-column:auto/span 6}.grid .g-col-md-7{grid-column:auto/span 7}.grid .g-col-md-8{grid-column:auto/span 8}.grid .g-col-md-9{grid-column:auto/span 9}.grid .g-col-md-10{grid-column:auto/span 10}.grid .g-col-md-11{grid-column:auto/span 11}.grid .g-col-md-12{grid-column:auto/span 12}.grid .g-start-md-1{grid-column-start:1}.grid .g-start-md-2{grid-column-start:2}.grid .g-start-md-3{grid-column-start:3}.grid .g-start-md-4{grid-column-start:4}.grid .g-start-md-5{grid-column-start:5}.grid .g-start-md-6{grid-column-start:6}.grid .g-start-md-7{grid-column-start:7}.grid .g-start-md-8{grid-column-start:8}.grid .g-start-md-9{grid-column-start:9}.grid .g-start-md-10{grid-column-start:10}.grid .g-start-md-11{grid-column-start:11}}@media (min-width:992px){.grid .g-col-lg-1{grid-column:auto/span 1}.grid .g-col-lg-2{grid-column:auto/span 2}.grid .g-col-lg-3{grid-column:auto/span 3}.grid .g-col-lg-4{grid-column:auto/span 4}.grid .g-col-lg-5{grid-column:auto/span 5}.grid .g-col-lg-6{grid-column:auto/span 6}.grid .g-col-lg-7{grid-column:auto/span 7}.grid .g-col-lg-8{grid-column:auto/span 8}.grid .g-col-lg-9{grid-column:auto/span 9}.grid .g-col-lg-10{grid-column:auto/span 10}.grid .g-col-lg-11{grid-column:auto/span 11}.grid .g-col-lg-12{grid-column:auto/span 12}.grid .g-start-lg-1{grid-column-start:1}.grid .g-start-lg-2{grid-column-start:2}.grid .g-start-lg-3{grid-column-start:3}.grid .g-start-lg-4{grid-column-start:4}.grid .g-start-lg-5{grid-column-start:5}.grid .g-start-lg-6{grid-column-start:6}.grid .g-start-lg-7{grid-column-start:7}.grid .g-start-lg-8{grid-column-start:8}.grid .g-start-lg-9{grid-column-start:9}.grid .g-start-lg-10{grid-column-start:10}.grid .g-start-lg-11{grid-column-start:11}}@media (min-width:1200px){.grid .g-col-xl-1{grid-column:auto/span 1}.grid .g-col-xl-2{grid-column:auto/span 2}.grid .g-col-xl-3{grid-column:auto/span 3}.grid .g-col-xl-4{grid-column:auto/span 4}.grid .g-col-xl-5{grid-column:auto/span 5}.grid .g-col-xl-6{grid-column:auto/span 6}.grid .g-col-xl-7{grid-column:auto/span 7}.grid .g-col-xl-8{grid-column:auto/span 8}.grid .g-col-xl-9{grid-column:auto/span 9}.grid .g-col-xl-10{grid-column:auto/span 10}.grid .g-col-xl-11{grid-column:auto/span 11}.grid .g-col-xl-12{grid-column:auto/span 12}.grid .g-start-xl-1{grid-column-start:1}.grid .g-start-xl-2{grid-column-start:2}.grid .g-start-xl-3{grid-column-start:3}.grid .g-start-xl-4{grid-column-start:4}.grid .g-start-xl-5{grid-column-start:5}.grid .g-start-xl-6{grid-column-start:6}.grid .g-start-xl-7{grid-column-start:7}.grid .g-start-xl-8{grid-column-start:8}.grid .g-start-xl-9{grid-column-start:9}.grid .g-start-xl-10{grid-column-start:10}.grid .g-start-xl-11{grid-column-start:11}}@media (min-width:1400px){.grid .g-col-xxl-1{grid-column:auto/span 1}.grid .g-col-xxl-2{grid-column:auto/span 2}.grid .g-col-xxl-3{grid-column:auto/span 3}.grid .g-col-xxl-4{grid-column:auto/span 4}.grid .g-col-xxl-5{grid-column:auto/span 5}.grid .g-col-xxl-6{grid-column:auto/span 6}.grid .g-col-xxl-7{grid-column:auto/span 7}.grid .g-col-xxl-8{grid-column:auto/span 8}.grid .g-col-xxl-9{grid-column:auto/span 9}.grid .g-col-xxl-10{grid-column:auto/span 10}.grid .g-col-xxl-11{grid-column:auto/span 11}.grid .g-col-xxl-12{grid-column:auto/span 12}.grid .g-start-xxl-1{grid-column-start:1}.grid .g-start-xxl-2{grid-column-start:2}.grid .g-start-xxl-3{grid-column-start:3}.grid .g-start-xxl-4{grid-column-start:4}.grid .g-start-xxl-5{grid-column-start:5}.grid .g-start-xxl-6{grid-column-start:6}.grid .g-start-xxl-7{grid-column-start:7}.grid .g-start-xxl-8{grid-column-start:8}.grid .g-start-xxl-9{grid-column-start:9}.grid .g-start-xxl-10{grid-column-start:10}.grid .g-start-xxl-11{grid-column-start:11}}.col{flex:1 0 0}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--tblr-gutter-x:0}.g-0,.gy-0{--tblr-gutter-y:0}.g-1,.gx-1{--tblr-gutter-x:0.25rem}.g-1,.gy-1{--tblr-gutter-y:0.25rem}.g-2,.gx-2{--tblr-gutter-x:0.5rem}.g-2,.gy-2{--tblr-gutter-y:0.5rem}.g-3,.gx-3{--tblr-gutter-x:1rem}.g-3,.gy-3{--tblr-gutter-y:1rem}.g-4,.gx-4{--tblr-gutter-x:1.5rem}.g-4,.gy-4{--tblr-gutter-y:1.5rem}.g-5,.gx-5{--tblr-gutter-x:2rem}.g-5,.gy-5{--tblr-gutter-y:2rem}.g-6,.gx-6{--tblr-gutter-x:2.5rem}.g-6,.gy-6{--tblr-gutter-y:2.5rem}@media (min-width:576px){.col-sm{flex:1 0 0}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--tblr-gutter-x:0}.g-sm-0,.gy-sm-0{--tblr-gutter-y:0}.g-sm-1,.gx-sm-1{--tblr-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--tblr-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--tblr-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--tblr-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--tblr-gutter-x:1rem}.g-sm-3,.gy-sm-3{--tblr-gutter-y:1rem}.g-sm-4,.gx-sm-4{--tblr-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--tblr-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--tblr-gutter-x:2rem}.g-sm-5,.gy-sm-5{--tblr-gutter-y:2rem}.g-sm-6,.gx-sm-6{--tblr-gutter-x:2.5rem}.g-sm-6,.gy-sm-6{--tblr-gutter-y:2.5rem}}@media (min-width:768px){.col-md{flex:1 0 0}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--tblr-gutter-x:0}.g-md-0,.gy-md-0{--tblr-gutter-y:0}.g-md-1,.gx-md-1{--tblr-gutter-x:0.25rem}.g-md-1,.gy-md-1{--tblr-gutter-y:0.25rem}.g-md-2,.gx-md-2{--tblr-gutter-x:0.5rem}.g-md-2,.gy-md-2{--tblr-gutter-y:0.5rem}.g-md-3,.gx-md-3{--tblr-gutter-x:1rem}.g-md-3,.gy-md-3{--tblr-gutter-y:1rem}.g-md-4,.gx-md-4{--tblr-gutter-x:1.5rem}.g-md-4,.gy-md-4{--tblr-gutter-y:1.5rem}.g-md-5,.gx-md-5{--tblr-gutter-x:2rem}.g-md-5,.gy-md-5{--tblr-gutter-y:2rem}.g-md-6,.gx-md-6{--tblr-gutter-x:2.5rem}.g-md-6,.gy-md-6{--tblr-gutter-y:2.5rem}}@media (min-width:992px){.col-lg{flex:1 0 0}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--tblr-gutter-x:0}.g-lg-0,.gy-lg-0{--tblr-gutter-y:0}.g-lg-1,.gx-lg-1{--tblr-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--tblr-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--tblr-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--tblr-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--tblr-gutter-x:1rem}.g-lg-3,.gy-lg-3{--tblr-gutter-y:1rem}.g-lg-4,.gx-lg-4{--tblr-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--tblr-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--tblr-gutter-x:2rem}.g-lg-5,.gy-lg-5{--tblr-gutter-y:2rem}.g-lg-6,.gx-lg-6{--tblr-gutter-x:2.5rem}.g-lg-6,.gy-lg-6{--tblr-gutter-y:2.5rem}}@media (min-width:1200px){.col-xl{flex:1 0 0}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--tblr-gutter-x:0}.g-xl-0,.gy-xl-0{--tblr-gutter-y:0}.g-xl-1,.gx-xl-1{--tblr-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--tblr-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--tblr-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--tblr-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--tblr-gutter-x:1rem}.g-xl-3,.gy-xl-3{--tblr-gutter-y:1rem}.g-xl-4,.gx-xl-4{--tblr-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--tblr-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--tblr-gutter-x:2rem}.g-xl-5,.gy-xl-5{--tblr-gutter-y:2rem}.g-xl-6,.gx-xl-6{--tblr-gutter-x:2.5rem}.g-xl-6,.gy-xl-6{--tblr-gutter-y:2.5rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--tblr-gutter-x:0}.g-xxl-0,.gy-xxl-0{--tblr-gutter-y:0}.g-xxl-1,.gx-xxl-1{--tblr-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--tblr-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--tblr-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--tblr-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--tblr-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--tblr-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--tblr-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--tblr-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--tblr-gutter-x:2rem}.g-xxl-5,.gy-xxl-5{--tblr-gutter-y:2rem}.g-xxl-6,.gx-xxl-6{--tblr-gutter-x:2.5rem}.g-xxl-6,.gy-xxl-6{--tblr-gutter-y:2.5rem}}.markdown>table,.table{--tblr-table-color-type:initial;--tblr-table-bg-type:initial;--tblr-table-color-state:initial;--tblr-table-bg-state:initial;--tblr-table-color:inherit;--tblr-table-bg:transparent;--tblr-table-border-color:var(--tblr-border-color-translucent);--tblr-table-accent-bg:transparent;--tblr-table-striped-color:inherit;--tblr-table-striped-bg:var(--tblr-bg-surface-tertiary);--tblr-table-active-color:inherit;--tblr-table-active-bg:var(--tblr-active-bg);--tblr-table-hover-color:inherit;--tblr-table-hover-bg:rgba(var(--tblr-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--tblr-table-border-color)}.markdown>table>:not(caption)>*>*,.table>:not(caption)>*>*{padding:.75rem .75rem;color:var(--tblr-table-color-state,var(--tblr-table-color-type,var(--tblr-table-color)));background-color:var(--tblr-table-bg);border-bottom-width:var(--tblr-border-width);box-shadow:inset 0 0 0 9999px var(--tblr-table-bg-state,var(--tblr-table-bg-type,var(--tblr-table-accent-bg)))}.markdown>table>tbody,.table>tbody{vertical-align:inherit}.markdown>table>thead,.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--tblr-border-width) * 2) solid var(--tblr-border-color-translucent)}.caption-top{caption-side:top}.markdown>table>:not(caption)>*>*,.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.markdown>table>:not(caption)>*,.table-bordered>:not(caption)>*{border-width:var(--tblr-border-width) 0}.markdown>table>:not(caption)>*>*,.table-bordered>:not(caption)>*>*{border-width:0 var(--tblr-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(2n)>*{--tblr-table-color-type:var(--tblr-table-striped-color);--tblr-table-bg-type:var(--tblr-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--tblr-table-color-type:var(--tblr-table-striped-color);--tblr-table-bg-type:var(--tblr-table-striped-bg)}.table-active{--tblr-table-color-state:var(--tblr-table-active-color);--tblr-table-bg-state:var(--tblr-table-active-bg)}.table-hover>tbody>tr:hover>*{--tblr-table-color-state:var(--tblr-table-hover-color);--tblr-table-bg-state:var(--tblr-table-hover-bg)}.table-primary{--tblr-table-color:#1f2937;--tblr-table-bg:rgb(205.2, 226.2, 245.8);--tblr-table-border-color:rgb(170.36, 189.16, 207.64);--tblr-table-striped-bg:rgb(196.49, 216.94, 236.26);--tblr-table-striped-color:#1f2937;--tblr-table-active-bg:rgb(187.78, 207.68, 226.72);--tblr-table-active-color:#f9fafb;--tblr-table-hover-bg:rgb(192.135, 212.31, 231.49);--tblr-table-hover-color:#1f2937;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.table-secondary{--tblr-table-color:#1f2937;--tblr-table-bg:rgb(225.4, 226.8, 229.6);--tblr-table-border-color:rgb(186.52, 189.64, 194.68);--tblr-table-striped-bg:rgb(215.68, 217.51, 220.87);--tblr-table-striped-color:#1f2937;--tblr-table-active-bg:rgb(205.96, 208.22, 212.14);--tblr-table-active-color:#1f2937;--tblr-table-hover-bg:rgb(210.82, 212.865, 216.505);--tblr-table-hover-color:#1f2937;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.table-success{--tblr-table-color:#1f2937;--tblr-table-bg:rgb(213.4, 239.8, 217.6);--tblr-table-border-color:rgb(176.92, 200.04, 185.08);--tblr-table-striped-bg:rgb(204.28, 229.86, 209.47);--tblr-table-striped-color:#1f2937;--tblr-table-active-bg:rgb(195.16, 219.92, 201.34);--tblr-table-active-color:#1f2937;--tblr-table-hover-bg:rgb(199.72, 224.89, 205.405);--tblr-table-hover-color:#1f2937;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.table-info{--tblr-table-color:#1f2937;--tblr-table-bg:rgb(217.2, 234.6, 249);--tblr-table-border-color:rgb(179.96, 195.88, 210.2);--tblr-table-striped-bg:rgb(207.89, 224.92, 239.3);--tblr-table-striped-color:#1f2937;--tblr-table-active-bg:rgb(198.58, 215.24, 229.6);--tblr-table-active-color:#1f2937;--tblr-table-hover-bg:rgb(203.235, 220.08, 234.45);--tblr-table-hover-color:#1f2937;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.table-warning{--tblr-table-color:#1f2937;--tblr-table-bg:rgb(253, 235.8, 204);--tblr-table-border-color:rgb(208.6, 196.84, 174.2);--tblr-table-striped-bg:rgb(241.9, 226.06, 196.55);--tblr-table-striped-color:#1f2937;--tblr-table-active-bg:rgb(230.8, 216.32, 189.1);--tblr-table-active-color:#1f2937;--tblr-table-hover-bg:rgb(236.35, 221.19, 192.825);--tblr-table-hover-color:#1f2937;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.table-danger{--tblr-table-color:#1f2937;--tblr-table-bg:rgb(246.8, 215.4, 215.4);--tblr-table-border-color:rgb(203.64, 180.52, 183.32);--tblr-table-striped-bg:rgb(236.01, 206.68, 207.38);--tblr-table-striped-color:#1f2937;--tblr-table-active-bg:rgb(225.22, 197.96, 199.36);--tblr-table-active-color:#f9fafb;--tblr-table-hover-bg:rgb(230.615, 202.32, 203.37);--tblr-table-hover-color:#1f2937;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.table-light{--tblr-table-color:#1f2937;--tblr-table-bg:#f9fafb;--tblr-table-border-color:rgb(205.4, 208.2, 211.8);--tblr-table-striped-bg:rgb(238.1, 239.55, 241.2);--tblr-table-striped-color:#1f2937;--tblr-table-active-bg:rgb(227.2, 229.1, 231.4);--tblr-table-active-color:#1f2937;--tblr-table-hover-bg:rgb(232.65, 234.325, 236.3);--tblr-table-hover-color:#1f2937;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.table-dark{--tblr-table-color:#f9fafb;--tblr-table-bg:#1f2937;--tblr-table-border-color:rgb(74.6, 82.8, 94.2);--tblr-table-striped-bg:rgb(41.9, 51.45, 64.8);--tblr-table-striped-color:#f9fafb;--tblr-table-active-bg:rgb(52.8, 61.9, 74.6);--tblr-table-active-color:#f9fafb;--tblr-table-hover-bg:rgb(47.35, 56.675, 69.7);--tblr-table-hover-color:#f9fafb;color:var(--tblr-table-color);border-color:var(--tblr-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem;font-size:.875rem;font-weight:var(--tblr-font-weight-medium)}.col-form-label{padding-top:calc(.5625rem + var(--tblr-border-width));padding-bottom:calc(.5625rem + var(--tblr-border-width));margin-bottom:0;font-size:inherit;font-weight:var(--tblr-font-weight-medium);line-height:1.25rem}.col-form-label-lg{padding-top:calc(.6875rem + var(--tblr-border-width));padding-bottom:calc(.6875rem + var(--tblr-border-width));font-size:1rem}.col-form-label-sm{padding-top:calc(.3125rem + var(--tblr-border-width));padding-bottom:calc(.3125rem + var(--tblr-border-width));font-size:.75rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--tblr-secondary-color)}.form-control{display:block;width:100%;padding:.5625rem 1rem;font-family:var(--tblr-body-font-family);font-size:.875rem;font-weight:400;line-height:1.25rem;color:var(--tblr-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--tblr-bg-forms);background-clip:padding-box;border:var(--tblr-border-width) solid var(--tblr-border-color);border-radius:var(--tblr-border-radius);box-shadow:var(--tblr-shadow-input);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--tblr-body-color);background-color:var(--tblr-bg-forms);border-color:rgb(130.5,183,232);outline:0;box-shadow:var(--tblr-shadow-input),0 0 0 .25rem rgba(var(--tblr-primary-rgb),.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.25rem;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:var(--tblr-tertiary);opacity:1}.form-control::placeholder{color:var(--tblr-tertiary);opacity:1}.form-control:disabled{background-color:var(--tblr-bg-surface-secondary);opacity:1}.form-control::file-selector-button{padding:.5625rem 1rem;margin:-.5625rem -1rem;margin-inline-end:1rem;color:var(--tblr-body-color);background-color:var(--tblr-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--tblr-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--tblr-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.5625rem 0;margin-bottom:0;line-height:1.25rem;color:var(--tblr-body-color);background-color:transparent;border:solid transparent;border-width:var(--tblr-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.25rem + .625rem + calc(var(--tblr-border-width) * 2));padding:.3125rem .5rem;font-size:.75rem;border-radius:var(--tblr-border-radius-sm)}.form-control-sm::file-selector-button{padding:.3125rem .5rem;margin:-.3125rem -.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.25rem + 1.375rem + calc(var(--tblr-border-width) * 2));padding:.6875rem 1.5rem;font-size:1rem;border-radius:var(--tblr-border-radius-lg)}.form-control-lg::file-selector-button{padding:.6875rem 1.5rem;margin:-.6875rem -1.5rem;margin-inline-end:1.5rem}textarea.form-control{min-height:calc(1.25rem + 1.125rem + calc(var(--tblr-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.25rem + .625rem + calc(var(--tblr-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.25rem + 1.375rem + calc(var(--tblr-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.25rem + 1.125rem + calc(var(--tblr-border-width) * 2));padding:.5625rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--tblr-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--tblr-border-radius)}.form-control-color.form-control-sm{height:calc(1.25rem + .625rem + calc(var(--tblr-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.25rem + 1.375rem + calc(var(--tblr-border-width) * 2))}.form-select{--tblr-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%239ca3af' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.5625rem 3rem .5625rem 1rem;font-family:var(--tblr-body-font-family);font-size:.875rem;font-weight:400;line-height:1.25rem;color:var(--tblr-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--tblr-bg-forms);background-image:var(--tblr-form-select-bg-img),var(--tblr-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right 1rem center;background-size:16px 12px;border:var(--tblr-border-width) solid var(--tblr-border-color);border-radius:var(--tblr-border-radius);box-shadow:var(--tblr-shadow-input);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:rgb(130.5,183,232);outline:0;box-shadow:var(--tblr-shadow-input),0 0 0 .25rem rgba(var(--tblr-primary-rgb),.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:1rem;background-image:none}.form-select:disabled{background-color:var(--tblr-bg-surface-secondary)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--tblr-body-color)}.form-select-sm{padding-top:.3125rem;padding-bottom:.3125rem;padding-left:.5rem;font-size:.75rem;border-radius:var(--tblr-border-radius-sm)}.form-select-lg{padding-top:.6875rem;padding-bottom:.6875rem;padding-left:1.5rem;font-size:1rem;border-radius:var(--tblr-border-radius-lg)}[data-bs-theme=dark] .form-select,body[data-bs-theme=dark] [data-bs-theme=light] .form-select{--tblr-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23e5e7eb' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.25rem;padding-left:2rem;margin-bottom:.75rem}.form-check .form-check-input{float:left;margin-left:-2rem}.form-check-reverse{padding-right:2rem;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-2rem;margin-left:0}.form-check-input{--tblr-form-check-bg:var(--tblr-bg-forms);flex-shrink:0;width:1.25rem;height:1.25rem;margin-top:.0892857143rem;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--tblr-form-check-bg);background-image:var(--tblr-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent);-webkit-print-color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:var(--tblr-border-radius)}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:rgb(130.5,183,232);outline:0;box-shadow:0 0 0 .25rem rgba(var(--tblr-primary-rgb),.25)}.form-check-input:checked{background-color:var(--tblr-primary);border-color:var(--tblr-border-color-translucent)}.form-check-input:checked[type=checkbox]{--tblr-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8.5l2.5 2.5l5.5 -5.5'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--tblr-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3ccircle r='3' fill='%23ffffff' cx='8' cy='8' /%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:var(--tblr-primary);border-color:var(--tblr-primary);--tblr-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.7}.form-switch{padding-left:2.5rem}.form-switch .form-check-input{--tblr-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23e5e7eb'/%3e%3c/svg%3e");width:2rem;margin-left:-2.5rem;background-image:var(--tblr-form-switch-bg);background-position:left center;border-radius:2rem;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--tblr-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgb%28130.5, 183, 232%29'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--tblr-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23ffffff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5rem;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5rem;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.4}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--tblr-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.25rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #f9fafb,0 0 0 .25rem rgba(var(--tblr-primary-rgb),.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #f9fafb,0 0 0 .25rem rgba(var(--tblr-primary-rgb),.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.375rem;-webkit-appearance:none;appearance:none;background-color:var(--tblr-primary);border:2px var(--tblr-border-style) #fff;border-radius:1rem;box-shadow:0 .1rem .25rem rgba(0,0,0,.1);-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:rgb(180.3,211.8,241.2)}.form-range::-webkit-slider-runnable-track{width:100%;height:.25rem;color:transparent;cursor:pointer;background-color:var(--tblr-border-color);border-color:transparent;border-radius:1rem;box-shadow:var(--tblr-box-shadow-inset)}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:var(--tblr-primary);border:2px var(--tblr-border-style) #fff;border-radius:1rem;box-shadow:0 .1rem .25rem rgba(0,0,0,.1);-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:rgb(180.3,211.8,241.2)}.form-range::-moz-range-track{width:100%;height:.25rem;color:transparent;cursor:pointer;background-color:var(--tblr-border-color);border-color:transparent;border-radius:1rem;box-shadow:var(--tblr-box-shadow-inset)}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--tblr-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--tblr-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--tblr-border-width) * 2));min-height:calc(3.5rem + calc(var(--tblr-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;max-width:100%;height:100%;padding:1rem 1rem;overflow:hidden;color:rgba(var(--tblr-body-color-rgb),.65);text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--tblr-border-width) solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem 1rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder),.form-floating>.form-control:not(:-moz-placeholder){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem;padding-left:1rem}.form-floating>.form-control:not(:-moz-placeholder)~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>textarea:not(:-moz-placeholder)~label::after{position:absolute;inset:1rem 0.5rem;z-index:-1;height:1.5em;content:"";background-color:var(--tblr-bg-forms);border-radius:var(--tblr-border-radius)}.form-floating>textarea:focus~label::after,.form-floating>textarea:not(:placeholder-shown)~label::after{position:absolute;inset:1rem 0.5rem;z-index:-1;height:1.5em;content:"";background-color:var(--tblr-bg-forms);border-radius:var(--tblr-border-radius)}.form-floating>textarea:disabled~label::after{background-color:var(--tblr-bg-surface-secondary)}.form-floating>.form-control-plaintext~label{border-width:var(--tblr-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#4b5563}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.5625rem 1rem;font-size:.875rem;font-weight:400;line-height:1.25rem;color:var(--tblr-gray-500);text-align:center;white-space:nowrap;background-color:var(--tblr-bg-surface-secondary);border:var(--tblr-border-width) solid var(--tblr-border-color);border-radius:var(--tblr-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.6875rem 1.5rem;font-size:1rem;border-radius:var(--tblr-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.3125rem .5rem;font-size:.75rem;border-radius:var(--tblr-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:4rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(-1 * var(--tblr-border-width));border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--tblr-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:var(--tblr-spacer-1) var(--tblr-spacer-3);margin-top:.1rem;font-size:.765625rem;color:#fff;background-color:var(--tblr-success);border-radius:var(--tblr-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--tblr-form-valid-border-color);padding-right:2.375rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232fb344' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='20 6 9 17 4 12'%3e%3c/polyline%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right 1.53125rem center;background-size:1.8125rem 1.8125rem}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--tblr-form-valid-border-color);box-shadow:var(--tblr-shadow-input),0 0 0 .25rem rgba(var(--tblr-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:2.375rem;background-position:top 1.53125rem right 1.53125rem}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--tblr-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--tblr-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232fb344' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='20 6 9 17 4 12'%3e%3c/polyline%3e%3c/svg%3e");padding-right:5.5rem;background-position:right 1rem center,center right 3rem;background-size:16px 12px,1.8125rem 1.8125rem}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--tblr-form-valid-border-color);box-shadow:var(--tblr-shadow-input),0 0 0 .25rem rgba(var(--tblr-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:5.375rem}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--tblr-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--tblr-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--tblr-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--tblr-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--tblr-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:var(--tblr-spacer-1) var(--tblr-spacer-3);margin-top:.1rem;font-size:.765625rem;color:#fff;background-color:var(--tblr-danger);border-radius:var(--tblr-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--tblr-form-invalid-border-color);padding-right:2.375rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23d63939' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cline x1='18' y1='6' x2='6' y2='18'%3e%3c/line%3e%3cline x1='6' y1='6' x2='18' y2='18'%3e%3c/line%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right 1.53125rem center;background-size:1.8125rem 1.8125rem}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--tblr-form-invalid-border-color);box-shadow:var(--tblr-shadow-input),0 0 0 .25rem rgba(var(--tblr-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:2.375rem;background-position:top 1.53125rem right 1.53125rem}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--tblr-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--tblr-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23d63939' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cline x1='18' y1='6' x2='6' y2='18'%3e%3c/line%3e%3cline x1='6' y1='6' x2='18' y2='18'%3e%3c/line%3e%3c/svg%3e");padding-right:5.5rem;background-position:right 1rem center,center right 3rem;background-size:16px 12px,1.8125rem 1.8125rem}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--tblr-form-invalid-border-color);box-shadow:var(--tblr-shadow-input),0 0 0 .25rem rgba(var(--tblr-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:5.375rem}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--tblr-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--tblr-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--tblr-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--tblr-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--tblr-btn-padding-x:1rem;--tblr-btn-padding-y:0.5625rem;--tblr-btn-font-family:var(--tblr-body-font-family);--tblr-btn-font-size:0.875rem;--tblr-btn-font-weight:var(--tblr-font-weight-medium);--tblr-btn-line-height:1.25rem;--tblr-btn-color:var(--tblr-body-color);--tblr-btn-bg:transparent;--tblr-btn-border-width:var(--tblr-border-width);--tblr-btn-border-color:transparent;--tblr-btn-border-radius:var(--tblr-border-radius);--tblr-btn-hover-border-color:transparent;--tblr-btn-box-shadow:var(--tblr-shadow-input);--tblr-btn-disabled-opacity:0.4;--tblr-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--tblr-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--tblr-btn-padding-y) var(--tblr-btn-padding-x);font-family:var(--tblr-btn-font-family);font-size:var(--tblr-btn-font-size);font-weight:var(--tblr-btn-font-weight);line-height:var(--tblr-btn-line-height);color:var(--tblr-btn-color);text-align:center;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--tblr-btn-border-width) solid var(--tblr-btn-border-color);border-radius:var(--tblr-btn-border-radius);background-color:var(--tblr-btn-bg);box-shadow:var(--tblr-btn-box-shadow);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--tblr-btn-hover-color);text-decoration:none;background-color:var(--tblr-btn-hover-bg);border-color:var(--tblr-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--tblr-btn-color);background-color:var(--tblr-btn-bg);border-color:var(--tblr-btn-border-color)}.btn:focus-visible{color:var(--tblr-btn-hover-color);background-color:var(--tblr-btn-hover-bg);border-color:var(--tblr-btn-hover-border-color);outline:0;box-shadow:var(--tblr-btn-box-shadow),var(--tblr-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--tblr-btn-hover-border-color);outline:0;box-shadow:var(--tblr-btn-box-shadow),var(--tblr-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--tblr-btn-active-color);background-color:var(--tblr-btn-active-bg);border-color:var(--tblr-btn-active-border-color);box-shadow:var(--tblr-btn-active-shadow)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--tblr-btn-active-shadow),var(--tblr-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--tblr-btn-active-shadow),var(--tblr-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--tblr-btn-disabled-color);pointer-events:none;background-color:var(--tblr-btn-disabled-bg);border-color:var(--tblr-btn-disabled-border-color);opacity:var(--tblr-btn-disabled-opacity);box-shadow:none}.btn-link{--tblr-btn-font-weight:400;--tblr-btn-color:var(--tblr-link-color);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-link-hover-color);--tblr-btn-hover-border-color:transparent;--tblr-btn-active-color:var(--tblr-link-hover-color);--tblr-btn-active-border-color:transparent;--tblr-btn-disabled-color:#4b5563;--tblr-btn-disabled-border-color:transparent;--tblr-btn-box-shadow:0 0 0 #000;--tblr-btn-focus-shadow-rgb:42,132,215;text-decoration:none}.btn-link:focus-visible,.btn-link:hover{text-decoration:underline}.btn-link:focus-visible{color:var(--tblr-btn-color)}.btn-link:hover{color:var(--tblr-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--tblr-btn-padding-y:0.6875rem;--tblr-btn-padding-x:1.5rem;--tblr-btn-font-size:1rem;--tblr-btn-border-radius:var(--tblr-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--tblr-btn-padding-y:0.3125rem;--tblr-btn-padding-x:0.5rem;--tblr-btn-font-size:0.75rem;--tblr-btn-border-radius:var(--tblr-border-radius-sm)}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle:after{content:"";display:inline-block;vertical-align:.306em;width:.36em;height:.36em;border-bottom:1px var(--tblr-border-style);border-left:1px var(--tblr-border-style);margin-right:.1em;margin-left:.4em;transform:rotate(-45deg)}.dropdown-menu{--tblr-dropdown-zindex:1000;--tblr-dropdown-min-width:11rem;--tblr-dropdown-padding-x:0;--tblr-dropdown-padding-y:0.25rem;--tblr-dropdown-spacer:1px;--tblr-dropdown-font-size:0.875rem;--tblr-dropdown-color:var(--tblr-body-color);--tblr-dropdown-bg:var(--tblr-bg-surface);--tblr-dropdown-border-color:var(--tblr-border-color-translucent);--tblr-dropdown-border-radius:var(--tblr-border-radius);--tblr-dropdown-border-width:var(--tblr-border-width);--tblr-dropdown-inner-border-radius:calc(var(--tblr-border-radius) - var(--tblr-border-width));--tblr-dropdown-divider-bg:var(--tblr-border-color-translucent);--tblr-dropdown-divider-margin-y:var(--tblr-spacer-2);--tblr-dropdown-box-shadow:var(--tblr-shadow-dropdown);--tblr-dropdown-link-color:inherit;--tblr-dropdown-link-hover-color:inherit;--tblr-dropdown-link-hover-bg:rgba(var(--tblr-secondary-rgb), 0.08);--tblr-dropdown-link-active-color:var(--tblr-primary);--tblr-dropdown-link-active-bg:var(--tblr-active-bg);--tblr-dropdown-link-disabled-color:var(--tblr-tertiary-color);--tblr-dropdown-item-padding-x:0.75rem;--tblr-dropdown-item-padding-y:0.5rem;--tblr-dropdown-header-color:#4b5563;--tblr-dropdown-header-padding-x:0.75rem;--tblr-dropdown-header-padding-y:0.25rem;position:absolute;z-index:var(--tblr-dropdown-zindex);display:none;min-width:var(--tblr-dropdown-min-width);padding:var(--tblr-dropdown-padding-y) var(--tblr-dropdown-padding-x);margin:0;font-size:var(--tblr-dropdown-font-size);color:var(--tblr-dropdown-color);text-align:left;list-style:none;background-color:var(--tblr-dropdown-bg);background-clip:padding-box;border:var(--tblr-dropdown-border-width) solid var(--tblr-dropdown-border-color);border-radius:var(--tblr-dropdown-border-radius);box-shadow:var(--tblr-dropdown-box-shadow)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--tblr-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--tblr-dropdown-spacer)}.dropup .dropdown-toggle:after{content:"";display:inline-block;vertical-align:.306em;width:.36em;height:.36em;border-bottom:1px var(--tblr-border-style);border-left:1px var(--tblr-border-style);margin-right:.1em;margin-left:.4em;transform:rotate(135deg)}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--tblr-dropdown-spacer)}.dropend .dropdown-toggle:after{content:"";display:inline-block;vertical-align:.306em;width:.36em;height:.36em;border-bottom:1px var(--tblr-border-style);border-left:1px var(--tblr-border-style);margin-right:.1em;margin-left:.4em;transform:rotate(-135deg)}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--tblr-dropdown-spacer)}.dropstart .dropdown-toggle:after{content:"";display:inline-block;vertical-align:.306em;width:.36em;height:.36em;border-bottom:1px var(--tblr-border-style);border-left:1px var(--tblr-border-style);margin-right:.1em;margin-left:.4em;transform:rotate(45deg)}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--tblr-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--tblr-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--tblr-dropdown-item-padding-y) var(--tblr-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--tblr-dropdown-link-color);text-align:inherit;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--tblr-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--tblr-dropdown-link-hover-color);text-decoration:none;background-color:var(--tblr-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--tblr-dropdown-link-active-color);text-decoration:none;background-color:var(--tblr-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--tblr-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--tblr-dropdown-header-padding-y) var(--tblr-dropdown-header-padding-x);margin-bottom:0;font-size:.765625rem;color:var(--tblr-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--tblr-dropdown-item-padding-y) var(--tblr-dropdown-item-padding-x);color:var(--tblr-dropdown-link-color)}.dropdown-menu-dark{--tblr-dropdown-color:#d1d5db;--tblr-dropdown-bg:#1f2937;--tblr-dropdown-border-color:var(--tblr-border-color-translucent);--tblr-dropdown-box-shadow: ;--tblr-dropdown-link-color:#d1d5db;--tblr-dropdown-link-hover-color:#ffffff;--tblr-dropdown-divider-bg:var(--tblr-border-color-translucent);--tblr-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--tblr-dropdown-link-active-color:var(--tblr-primary);--tblr-dropdown-link-active-bg:var(--tblr-active-bg);--tblr-dropdown-link-disabled-color:#6b7280;--tblr-dropdown-header-color:#6b7280}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--tblr-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(-1 * var(--tblr-border-width))}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:1.125rem;padding-left:1.125rem}.btn-group.show .dropdown-toggle{box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.show .dropdown-toggle.btn-link{box-shadow:none}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(-1 * var(--tblr-border-width))}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:nth-child(n+3),.btn-group-vertical>:not(.btn-check)+.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--tblr-nav-link-padding-x:0.75rem;--tblr-nav-link-padding-y:0.5rem;--tblr-nav-link-font-weight: ;--tblr-nav-link-color:var(--tblr-gray-500);--tblr-nav-link-hover-color:var(--tblr-link-hover-color);--tblr-nav-link-disabled-color:var(--tblr-disabled-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--tblr-nav-link-padding-y) var(--tblr-nav-link-padding-x);font-size:var(--tblr-nav-link-font-size);font-weight:var(--tblr-nav-link-font-weight);color:var(--tblr-nav-link-color);background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--tblr-nav-link-hover-color);text-decoration:none}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(var(--tblr-primary-rgb),.25)}.nav-link.disabled,.nav-link:disabled{color:var(--tblr-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--tblr-nav-tabs-border-width:var(--tblr-border-width);--tblr-nav-tabs-border-color:var(--tblr-border-color);--tblr-nav-tabs-border-radius:var(--tblr-border-radius);--tblr-nav-tabs-link-hover-border-color:var(--tblr-border-color) var(--tblr-border-color) var(--tblr-border-color);--tblr-nav-tabs-link-active-color:var(--tblr-body-color);--tblr-nav-tabs-link-active-bg:var(--tblr-body-bg);--tblr-nav-tabs-link-active-border-color:var(--tblr-border-color) var(--tblr-border-color) var(--tblr-border-color);border-bottom:var(--tblr-nav-tabs-border-width) solid var(--tblr-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--tblr-nav-tabs-border-width));border:var(--tblr-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--tblr-nav-tabs-border-radius);border-top-right-radius:var(--tblr-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--tblr-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--tblr-nav-tabs-link-active-color);background-color:var(--tblr-nav-tabs-link-active-bg);border-color:var(--tblr-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--tblr-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--tblr-nav-pills-border-radius:var(--tblr-border-radius);--tblr-nav-pills-link-active-color:var(--tblr-primary);--tblr-nav-pills-link-active-bg:var(--tblr-active-bg)}.nav-pills .nav-link{border-radius:var(--tblr-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--tblr-nav-pills-link-active-color);background-color:var(--tblr-nav-pills-link-active-bg)}.nav-underline{--tblr-nav-underline-gap:1rem;--tblr-nav-underline-border-width:0.125rem;--tblr-nav-underline-link-active-color:var(--tblr-emphasis-color);gap:var(--tblr-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--tblr-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:600;color:var(--tblr-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-grow:1;flex-basis:0;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--tblr-navbar-padding-x:0;--tblr-navbar-padding-y:0.25rem;--tblr-navbar-color:var(--tblr-secondary);--tblr-navbar-hover-color:var(--tblr-body-color);--tblr-navbar-disabled-color:var(--tblr-disabled-color);--tblr-navbar-active-color:var(--tblr-body-color);--tblr-navbar-brand-padding-y:0.5rem;--tblr-navbar-brand-margin-end:1rem;--tblr-navbar-brand-font-size:1.25rem;--tblr-navbar-brand-color:var(--tblr-body-color);--tblr-navbar-brand-hover-color:var(--tblr-body-color);--tblr-navbar-nav-link-padding-x:0.75rem;--tblr-navbar-toggler-padding-y:0;--tblr-navbar-toggler-padding-x:0;--tblr-navbar-toggler-font-size:1rem;--tblr-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2831, 41, 55, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--tblr-navbar-toggler-border-color:rgba(var(--tblr-emphasis-color-rgb), 0.15);--tblr-navbar-toggler-border-radius:var(--tblr-border-radius);--tblr-navbar-toggler-focus-width:0;--tblr-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--tblr-navbar-padding-y) var(--tblr-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--tblr-navbar-brand-padding-y);padding-bottom:var(--tblr-navbar-brand-padding-y);margin-right:var(--tblr-navbar-brand-margin-end);font-size:var(--tblr-navbar-brand-font-size);color:var(--tblr-navbar-brand-color);white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--tblr-navbar-brand-hover-color);text-decoration:none}.navbar-nav{--tblr-nav-link-padding-x:0;--tblr-nav-link-padding-y:0.5rem;--tblr-nav-link-font-weight: ;--tblr-nav-link-color:var(--tblr-navbar-color);--tblr-nav-link-hover-color:var(--tblr-navbar-hover-color);--tblr-nav-link-disabled-color:var(--tblr-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--tblr-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--tblr-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--tblr-navbar-active-color)}.navbar-collapse{flex-grow:1;flex-basis:100%;align-items:center}.navbar-toggler{padding:var(--tblr-navbar-toggler-padding-y) var(--tblr-navbar-toggler-padding-x);font-size:var(--tblr-navbar-toggler-font-size);line-height:1;color:var(--tblr-navbar-color);background-color:transparent;border:var(--tblr-border-width) solid var(--tblr-navbar-toggler-border-color);border-radius:var(--tblr-navbar-toggler-border-radius);transition:var(--tblr-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--tblr-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--tblr-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--tblr-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--tblr-navbar-nav-link-padding-x);padding-left:var(--tblr-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--tblr-navbar-nav-link-padding-x);padding-left:var(--tblr-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--tblr-navbar-nav-link-padding-x);padding-left:var(--tblr-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--tblr-navbar-nav-link-padding-x);padding-left:var(--tblr-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--tblr-navbar-nav-link-padding-x);padding-left:var(--tblr-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--tblr-navbar-nav-link-padding-x);padding-left:var(--tblr-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;box-shadow:none;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark],body[data-bs-theme=dark] .navbar[data-bs-theme=light]{--tblr-navbar-color:rgba(255, 255, 255, 0.7);--tblr-navbar-hover-color:rgba(255, 255, 255, 0.75);--tblr-navbar-disabled-color:var(--tblr-disabled-color);--tblr-navbar-active-color:#ffffff;--tblr-navbar-brand-color:#ffffff;--tblr-navbar-brand-hover-color:#ffffff;--tblr-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--tblr-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.7%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon,body[data-bs-theme=dark] [data-bs-theme=light] .navbar-toggler-icon{--tblr-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.7%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--tblr-card-spacer-y:1rem;--tblr-card-spacer-x:1.25rem;--tblr-card-title-spacer-y:1.25rem;--tblr-card-title-color: ;--tblr-card-subtitle-color: ;--tblr-card-border-width:var(--tblr-border-width);--tblr-card-border-color:var(--tblr-border-color-translucent);--tblr-card-border-radius:var(--tblr-border-radius-lg);--tblr-card-box-shadow:var(--tblr-shadow-card);--tblr-card-inner-border-radius:calc(var(--tblr-border-radius-lg) - (var(--tblr-border-width)));--tblr-card-cap-padding-y:1rem;--tblr-card-cap-padding-x:1.25rem;--tblr-card-cap-bg:var(--tblr-bg-surface-tertiary);--tblr-card-cap-color:inherit;--tblr-card-height: ;--tblr-card-color:inherit;--tblr-card-bg:var(--tblr-bg-surface);--tblr-card-img-overlay-padding:1rem;--tblr-card-group-margin:1.5rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--tblr-card-height);color:var(--tblr-body-color);word-wrap:break-word;background-color:var(--tblr-card-bg);background-clip:border-box;border:var(--tblr-card-border-width) solid var(--tblr-card-border-color);border-radius:var(--tblr-card-border-radius);box-shadow:var(--tblr-card-box-shadow)}.card>.hr,.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--tblr-card-inner-border-radius);border-top-right-radius:var(--tblr-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--tblr-card-inner-border-radius);border-bottom-left-radius:var(--tblr-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--tblr-card-spacer-y) var(--tblr-card-spacer-x);color:var(--tblr-card-color)}.card-title{margin-bottom:var(--tblr-card-title-spacer-y);color:var(--tblr-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--tblr-card-title-spacer-y));margin-bottom:0;color:var(--tblr-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:var(--tblr-card-spacer-x)}.card-header{padding:var(--tblr-card-cap-padding-y) var(--tblr-card-cap-padding-x);margin-bottom:0;color:var(--tblr-card-cap-color);background-color:var(--tblr-card-cap-bg);border-bottom:var(--tblr-card-border-width) solid var(--tblr-card-border-color)}.card-header:first-child{border-radius:var(--tblr-card-inner-border-radius) var(--tblr-card-inner-border-radius) 0 0}.card-footer{padding:var(--tblr-card-cap-padding-y) var(--tblr-card-cap-padding-x);color:var(--tblr-card-cap-color);background-color:var(--tblr-card-cap-bg);border-top:var(--tblr-card-border-width) solid var(--tblr-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--tblr-card-inner-border-radius) var(--tblr-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--tblr-card-cap-padding-x));margin-bottom:calc(-1 * var(--tblr-card-cap-padding-y));margin-left:calc(-.5 * var(--tblr-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--tblr-card-bg);border-bottom-color:var(--tblr-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--tblr-card-cap-padding-x));margin-left:calc(-.5 * var(--tblr-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--tblr-card-img-overlay-padding);border-radius:var(--tblr-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--tblr-card-inner-border-radius);border-top-right-radius:var(--tblr-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--tblr-card-inner-border-radius);border-bottom-left-radius:var(--tblr-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--tblr-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child)>.card-header,.card-group>.card:not(:last-child)>.card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child)>.card-footer,.card-group>.card:not(:last-child)>.card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child)>.card-header,.card-group>.card:not(:first-child)>.card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child)>.card-footer,.card-group>.card:not(:first-child)>.card-img-bottom{border-bottom-left-radius:0}}.pagination{--tblr-pagination-padding-x:0.25rem;--tblr-pagination-padding-y:calc(0.25rem + 1px);--tblr-pagination-font-size:0.875rem;--tblr-pagination-color:var(--tblr-body-color);--tblr-pagination-bg:transparent;--tblr-pagination-border-width:1px;--tblr-pagination-border-color:transparent;--tblr-pagination-border-radius:var(--tblr-border-radius);--tblr-pagination-hover-color:var(--tblr-link-hover-color);--tblr-pagination-hover-bg:var(--tblr-active-bg);--tblr-pagination-hover-border-color:var(--tblr-pagination-border-color);--tblr-pagination-focus-color:var(--tblr-link-hover-color);--tblr-pagination-focus-bg:var(--tblr-secondary-bg);--tblr-pagination-focus-box-shadow:0 0 0 0.25rem rgba(var(--tblr-primary-rgb), 0.25);--tblr-pagination-active-color:#ffffff;--tblr-pagination-active-bg:var(--tblr-primary);--tblr-pagination-active-border-color:var(--tblr-primary);--tblr-pagination-disabled-color:var(--tblr-disabled-color);--tblr-pagination-disabled-bg:transparent;--tblr-pagination-disabled-border-color:var(--tblr-pagination-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--tblr-pagination-padding-y) var(--tblr-pagination-padding-x);font-size:var(--tblr-pagination-font-size);color:var(--tblr-pagination-color);background-color:var(--tblr-pagination-bg);border:var(--tblr-pagination-border-width) solid var(--tblr-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--tblr-pagination-hover-color);text-decoration:none;background-color:var(--tblr-pagination-hover-bg);border-color:var(--tblr-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--tblr-pagination-focus-color);background-color:var(--tblr-pagination-focus-bg);outline:0;box-shadow:var(--tblr-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--tblr-pagination-active-color);background-color:var(--tblr-pagination-active-bg);border-color:var(--tblr-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--tblr-pagination-disabled-color);pointer-events:none;background-color:var(--tblr-pagination-disabled-bg);border-color:var(--tblr-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(-1 * 1px)}.page-item:first-child .page-link{border-top-left-radius:var(--tblr-pagination-border-radius);border-bottom-left-radius:var(--tblr-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--tblr-pagination-border-radius);border-bottom-right-radius:var(--tblr-pagination-border-radius)}.pagination-lg{--tblr-pagination-padding-x:1.5rem;--tblr-pagination-padding-y:0.75rem;--tblr-pagination-font-size:1.09375rem;--tblr-pagination-border-radius:var(--tblr-border-radius-lg)}.pagination-sm{--tblr-pagination-padding-x:0.5rem;--tblr-pagination-padding-y:0.25rem;--tblr-pagination-font-size:0.765625rem;--tblr-pagination-border-radius:var(--tblr-border-radius-sm)}@keyframes progress-bar-stripes{0%{background-position-x:var(--tblr-progress-height)}}.progress,.progress-stacked{--tblr-progress-height:0.5rem;--tblr-progress-font-size:0.65625rem;--tblr-progress-bg:var(--tblr-border-color);--tblr-progress-border-radius:var(--tblr-border-radius);--tblr-progress-box-shadow:var(--tblr-box-shadow-inset);--tblr-progress-bar-color:#ffffff;--tblr-progress-bar-bg:var(--tblr-primary);--tblr-progress-bar-transition:width 0.6s ease;display:flex;height:var(--tblr-progress-height);overflow:hidden;font-size:var(--tblr-progress-font-size);background-color:var(--tblr-progress-bg);border-radius:var(--tblr-progress-border-radius);box-shadow:var(--tblr-progress-box-shadow)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--tblr-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--tblr-progress-bar-bg);transition:var(--tblr-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--tblr-progress-height) var(--tblr-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--tblr-list-group-color:var(--tblr-body-color);--tblr-list-group-bg:inherit;--tblr-list-group-border-color:var(--tblr-border-color);--tblr-list-group-border-width:var(--tblr-border-width);--tblr-list-group-border-radius:var(--tblr-border-radius);--tblr-list-group-item-padding-x:1.25rem;--tblr-list-group-item-padding-y:1rem;--tblr-list-group-action-color:inherit;--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:rgba(var(--tblr-secondary-rgb), 0.08);--tblr-list-group-action-active-color:var(--tblr-body-color);--tblr-list-group-action-active-bg:var(--tblr-secondary-bg);--tblr-list-group-disabled-color:var(--tblr-secondary-color);--tblr-list-group-disabled-bg:inherit;--tblr-list-group-active-color:inherit;--tblr-list-group-active-bg:var(--tblr-active-bg);--tblr-list-group-active-border-color:var(--tblr-border-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--tblr-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item{position:relative;display:block;padding:var(--tblr-list-group-item-padding-y) var(--tblr-list-group-item-padding-x);color:var(--tblr-list-group-color);background-color:var(--tblr-list-group-bg);border:var(--tblr-list-group-border-width) solid var(--tblr-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--tblr-list-group-disabled-color);pointer-events:none;background-color:var(--tblr-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--tblr-list-group-active-color);background-color:var(--tblr-list-group-active-bg);border-color:var(--tblr-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--tblr-list-group-border-width));border-top-width:var(--tblr-list-group-border-width)}.list-group-item-action{width:100%;color:var(--tblr-list-group-action-color);text-align:inherit}.list-group-item-action:not(.active):focus,.list-group-item-action:not(.active):hover{z-index:1;color:var(--tblr-list-group-action-hover-color);text-decoration:none;background-color:var(--tblr-list-group-action-hover-bg)}.list-group-item-action:not(.active):active{color:var(--tblr-list-group-action-active-color);background-color:var(--tblr-list-group-action-active-bg)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--tblr-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--tblr-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--tblr-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--tblr-list-group-border-width));border-left-width:var(--tblr-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--tblr-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--tblr-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--tblr-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--tblr-list-group-border-width));border-left-width:var(--tblr-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--tblr-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--tblr-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--tblr-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--tblr-list-group-border-width));border-left-width:var(--tblr-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--tblr-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--tblr-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--tblr-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--tblr-list-group-border-width));border-left-width:var(--tblr-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--tblr-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--tblr-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--tblr-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--tblr-list-group-border-width));border-left-width:var(--tblr-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--tblr-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--tblr-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--tblr-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--tblr-list-group-border-width));border-left-width:var(--tblr-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--tblr-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--tblr-list-group-color:var(--tblr-primary-text-emphasis);--tblr-list-group-bg:var(--tblr-primary-bg-subtle);--tblr-list-group-border-color:var(--tblr-primary-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-primary-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-primary-border-subtle);--tblr-list-group-active-color:var(--tblr-primary-bg-subtle);--tblr-list-group-active-bg:var(--tblr-primary-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-primary-text-emphasis)}.list-group-item-secondary{--tblr-list-group-color:var(--tblr-secondary-text-emphasis);--tblr-list-group-bg:var(--tblr-secondary-bg-subtle);--tblr-list-group-border-color:var(--tblr-secondary-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-secondary-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-secondary-border-subtle);--tblr-list-group-active-color:var(--tblr-secondary-bg-subtle);--tblr-list-group-active-bg:var(--tblr-secondary-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-secondary-text-emphasis)}.list-group-item-success{--tblr-list-group-color:var(--tblr-success-text-emphasis);--tblr-list-group-bg:var(--tblr-success-bg-subtle);--tblr-list-group-border-color:var(--tblr-success-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-success-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-success-border-subtle);--tblr-list-group-active-color:var(--tblr-success-bg-subtle);--tblr-list-group-active-bg:var(--tblr-success-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-success-text-emphasis)}.list-group-item-info{--tblr-list-group-color:var(--tblr-info-text-emphasis);--tblr-list-group-bg:var(--tblr-info-bg-subtle);--tblr-list-group-border-color:var(--tblr-info-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-info-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-info-border-subtle);--tblr-list-group-active-color:var(--tblr-info-bg-subtle);--tblr-list-group-active-bg:var(--tblr-info-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-info-text-emphasis)}.list-group-item-warning{--tblr-list-group-color:var(--tblr-warning-text-emphasis);--tblr-list-group-bg:var(--tblr-warning-bg-subtle);--tblr-list-group-border-color:var(--tblr-warning-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-warning-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-warning-border-subtle);--tblr-list-group-active-color:var(--tblr-warning-bg-subtle);--tblr-list-group-active-bg:var(--tblr-warning-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-warning-text-emphasis)}.list-group-item-danger{--tblr-list-group-color:var(--tblr-danger-text-emphasis);--tblr-list-group-bg:var(--tblr-danger-bg-subtle);--tblr-list-group-border-color:var(--tblr-danger-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-danger-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-danger-border-subtle);--tblr-list-group-active-color:var(--tblr-danger-bg-subtle);--tblr-list-group-active-bg:var(--tblr-danger-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-danger-text-emphasis)}.list-group-item-light{--tblr-list-group-color:var(--tblr-light-text-emphasis);--tblr-list-group-bg:var(--tblr-light-bg-subtle);--tblr-list-group-border-color:var(--tblr-light-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-light-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-light-border-subtle);--tblr-list-group-active-color:var(--tblr-light-bg-subtle);--tblr-list-group-active-bg:var(--tblr-light-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-light-text-emphasis)}.list-group-item-dark{--tblr-list-group-color:var(--tblr-dark-text-emphasis);--tblr-list-group-bg:var(--tblr-dark-bg-subtle);--tblr-list-group-border-color:var(--tblr-dark-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-dark-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-dark-border-subtle);--tblr-list-group-active-color:var(--tblr-dark-bg-subtle);--tblr-list-group-active-bg:var(--tblr-dark-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-dark-text-emphasis)}.list-group-item-muted{--tblr-list-group-color:var(--tblr-muted-text-emphasis);--tblr-list-group-bg:var(--tblr-muted-bg-subtle);--tblr-list-group-border-color:var(--tblr-muted-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-muted-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-muted-border-subtle);--tblr-list-group-active-color:var(--tblr-muted-bg-subtle);--tblr-list-group-active-bg:var(--tblr-muted-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-muted-text-emphasis)}.list-group-item-blue{--tblr-list-group-color:var(--tblr-blue-text-emphasis);--tblr-list-group-bg:var(--tblr-blue-bg-subtle);--tblr-list-group-border-color:var(--tblr-blue-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-blue-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-blue-border-subtle);--tblr-list-group-active-color:var(--tblr-blue-bg-subtle);--tblr-list-group-active-bg:var(--tblr-blue-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-blue-text-emphasis)}.list-group-item-azure{--tblr-list-group-color:var(--tblr-azure-text-emphasis);--tblr-list-group-bg:var(--tblr-azure-bg-subtle);--tblr-list-group-border-color:var(--tblr-azure-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-azure-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-azure-border-subtle);--tblr-list-group-active-color:var(--tblr-azure-bg-subtle);--tblr-list-group-active-bg:var(--tblr-azure-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-azure-text-emphasis)}.list-group-item-indigo{--tblr-list-group-color:var(--tblr-indigo-text-emphasis);--tblr-list-group-bg:var(--tblr-indigo-bg-subtle);--tblr-list-group-border-color:var(--tblr-indigo-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-indigo-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-indigo-border-subtle);--tblr-list-group-active-color:var(--tblr-indigo-bg-subtle);--tblr-list-group-active-bg:var(--tblr-indigo-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-indigo-text-emphasis)}.list-group-item-purple{--tblr-list-group-color:var(--tblr-purple-text-emphasis);--tblr-list-group-bg:var(--tblr-purple-bg-subtle);--tblr-list-group-border-color:var(--tblr-purple-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-purple-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-purple-border-subtle);--tblr-list-group-active-color:var(--tblr-purple-bg-subtle);--tblr-list-group-active-bg:var(--tblr-purple-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-purple-text-emphasis)}.list-group-item-pink{--tblr-list-group-color:var(--tblr-pink-text-emphasis);--tblr-list-group-bg:var(--tblr-pink-bg-subtle);--tblr-list-group-border-color:var(--tblr-pink-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-pink-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-pink-border-subtle);--tblr-list-group-active-color:var(--tblr-pink-bg-subtle);--tblr-list-group-active-bg:var(--tblr-pink-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-pink-text-emphasis)}.list-group-item-red{--tblr-list-group-color:var(--tblr-red-text-emphasis);--tblr-list-group-bg:var(--tblr-red-bg-subtle);--tblr-list-group-border-color:var(--tblr-red-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-red-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-red-border-subtle);--tblr-list-group-active-color:var(--tblr-red-bg-subtle);--tblr-list-group-active-bg:var(--tblr-red-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-red-text-emphasis)}.list-group-item-orange{--tblr-list-group-color:var(--tblr-orange-text-emphasis);--tblr-list-group-bg:var(--tblr-orange-bg-subtle);--tblr-list-group-border-color:var(--tblr-orange-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-orange-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-orange-border-subtle);--tblr-list-group-active-color:var(--tblr-orange-bg-subtle);--tblr-list-group-active-bg:var(--tblr-orange-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-orange-text-emphasis)}.list-group-item-yellow{--tblr-list-group-color:var(--tblr-yellow-text-emphasis);--tblr-list-group-bg:var(--tblr-yellow-bg-subtle);--tblr-list-group-border-color:var(--tblr-yellow-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-yellow-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-yellow-border-subtle);--tblr-list-group-active-color:var(--tblr-yellow-bg-subtle);--tblr-list-group-active-bg:var(--tblr-yellow-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-yellow-text-emphasis)}.list-group-item-lime{--tblr-list-group-color:var(--tblr-lime-text-emphasis);--tblr-list-group-bg:var(--tblr-lime-bg-subtle);--tblr-list-group-border-color:var(--tblr-lime-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-lime-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-lime-border-subtle);--tblr-list-group-active-color:var(--tblr-lime-bg-subtle);--tblr-list-group-active-bg:var(--tblr-lime-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-lime-text-emphasis)}.list-group-item-green{--tblr-list-group-color:var(--tblr-green-text-emphasis);--tblr-list-group-bg:var(--tblr-green-bg-subtle);--tblr-list-group-border-color:var(--tblr-green-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-green-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-green-border-subtle);--tblr-list-group-active-color:var(--tblr-green-bg-subtle);--tblr-list-group-active-bg:var(--tblr-green-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-green-text-emphasis)}.list-group-item-teal{--tblr-list-group-color:var(--tblr-teal-text-emphasis);--tblr-list-group-bg:var(--tblr-teal-bg-subtle);--tblr-list-group-border-color:var(--tblr-teal-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-teal-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-teal-border-subtle);--tblr-list-group-active-color:var(--tblr-teal-bg-subtle);--tblr-list-group-active-bg:var(--tblr-teal-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-teal-text-emphasis)}.list-group-item-cyan{--tblr-list-group-color:var(--tblr-cyan-text-emphasis);--tblr-list-group-bg:var(--tblr-cyan-bg-subtle);--tblr-list-group-border-color:var(--tblr-cyan-border-subtle);--tblr-list-group-action-hover-color:var(--tblr-emphasis-color);--tblr-list-group-action-hover-bg:var(--tblr-cyan-border-subtle);--tblr-list-group-action-active-color:var(--tblr-emphasis-color);--tblr-list-group-action-active-bg:var(--tblr-cyan-border-subtle);--tblr-list-group-active-color:var(--tblr-cyan-bg-subtle);--tblr-list-group-active-bg:var(--tblr-cyan-text-emphasis);--tblr-list-group-active-border-color:var(--tblr-cyan-text-emphasis)}.toast{--tblr-toast-zindex:1090;--tblr-toast-padding-x:0.75rem;--tblr-toast-padding-y:0.5rem;--tblr-toast-spacing:calc(var(--tblr-page-padding) * 2);--tblr-toast-max-width:350px;--tblr-toast-font-size:0.875rem;--tblr-toast-color: ;--tblr-toast-bg:var(--tblr-bg-surface);--tblr-toast-border-width:var(--tblr-border-width);--tblr-toast-border-color:var(--tblr-border-color);--tblr-toast-border-radius:var(--tblr-border-radius);--tblr-toast-box-shadow:var(--tblr-box-shadow);--tblr-toast-header-color:var(--tblr-gray-500);--tblr-toast-header-bg:rgba(var(--tblr-body-bg-rgb), 0.85);--tblr-toast-header-border-color:var(--tblr-border-color);width:var(--tblr-toast-max-width);max-width:100%;font-size:var(--tblr-toast-font-size);color:var(--tblr-toast-color);pointer-events:auto;background-color:var(--tblr-toast-bg);background-clip:padding-box;border:var(--tblr-toast-border-width) solid var(--tblr-toast-border-color);box-shadow:var(--tblr-toast-box-shadow);border-radius:var(--tblr-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--tblr-toast-zindex:1090;position:absolute;z-index:var(--tblr-toast-zindex);width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--tblr-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--tblr-toast-padding-y) var(--tblr-toast-padding-x);color:var(--tblr-toast-header-color);background-color:var(--tblr-toast-header-bg);background-clip:padding-box;border-bottom:var(--tblr-toast-border-width) solid var(--tblr-toast-header-border-color);border-top-left-radius:calc(var(--tblr-toast-border-radius) - var(--tblr-toast-border-width));border-top-right-radius:calc(var(--tblr-toast-border-radius) - var(--tblr-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--tblr-toast-padding-x));margin-left:var(--tblr-toast-padding-x)}.toast-body{padding:var(--tblr-toast-padding-x);word-wrap:break-word}.modal{--tblr-modal-zindex:1055;--tblr-modal-width:540px;--tblr-modal-padding:1.5rem;--tblr-modal-margin:0.5rem;--tblr-modal-color:var(--tblr-body-color);--tblr-modal-bg:var(--tblr-bg-surface);--tblr-modal-border-color:transparent;--tblr-modal-border-width:var(--tblr-border-width);--tblr-modal-border-radius:var(--tblr-border-radius-lg);--tblr-modal-box-shadow:var(--tblr-box-shadow-sm);--tblr-modal-inner-border-radius:calc(var(--tblr-modal-border-radius) - 1px);--tblr-modal-header-padding-x:1.5rem;--tblr-modal-header-padding-y:1.5rem;--tblr-modal-header-padding:1.5rem;--tblr-modal-header-border-color:var(--tblr-border-color);--tblr-modal-header-border-width:var(--tblr-border-width);--tblr-modal-title-line-height:1.4285714286;--tblr-modal-footer-gap:0.75rem;--tblr-modal-footer-bg:var(--tblr-bg-surface-tertiary);--tblr-modal-footer-border-color:var(--tblr-border-color);--tblr-modal-footer-border-width:var(--tblr-border-width);position:fixed;top:0;left:0;z-index:var(--tblr-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--tblr-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transform:translate(0,-1rem);transition:transform .3s ease-out}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--tblr-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--tblr-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--tblr-modal-color);pointer-events:auto;background-color:var(--tblr-modal-bg);background-clip:padding-box;border:var(--tblr-modal-border-width) solid var(--tblr-modal-border-color);border-radius:var(--tblr-modal-border-radius);box-shadow:var(--tblr-modal-box-shadow);outline:0}.modal-backdrop{--tblr-backdrop-zindex:1050;--tblr-backdrop-bg:var(--tblr-gray-800);--tblr-backdrop-opacity:0.24;position:fixed;top:0;left:0;z-index:var(--tblr-backdrop-zindex);width:100vw;height:100vh;background-color:var(--tblr-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--tblr-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--tblr-modal-header-padding);border-bottom:var(--tblr-modal-header-border-width) solid var(--tblr-modal-header-border-color);border-top-left-radius:var(--tblr-modal-inner-border-radius);border-top-right-radius:var(--tblr-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--tblr-modal-header-padding-y) * .5) calc(var(--tblr-modal-header-padding-x) * .5);margin-top:calc(-.5 * var(--tblr-modal-header-padding-y));margin-right:calc(-.5 * var(--tblr-modal-header-padding-x));margin-bottom:calc(-.5 * var(--tblr-modal-header-padding-y));margin-left:auto}.modal-title{margin-bottom:0;line-height:var(--tblr-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--tblr-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--tblr-modal-padding) - var(--tblr-modal-footer-gap) * .5);background-color:var(--tblr-modal-footer-bg);border-top:var(--tblr-modal-footer-border-width) solid var(--tblr-modal-footer-border-color);border-bottom-right-radius:var(--tblr-modal-inner-border-radius);border-bottom-left-radius:var(--tblr-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--tblr-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--tblr-modal-margin:1.75rem;--tblr-modal-box-shadow:var(--tblr-box-shadow)}.modal-dialog{max-width:var(--tblr-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--tblr-modal-width:380px}}@media (min-width:992px){.modal-lg,.modal-xl{--tblr-modal-width:720px}}@media (min-width:1200px){.modal-xl{--tblr-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--tblr-tooltip-zindex:1080;--tblr-tooltip-max-width:200px;--tblr-tooltip-padding-x:var(--tblr-spacer-3);--tblr-tooltip-padding-y:var(--tblr-spacer-1);--tblr-tooltip-margin: ;--tblr-tooltip-font-size:0.765625rem;--tblr-tooltip-color:var(--tblr-text-inverted);--tblr-tooltip-bg:var(--tblr-bg-surface-inverted);--tblr-tooltip-border-radius:var(--tblr-border-radius);--tblr-tooltip-opacity:0.9;--tblr-tooltip-arrow-width:0.8rem;--tblr-tooltip-arrow-height:0.4rem;z-index:var(--tblr-tooltip-zindex);display:block;margin:var(--tblr-tooltip-margin);font-family:var(--tblr-font-sans-serif);font-style:normal;font-weight:400;line-height:1.4285714286;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--tblr-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--tblr-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--tblr-tooltip-arrow-width);height:var(--tblr-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--tblr-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--tblr-tooltip-arrow-height) calc(var(--tblr-tooltip-arrow-width) * .5) 0;border-top-color:var(--tblr-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--tblr-tooltip-arrow-height));width:var(--tblr-tooltip-arrow-height);height:var(--tblr-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--tblr-tooltip-arrow-width) * .5) var(--tblr-tooltip-arrow-height) calc(var(--tblr-tooltip-arrow-width) * .5) 0;border-right-color:var(--tblr-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--tblr-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--tblr-tooltip-arrow-width) * .5) var(--tblr-tooltip-arrow-height);border-bottom-color:var(--tblr-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--tblr-tooltip-arrow-height));width:var(--tblr-tooltip-arrow-height);height:var(--tblr-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--tblr-tooltip-arrow-width) * .5) 0 calc(var(--tblr-tooltip-arrow-width) * .5) var(--tblr-tooltip-arrow-height);border-left-color:var(--tblr-tooltip-bg)}.tooltip-inner{max-width:var(--tblr-tooltip-max-width);padding:var(--tblr-tooltip-padding-y) var(--tblr-tooltip-padding-x);color:var(--tblr-tooltip-color);text-align:center;background-color:var(--tblr-tooltip-bg);border-radius:var(--tblr-tooltip-border-radius)}.popover{--tblr-popover-zindex:1070;--tblr-popover-max-width:276px;--tblr-popover-font-size:0.765625rem;--tblr-popover-bg:var(--tblr-bg-surface);--tblr-popover-border-width:var(--tblr-border-width);--tblr-popover-border-color:var(--tblr-border-color);--tblr-popover-border-radius:var(--tblr-border-radius-lg);--tblr-popover-inner-border-radius:calc(var(--tblr-border-radius-lg) - var(--tblr-border-width));--tblr-popover-box-shadow:var(--tblr-shadow-lg);--tblr-popover-header-padding-x:1rem;--tblr-popover-header-padding-y:0.5rem;--tblr-popover-header-font-size:0.875rem;--tblr-popover-header-color:inherit;--tblr-popover-header-bg:transparent;--tblr-popover-body-padding-x:0.5rem;--tblr-popover-body-padding-y:0.5rem;--tblr-popover-body-color:inherit;--tblr-popover-arrow-width:1rem;--tblr-popover-arrow-height:0.5rem;--tblr-popover-arrow-border:var(--tblr-popover-border-color);z-index:var(--tblr-popover-zindex);display:block;max-width:var(--tblr-popover-max-width);font-family:var(--tblr-font-sans-serif);font-style:normal;font-weight:400;line-height:1.4285714286;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--tblr-popover-font-size);word-wrap:break-word;background-color:var(--tblr-popover-bg);background-clip:padding-box;border:var(--tblr-popover-border-width) solid var(--tblr-popover-border-color);border-radius:var(--tblr-popover-border-radius);box-shadow:var(--tblr-popover-box-shadow)}.popover .popover-arrow{display:block;width:var(--tblr-popover-arrow-width);height:var(--tblr-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--tblr-popover-arrow-height)) - var(--tblr-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--tblr-popover-arrow-height) calc(var(--tblr-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--tblr-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--tblr-popover-border-width);border-top-color:var(--tblr-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--tblr-popover-arrow-height)) - var(--tblr-popover-border-width));width:var(--tblr-popover-arrow-height);height:var(--tblr-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--tblr-popover-arrow-width) * .5) var(--tblr-popover-arrow-height) calc(var(--tblr-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--tblr-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--tblr-popover-border-width);border-right-color:var(--tblr-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--tblr-popover-arrow-height)) - var(--tblr-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--tblr-popover-arrow-width) * .5) var(--tblr-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--tblr-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--tblr-popover-border-width);border-bottom-color:var(--tblr-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--tblr-popover-arrow-width);margin-left:calc(-.5 * var(--tblr-popover-arrow-width));content:"";border-bottom:var(--tblr-popover-border-width) solid var(--tblr-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--tblr-popover-arrow-height)) - var(--tblr-popover-border-width));width:var(--tblr-popover-arrow-height);height:var(--tblr-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--tblr-popover-arrow-width) * .5) 0 calc(var(--tblr-popover-arrow-width) * .5) var(--tblr-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--tblr-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--tblr-popover-border-width);border-left-color:var(--tblr-popover-bg)}.popover-header{padding:var(--tblr-popover-header-padding-y) var(--tblr-popover-header-padding-x);margin-bottom:0;font-size:var(--tblr-popover-header-font-size);color:var(--tblr-popover-header-color);background-color:var(--tblr-popover-header-bg);border-bottom:var(--tblr-popover-border-width) solid var(--tblr-popover-border-color);border-top-left-radius:var(--tblr-popover-inner-border-radius);border-top-right-radius:var(--tblr-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--tblr-popover-body-padding-y) var(--tblr-popover-body-padding-x);color:var(--tblr-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;filter:var(--tblr-carousel-control-icon-filter);border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:1.5rem;height:1.5rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='15 18 9 12 15 6'%3e%3c/polyline%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='9 18 15 12 9 6'%3e%3c/polyline%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:var(--tblr-carousel-indicator-active-bg);background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:var(--tblr-carousel-caption-color);text-align:center}.carousel-dark{--tblr-carousel-indicator-active-bg:#000000;--tblr-carousel-caption-color:#000000;--tblr-carousel-control-icon-filter:invert(1) grayscale(100)}:root,[data-bs-theme=light]{--tblr-carousel-indicator-active-bg:#ffffff;--tblr-carousel-caption-color:#ffffff;--tblr-carousel-control-icon-filter: }[data-bs-theme=dark],body[data-bs-theme=dark] [data-bs-theme=light]{--tblr-carousel-indicator-active-bg:#000000;--tblr-carousel-caption-color:#000000;--tblr-carousel-control-icon-filter:invert(1) grayscale(100)}.spinner-border,.spinner-grow{display:inline-block;width:var(--tblr-spinner-width);height:var(--tblr-spinner-height);vertical-align:var(--tblr-spinner-vertical-align);border-radius:50%;animation:var(--tblr-spinner-animation-speed) linear infinite var(--tblr-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--tblr-spinner-width:1.5rem;--tblr-spinner-height:1.5rem;--tblr-spinner-vertical-align:-0.125em;--tblr-spinner-border-width:2px;--tblr-spinner-animation-speed:0.75s;--tblr-spinner-animation-name:spinner-border;border:var(--tblr-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--tblr-spinner-width:1rem;--tblr-spinner-height:1rem;--tblr-spinner-border-width:1px}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--tblr-spinner-width:1.5rem;--tblr-spinner-height:1.5rem;--tblr-spinner-vertical-align:-0.125em;--tblr-spinner-animation-speed:0.75s;--tblr-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--tblr-spinner-width:1rem;--tblr-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--tblr-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--tblr-offcanvas-zindex:1045;--tblr-offcanvas-width:400px;--tblr-offcanvas-height:30vh;--tblr-offcanvas-padding-x:1.5rem;--tblr-offcanvas-padding-y:1.5rem;--tblr-offcanvas-color:var(--tblr-body-color);--tblr-offcanvas-bg:var(--tblr-bg-surface);--tblr-offcanvas-border-width:var(--tblr-border-width);--tblr-offcanvas-border-color:var(--tblr-border-color);--tblr-offcanvas-box-shadow:var(--tblr-box-shadow-sm);--tblr-offcanvas-transition:transform 0.3s ease-in-out;--tblr-offcanvas-title-line-height:1.4285714286}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--tblr-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--tblr-offcanvas-color);visibility:hidden;background-color:var(--tblr-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--tblr-offcanvas-box-shadow);transition:var(--tblr-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--tblr-offcanvas-width);border-right:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--tblr-offcanvas-width);border-left:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-bottom:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-top:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--tblr-offcanvas-height:auto;--tblr-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--tblr-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--tblr-offcanvas-color);visibility:hidden;background-color:var(--tblr-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--tblr-offcanvas-box-shadow);transition:var(--tblr-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--tblr-offcanvas-width);border-right:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--tblr-offcanvas-width);border-left:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-bottom:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-top:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--tblr-offcanvas-height:auto;--tblr-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--tblr-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--tblr-offcanvas-color);visibility:hidden;background-color:var(--tblr-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--tblr-offcanvas-box-shadow);transition:var(--tblr-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--tblr-offcanvas-width);border-right:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--tblr-offcanvas-width);border-left:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-bottom:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-top:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--tblr-offcanvas-height:auto;--tblr-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--tblr-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--tblr-offcanvas-color);visibility:hidden;background-color:var(--tblr-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--tblr-offcanvas-box-shadow);transition:var(--tblr-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--tblr-offcanvas-width);border-right:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--tblr-offcanvas-width);border-left:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-bottom:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-top:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--tblr-offcanvas-height:auto;--tblr-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--tblr-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--tblr-offcanvas-color);visibility:hidden;background-color:var(--tblr-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--tblr-offcanvas-box-shadow);transition:var(--tblr-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--tblr-offcanvas-width);border-right:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--tblr-offcanvas-width);border-left:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-bottom:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-top:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--tblr-offcanvas-height:auto;--tblr-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--tblr-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--tblr-offcanvas-color);visibility:hidden;background-color:var(--tblr-offcanvas-bg);background-clip:padding-box;outline:0;box-shadow:var(--tblr-offcanvas-box-shadow);transition:var(--tblr-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--tblr-offcanvas-width);border-right:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--tblr-offcanvas-width);border-left:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-bottom:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--tblr-offcanvas-height);max-height:100%;border-top:var(--tblr-offcanvas-border-width) solid var(--tblr-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:var(--tblr-gray-800)}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.24}.offcanvas-header{display:flex;align-items:center;padding:var(--tblr-offcanvas-padding-y) var(--tblr-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--tblr-offcanvas-padding-y) * .5) calc(var(--tblr-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--tblr-offcanvas-padding-y));margin-right:calc(-.5 * var(--tblr-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--tblr-offcanvas-padding-y));margin-left:auto}.offcanvas-title{margin-bottom:0;line-height:var(--tblr-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--tblr-offcanvas-padding-y) var(--tblr-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.2}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.1}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000000 55%,rgba(0,0,0,0.9) 75%,#000000 95%);mask-image:linear-gradient(130deg,#000000 55%,rgba(0,0,0,0.9) 75%,#000000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:var(--tblr-box-shadow)!important}.shadow-sm{box-shadow:var(--tblr-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--tblr-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--tblr-focus-ring-color:rgba(var(--tblr-primary-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-secondary{--tblr-focus-ring-color:rgba(var(--tblr-secondary-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-success{--tblr-focus-ring-color:rgba(var(--tblr-success-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-info{--tblr-focus-ring-color:rgba(var(--tblr-info-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-warning{--tblr-focus-ring-color:rgba(var(--tblr-warning-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-danger{--tblr-focus-ring-color:rgba(var(--tblr-danger-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-light{--tblr-focus-ring-color:rgba(var(--tblr-light-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-dark{--tblr-focus-ring-color:rgba(var(--tblr-dark-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-muted{--tblr-focus-ring-color:rgba(var(--tblr-muted-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-blue{--tblr-focus-ring-color:rgba(var(--tblr-blue-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-azure{--tblr-focus-ring-color:rgba(var(--tblr-azure-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-indigo{--tblr-focus-ring-color:rgba(var(--tblr-indigo-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-purple{--tblr-focus-ring-color:rgba(var(--tblr-purple-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-pink{--tblr-focus-ring-color:rgba(var(--tblr-pink-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-red{--tblr-focus-ring-color:rgba(var(--tblr-red-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-orange{--tblr-focus-ring-color:rgba(var(--tblr-orange-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-yellow{--tblr-focus-ring-color:rgba(var(--tblr-yellow-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-lime{--tblr-focus-ring-color:rgba(var(--tblr-lime-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-green{--tblr-focus-ring-color:rgba(var(--tblr-green-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-teal{--tblr-focus-ring-color:rgba(var(--tblr-teal-rgb), var(--tblr-focus-ring-opacity))}.focus-ring-cyan{--tblr-focus-ring-color:rgba(var(--tblr-cyan-rgb), var(--tblr-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.border-wide{border:2px var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.border-0{border:0!important}.border-top{border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.border-top-wide{border-top:2px var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.border-end-wide{border-right:2px var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.border-bottom-wide{border-bottom:2px var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.border-start-wide{border-left:2px var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.border-start-0{border-left:0!important}.border-red{--tblr-border-opacity:1;border-color:rgba(var(--tblr-red-rgb),var(--tblr-border-opacity))!important}.border-green{--tblr-border-opacity:1;border-color:rgba(var(--tblr-green-rgb),var(--tblr-border-opacity))!important}.border-primary-subtle{border-color:var(--tblr-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--tblr-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--tblr-success-border-subtle)!important}.border-info-subtle{border-color:var(--tblr-info-border-subtle)!important}.border-warning-subtle{border-color:var(--tblr-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--tblr-danger-border-subtle)!important}.border-light-subtle{border-color:var(--tblr-light-border-subtle)!important}.border-dark-subtle{border-color:var(--tblr-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--tblr-border-opacity:0.1}.border-opacity-25{--tblr-border-opacity:0.25}.border-opacity-50{--tblr-border-opacity:0.5}.border-opacity-75{--tblr-border-opacity:0.75}.border-opacity-100{--tblr-border-opacity:1}.w-0{width:0!important}.w-1{width:.25rem!important}.w-2{width:.5rem!important}.w-3{width:1rem!important}.w-4{width:1.5rem!important}.w-5{width:2rem!important}.w-6{width:2.5rem!important}.w-25{width:25%!important}.w-33{width:33.33333%!important}.w-50{width:50%!important}.w-66{width:66.66666%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-0{height:0!important}.h-1{height:.25rem!important}.h-2{height:.5rem!important}.h-3{height:1rem!important}.h-4{height:1.5rem!important}.h-5{height:2rem!important}.h-6{height:2.5rem!important}.h-25{height:25%!important}.h-33{height:33.33333%!important}.h-50{height:50%!important}.h-66{height:66.66666%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:2rem!important}.m-6{margin:2.5rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:2rem!important;margin-left:2rem!important}.mx-6{margin-right:2.5rem!important;margin-left:2.5rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:2rem!important;margin-bottom:2rem!important}.my-6{margin-top:2.5rem!important;margin-bottom:2.5rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:2rem!important}.mt-6{margin-top:2.5rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:2rem!important}.me-6{margin-right:2.5rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:2rem!important}.mb-6{margin-bottom:2.5rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:2rem!important}.ms-6{margin-left:2.5rem!important}.ms-auto{margin-left:auto!important}.m-n1{margin:-.25rem!important}.m-n2{margin:-.5rem!important}.m-n3{margin:-1rem!important}.m-n4{margin:-1.5rem!important}.m-n5{margin:-2rem!important}.m-n6{margin:-2.5rem!important}.mx-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-n5{margin-right:-2rem!important;margin-left:-2rem!important}.mx-n6{margin-right:-2.5rem!important;margin-left:-2.5rem!important}.my-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-n5{margin-top:-2rem!important;margin-bottom:-2rem!important}.my-n6{margin-top:-2.5rem!important;margin-bottom:-2.5rem!important}.mt-n1{margin-top:-.25rem!important}.mt-n2{margin-top:-.5rem!important}.mt-n3{margin-top:-1rem!important}.mt-n4{margin-top:-1.5rem!important}.mt-n5{margin-top:-2rem!important}.mt-n6{margin-top:-2.5rem!important}.me-n1{margin-right:-.25rem!important}.me-n2{margin-right:-.5rem!important}.me-n3{margin-right:-1rem!important}.me-n4{margin-right:-1.5rem!important}.me-n5{margin-right:-2rem!important}.me-n6{margin-right:-2.5rem!important}.mb-n1{margin-bottom:-.25rem!important}.mb-n2{margin-bottom:-.5rem!important}.mb-n3{margin-bottom:-1rem!important}.mb-n4{margin-bottom:-1.5rem!important}.mb-n5{margin-bottom:-2rem!important}.mb-n6{margin-bottom:-2.5rem!important}.ms-n1{margin-left:-.25rem!important}.ms-n2{margin-left:-.5rem!important}.ms-n3{margin-left:-1rem!important}.ms-n4{margin-left:-1.5rem!important}.ms-n5{margin-left:-2rem!important}.ms-n6{margin-left:-2.5rem!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:2rem!important}.p-6{padding:2.5rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:2rem!important;padding-left:2rem!important}.px-6{padding-right:2.5rem!important;padding-left:2.5rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:2rem!important;padding-bottom:2rem!important}.py-6{padding-top:2.5rem!important;padding-bottom:2.5rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:2rem!important}.pt-6{padding-top:2.5rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:2rem!important}.pe-6{padding-right:2.5rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:2rem!important}.pb-6{padding-bottom:2.5rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:2rem!important}.ps-6{padding-left:2.5rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:2rem!important}.gap-6{gap:2.5rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:2rem!important}.row-gap-6{row-gap:2.5rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-6{-moz-column-gap:2.5rem!important;column-gap:2.5rem!important}.font-monospace{font-family:var(--tblr-font-monospace)!important}.fs-1{font-size:1.5rem!important}.fs-2{font-size:1.25rem!important}.fs-3{font-size:1rem!important}.fs-4{font-size:.875rem!important}.fs-5{font-size:.75rem!important}.fs-6{font-size:.625rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:600!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.1428571429!important}.lh-base{line-height:1.4285714286!important}.lh-lg{line-height:1.7142857143!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--tblr-text-opacity:1;color:rgba(var(--tblr-primary-rgb),var(--tblr-text-opacity))!important}.text-secondary{--tblr-text-opacity:1;color:rgba(var(--tblr-secondary-rgb),var(--tblr-text-opacity))!important}.text-success{--tblr-text-opacity:1;color:rgba(var(--tblr-success-rgb),var(--tblr-text-opacity))!important}.text-info{--tblr-text-opacity:1;color:rgba(var(--tblr-info-rgb),var(--tblr-text-opacity))!important}.text-warning{--tblr-text-opacity:1;color:rgba(var(--tblr-warning-rgb),var(--tblr-text-opacity))!important}.text-danger{--tblr-text-opacity:1;color:rgba(var(--tblr-danger-rgb),var(--tblr-text-opacity))!important}.text-light{--tblr-text-opacity:1;color:rgba(var(--tblr-light-rgb),var(--tblr-text-opacity))!important}.text-dark{--tblr-text-opacity:1;color:rgba(var(--tblr-dark-rgb),var(--tblr-text-opacity))!important}.text-muted{--tblr-text-opacity:1;color:var(--tblr-secondary-color)!important}.text-blue{--tblr-text-opacity:1;color:rgba(var(--tblr-blue-rgb),var(--tblr-text-opacity))!important}.text-azure{--tblr-text-opacity:1;color:rgba(var(--tblr-azure-rgb),var(--tblr-text-opacity))!important}.text-indigo{--tblr-text-opacity:1;color:rgba(var(--tblr-indigo-rgb),var(--tblr-text-opacity))!important}.text-purple{--tblr-text-opacity:1;color:rgba(var(--tblr-purple-rgb),var(--tblr-text-opacity))!important}.text-pink{--tblr-text-opacity:1;color:rgba(var(--tblr-pink-rgb),var(--tblr-text-opacity))!important}.text-red{--tblr-text-opacity:1;color:rgba(var(--tblr-red-rgb),var(--tblr-text-opacity))!important}.text-orange{--tblr-text-opacity:1;color:rgba(var(--tblr-orange-rgb),var(--tblr-text-opacity))!important}.text-yellow{--tblr-text-opacity:1;color:rgba(var(--tblr-yellow-rgb),var(--tblr-text-opacity))!important}.text-lime{--tblr-text-opacity:1;color:rgba(var(--tblr-lime-rgb),var(--tblr-text-opacity))!important}.text-green{--tblr-text-opacity:1;color:rgba(var(--tblr-green-rgb),var(--tblr-text-opacity))!important}.text-teal{--tblr-text-opacity:1;color:rgba(var(--tblr-teal-rgb),var(--tblr-text-opacity))!important}.text-cyan{--tblr-text-opacity:1;color:rgba(var(--tblr-cyan-rgb),var(--tblr-text-opacity))!important}.text-black{--tblr-text-opacity:1;color:rgba(var(--tblr-black-rgb),var(--tblr-text-opacity))!important}.text-white{--tblr-text-opacity:1;color:rgba(var(--tblr-white-rgb),var(--tblr-text-opacity))!important}.text-body{--tblr-text-opacity:1;color:rgba(var(--tblr-body-color-rgb),var(--tblr-text-opacity))!important}.text-black-50{--tblr-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--tblr-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--tblr-text-opacity:1;color:var(--tblr-secondary-color)!important}.text-body-tertiary{--tblr-text-opacity:1;color:var(--tblr-tertiary-color)!important}.text-body-emphasis{--tblr-text-opacity:1;color:var(--tblr-emphasis-color)!important}.text-reset{--tblr-text-opacity:1;color:inherit!important}.text-opacity-25{--tblr-text-opacity:0.25}.text-opacity-50{--tblr-text-opacity:0.5}.text-opacity-75{--tblr-text-opacity:0.75}.text-opacity-100{--tblr-text-opacity:1}.text-primary-emphasis{color:var(--tblr-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--tblr-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--tblr-success-text-emphasis)!important}.text-info-emphasis{color:var(--tblr-info-text-emphasis)!important}.text-warning-emphasis{color:var(--tblr-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--tblr-danger-text-emphasis)!important}.text-light-emphasis{color:var(--tblr-light-text-emphasis)!important}.text-dark-emphasis{color:var(--tblr-dark-text-emphasis)!important}.link-opacity-10{--tblr-link-opacity:0.1}.link-opacity-10-hover:hover{--tblr-link-opacity:0.1}.link-opacity-25{--tblr-link-opacity:0.25}.link-opacity-25-hover:hover{--tblr-link-opacity:0.25}.link-opacity-50{--tblr-link-opacity:0.5}.link-opacity-50-hover:hover{--tblr-link-opacity:0.5}.link-opacity-75{--tblr-link-opacity:0.75}.link-opacity-75-hover:hover{--tblr-link-opacity:0.75}.link-opacity-100{--tblr-link-opacity:1}.link-opacity-100-hover:hover{--tblr-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-primary-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-secondary{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-secondary-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-success{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-success-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-info{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-info-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-warning{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-warning-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-danger{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-danger-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-light{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-light-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-dark{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-dark-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-muted{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-muted-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-blue{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-blue-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-azure{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-azure-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-indigo{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-indigo-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-purple{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-purple-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-pink{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-pink-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-red{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-red-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-orange{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-orange-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-yellow{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-yellow-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-lime{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-lime-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-green{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-green-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-teal{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-teal-rgb),var(--tblr-link-underline-opacity))!important}.link-underline-cyan{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-cyan-rgb),var(--tblr-link-underline-opacity))!important}.link-underline{--tblr-link-underline-opacity:1;text-decoration-color:rgba(var(--tblr-link-color-rgb),var(--tblr-link-underline-opacity,1))!important}.link-underline-opacity-0{--tblr-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--tblr-link-underline-opacity:0}.link-underline-opacity-10{--tblr-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--tblr-link-underline-opacity:0.1}.link-underline-opacity-25{--tblr-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--tblr-link-underline-opacity:0.25}.link-underline-opacity-50{--tblr-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--tblr-link-underline-opacity:0.5}.link-underline-opacity-75{--tblr-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--tblr-link-underline-opacity:0.75}.link-underline-opacity-100{--tblr-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--tblr-link-underline-opacity:1}.bg-primary{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-primary-rgb),var(--tblr-bg-opacity))!important}.bg-secondary{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-secondary-rgb),var(--tblr-bg-opacity))!important}.bg-success{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-success-rgb),var(--tblr-bg-opacity))!important}.bg-info{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-info-rgb),var(--tblr-bg-opacity))!important}.bg-warning{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-warning-rgb),var(--tblr-bg-opacity))!important}.bg-danger{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-danger-rgb),var(--tblr-bg-opacity))!important}.bg-light{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-light-rgb),var(--tblr-bg-opacity))!important}.bg-dark{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-dark-rgb),var(--tblr-bg-opacity))!important}.bg-muted{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-muted-rgb),var(--tblr-bg-opacity))!important}.bg-blue{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-blue-rgb),var(--tblr-bg-opacity))!important}.bg-azure{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-azure-rgb),var(--tblr-bg-opacity))!important}.bg-indigo{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-indigo-rgb),var(--tblr-bg-opacity))!important}.bg-purple{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-purple-rgb),var(--tblr-bg-opacity))!important}.bg-pink{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-pink-rgb),var(--tblr-bg-opacity))!important}.bg-red{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-red-rgb),var(--tblr-bg-opacity))!important}.bg-orange{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-orange-rgb),var(--tblr-bg-opacity))!important}.bg-yellow{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-yellow-rgb),var(--tblr-bg-opacity))!important}.bg-lime{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-lime-rgb),var(--tblr-bg-opacity))!important}.bg-green{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-green-rgb),var(--tblr-bg-opacity))!important}.bg-teal{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-teal-rgb),var(--tblr-bg-opacity))!important}.bg-cyan{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-cyan-rgb),var(--tblr-bg-opacity))!important}.bg-black{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-black-rgb),var(--tblr-bg-opacity))!important}.bg-white{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-white-rgb),var(--tblr-bg-opacity))!important}.bg-body{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-body-bg-rgb),var(--tblr-bg-opacity))!important}.bg-transparent{--tblr-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-secondary-bg-rgb),var(--tblr-bg-opacity))!important}.bg-body-tertiary{--tblr-bg-opacity:1;background-color:rgba(var(--tblr-tertiary-bg-rgb),var(--tblr-bg-opacity))!important}.bg-opacity-10{--tblr-bg-opacity:0.1}.bg-opacity-25{--tblr-bg-opacity:0.25}.bg-opacity-50{--tblr-bg-opacity:0.5}.bg-opacity-75{--tblr-bg-opacity:0.75}.bg-opacity-100{--tblr-bg-opacity:1}.bg-primary-subtle{background-color:var(--tblr-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--tblr-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--tblr-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--tblr-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--tblr-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--tblr-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--tblr-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--tblr-dark-bg-subtle)!important}.bg-gradient{background-image:var(--tblr-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--tblr-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--tblr-border-radius-sm)!important}.rounded-2{border-radius:var(--tblr-border-radius)!important}.rounded-3{border-radius:var(--tblr-border-radius-lg)!important}.rounded-4{border-radius:var(--tblr-border-radius-xl)!important}.rounded-5{border-radius:var(--tblr-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--tblr-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--tblr-border-radius)!important;border-top-right-radius:var(--tblr-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--tblr-border-radius-sm)!important;border-top-right-radius:var(--tblr-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--tblr-border-radius)!important;border-top-right-radius:var(--tblr-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--tblr-border-radius-lg)!important;border-top-right-radius:var(--tblr-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--tblr-border-radius-xl)!important;border-top-right-radius:var(--tblr-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--tblr-border-radius-xxl)!important;border-top-right-radius:var(--tblr-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--tblr-border-radius-pill)!important;border-top-right-radius:var(--tblr-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--tblr-border-radius)!important;border-bottom-right-radius:var(--tblr-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--tblr-border-radius-sm)!important;border-bottom-right-radius:var(--tblr-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--tblr-border-radius)!important;border-bottom-right-radius:var(--tblr-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--tblr-border-radius-lg)!important;border-bottom-right-radius:var(--tblr-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--tblr-border-radius-xl)!important;border-bottom-right-radius:var(--tblr-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--tblr-border-radius-xxl)!important;border-bottom-right-radius:var(--tblr-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--tblr-border-radius-pill)!important;border-bottom-right-radius:var(--tblr-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--tblr-border-radius)!important;border-bottom-left-radius:var(--tblr-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--tblr-border-radius-sm)!important;border-bottom-left-radius:var(--tblr-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--tblr-border-radius)!important;border-bottom-left-radius:var(--tblr-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--tblr-border-radius-lg)!important;border-bottom-left-radius:var(--tblr-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--tblr-border-radius-xl)!important;border-bottom-left-radius:var(--tblr-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--tblr-border-radius-xxl)!important;border-bottom-left-radius:var(--tblr-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--tblr-border-radius-pill)!important;border-bottom-left-radius:var(--tblr-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--tblr-border-radius)!important;border-top-left-radius:var(--tblr-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--tblr-border-radius-sm)!important;border-top-left-radius:var(--tblr-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--tblr-border-radius)!important;border-top-left-radius:var(--tblr-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--tblr-border-radius-lg)!important;border-top-left-radius:var(--tblr-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--tblr-border-radius-xl)!important;border-top-left-radius:var(--tblr-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--tblr-border-radius-xxl)!important;border-top-left-radius:var(--tblr-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--tblr-border-radius-pill)!important;border-top-left-radius:var(--tblr-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}.object-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-scale-down{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-none{-o-object-fit:none!important;object-fit:none!important}.cursor-auto{cursor:auto!important}.cursor-pointer{cursor:pointer!important}.cursor-move{cursor:move!important}.cursor-not-allowed{cursor:not-allowed!important}.cursor-zoom-in{cursor:zoom-in!important}.cursor-zoom-out{cursor:zoom-out!important}.cursor-default{cursor:default!important}.cursor-none{cursor:none!important}.cursor-help{cursor:help!important}.cursor-progress{cursor:progress!important}.cursor-wait{cursor:wait!important}.cursor-text{cursor:text!important}.cursor-v-text{cursor:vertical-text!important}.cursor-grab{cursor:grab!important}.cursor-grabbing{cursor:grabbing!important}.cursor-crosshair{cursor:crosshair!important}.border-x{border-left:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important;border-right:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.border-x-wide{border-left:2px var(--tblr-border-style) var(--tblr-border-color-translucent)!important;border-right:2px var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.border-x-0{border-left:0!important;border-right:0!important}.border-y{border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important;border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.border-y-wide{border-top:2px var(--tblr-border-style) var(--tblr-border-color-translucent)!important;border-bottom:2px var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.border-y-0{border-top:0!important;border-bottom:0!important}.columns-2{-moz-columns:2!important;columns:2!important}.columns-3{-moz-columns:3!important;columns:3!important}.columns-4{-moz-columns:4!important;columns:4!important}.bg-pattern-transparent{background:url('data:image/svg+xml;charset=UTF-8,') repeat center/16px 16px!important}.bg-gradient{background:linear-gradient(var(--tblr-gradient-direction,to right),var(--tblr-gradient-stops,var(--tblr-gradient-from,transparent),var(--tblr-gradient-to,transparent))) no-repeat!important}.bg-gradient-to-t{--tblr-gradient-direction:to top!important}.bg-gradient-to-te{--tblr-gradient-direction:to top right!important}.bg-gradient-to-e{--tblr-gradient-direction:to right!important}.bg-gradient-to-be{--tblr-gradient-direction:to bottom right!important}.bg-gradient-to-b{--tblr-gradient-direction:to bottom!important}.bg-gradient-to-bs{--tblr-gradient-direction:to bottom left!important}.bg-gradient-to-s{--tblr-gradient-direction:to left!important}.bg-gradient-to-ts{--tblr-gradient-direction:to top left!important}.table-auto{table-layout:auto!important}.table-fixed{table-layout:fixed!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:2rem!important}.m-sm-6{margin:2.5rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:2rem!important;margin-left:2rem!important}.mx-sm-6{margin-right:2.5rem!important;margin-left:2.5rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:2rem!important;margin-bottom:2rem!important}.my-sm-6{margin-top:2.5rem!important;margin-bottom:2.5rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:2rem!important}.mt-sm-6{margin-top:2.5rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:2rem!important}.me-sm-6{margin-right:2.5rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:2rem!important}.mb-sm-6{margin-bottom:2.5rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:2rem!important}.ms-sm-6{margin-left:2.5rem!important}.ms-sm-auto{margin-left:auto!important}.m-sm-n1{margin:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.m-sm-n3{margin:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.m-sm-n5{margin:-2rem!important}.m-sm-n6{margin:-2.5rem!important}.mx-sm-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-sm-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-sm-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-sm-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-sm-n5{margin-right:-2rem!important;margin-left:-2rem!important}.mx-sm-n6{margin-right:-2.5rem!important;margin-left:-2.5rem!important}.my-sm-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-sm-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-sm-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-sm-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-sm-n5{margin-top:-2rem!important;margin-bottom:-2rem!important}.my-sm-n6{margin-top:-2.5rem!important;margin-bottom:-2.5rem!important}.mt-sm-n1{margin-top:-.25rem!important}.mt-sm-n2{margin-top:-.5rem!important}.mt-sm-n3{margin-top:-1rem!important}.mt-sm-n4{margin-top:-1.5rem!important}.mt-sm-n5{margin-top:-2rem!important}.mt-sm-n6{margin-top:-2.5rem!important}.me-sm-n1{margin-right:-.25rem!important}.me-sm-n2{margin-right:-.5rem!important}.me-sm-n3{margin-right:-1rem!important}.me-sm-n4{margin-right:-1.5rem!important}.me-sm-n5{margin-right:-2rem!important}.me-sm-n6{margin-right:-2.5rem!important}.mb-sm-n1{margin-bottom:-.25rem!important}.mb-sm-n2{margin-bottom:-.5rem!important}.mb-sm-n3{margin-bottom:-1rem!important}.mb-sm-n4{margin-bottom:-1.5rem!important}.mb-sm-n5{margin-bottom:-2rem!important}.mb-sm-n6{margin-bottom:-2.5rem!important}.ms-sm-n1{margin-left:-.25rem!important}.ms-sm-n2{margin-left:-.5rem!important}.ms-sm-n3{margin-left:-1rem!important}.ms-sm-n4{margin-left:-1.5rem!important}.ms-sm-n5{margin-left:-2rem!important}.ms-sm-n6{margin-left:-2.5rem!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:2rem!important}.p-sm-6{padding:2.5rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:2rem!important;padding-left:2rem!important}.px-sm-6{padding-right:2.5rem!important;padding-left:2.5rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:2rem!important;padding-bottom:2rem!important}.py-sm-6{padding-top:2.5rem!important;padding-bottom:2.5rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:2rem!important}.pt-sm-6{padding-top:2.5rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:2rem!important}.pe-sm-6{padding-right:2.5rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:2rem!important}.pb-sm-6{padding-bottom:2.5rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:2rem!important}.ps-sm-6{padding-left:2.5rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:2rem!important}.gap-sm-6{gap:2.5rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:2rem!important}.row-gap-sm-6{row-gap:2.5rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-sm-6{-moz-column-gap:2.5rem!important;column-gap:2.5rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}.columns-sm-2{-moz-columns:2!important;columns:2!important}.columns-sm-3{-moz-columns:3!important;columns:3!important}.columns-sm-4{-moz-columns:4!important;columns:4!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:2rem!important}.m-md-6{margin:2.5rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:2rem!important;margin-left:2rem!important}.mx-md-6{margin-right:2.5rem!important;margin-left:2.5rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:2rem!important;margin-bottom:2rem!important}.my-md-6{margin-top:2.5rem!important;margin-bottom:2.5rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:2rem!important}.mt-md-6{margin-top:2.5rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:2rem!important}.me-md-6{margin-right:2.5rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:2rem!important}.mb-md-6{margin-bottom:2.5rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:2rem!important}.ms-md-6{margin-left:2.5rem!important}.ms-md-auto{margin-left:auto!important}.m-md-n1{margin:-.25rem!important}.m-md-n2{margin:-.5rem!important}.m-md-n3{margin:-1rem!important}.m-md-n4{margin:-1.5rem!important}.m-md-n5{margin:-2rem!important}.m-md-n6{margin:-2.5rem!important}.mx-md-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-md-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-md-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-md-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-md-n5{margin-right:-2rem!important;margin-left:-2rem!important}.mx-md-n6{margin-right:-2.5rem!important;margin-left:-2.5rem!important}.my-md-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-md-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-md-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-md-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-md-n5{margin-top:-2rem!important;margin-bottom:-2rem!important}.my-md-n6{margin-top:-2.5rem!important;margin-bottom:-2.5rem!important}.mt-md-n1{margin-top:-.25rem!important}.mt-md-n2{margin-top:-.5rem!important}.mt-md-n3{margin-top:-1rem!important}.mt-md-n4{margin-top:-1.5rem!important}.mt-md-n5{margin-top:-2rem!important}.mt-md-n6{margin-top:-2.5rem!important}.me-md-n1{margin-right:-.25rem!important}.me-md-n2{margin-right:-.5rem!important}.me-md-n3{margin-right:-1rem!important}.me-md-n4{margin-right:-1.5rem!important}.me-md-n5{margin-right:-2rem!important}.me-md-n6{margin-right:-2.5rem!important}.mb-md-n1{margin-bottom:-.25rem!important}.mb-md-n2{margin-bottom:-.5rem!important}.mb-md-n3{margin-bottom:-1rem!important}.mb-md-n4{margin-bottom:-1.5rem!important}.mb-md-n5{margin-bottom:-2rem!important}.mb-md-n6{margin-bottom:-2.5rem!important}.ms-md-n1{margin-left:-.25rem!important}.ms-md-n2{margin-left:-.5rem!important}.ms-md-n3{margin-left:-1rem!important}.ms-md-n4{margin-left:-1.5rem!important}.ms-md-n5{margin-left:-2rem!important}.ms-md-n6{margin-left:-2.5rem!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:2rem!important}.p-md-6{padding:2.5rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:2rem!important;padding-left:2rem!important}.px-md-6{padding-right:2.5rem!important;padding-left:2.5rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:2rem!important;padding-bottom:2rem!important}.py-md-6{padding-top:2.5rem!important;padding-bottom:2.5rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:2rem!important}.pt-md-6{padding-top:2.5rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:2rem!important}.pe-md-6{padding-right:2.5rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:2rem!important}.pb-md-6{padding-bottom:2.5rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:2rem!important}.ps-md-6{padding-left:2.5rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:2rem!important}.gap-md-6{gap:2.5rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:2rem!important}.row-gap-md-6{row-gap:2.5rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-md-6{-moz-column-gap:2.5rem!important;column-gap:2.5rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}.columns-md-2{-moz-columns:2!important;columns:2!important}.columns-md-3{-moz-columns:3!important;columns:3!important}.columns-md-4{-moz-columns:4!important;columns:4!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:2rem!important}.m-lg-6{margin:2.5rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:2rem!important;margin-left:2rem!important}.mx-lg-6{margin-right:2.5rem!important;margin-left:2.5rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:2rem!important;margin-bottom:2rem!important}.my-lg-6{margin-top:2.5rem!important;margin-bottom:2.5rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:2rem!important}.mt-lg-6{margin-top:2.5rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:2rem!important}.me-lg-6{margin-right:2.5rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:2rem!important}.mb-lg-6{margin-bottom:2.5rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:2rem!important}.ms-lg-6{margin-left:2.5rem!important}.ms-lg-auto{margin-left:auto!important}.m-lg-n1{margin:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.m-lg-n3{margin:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.m-lg-n5{margin:-2rem!important}.m-lg-n6{margin:-2.5rem!important}.mx-lg-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-lg-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-lg-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-lg-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-lg-n5{margin-right:-2rem!important;margin-left:-2rem!important}.mx-lg-n6{margin-right:-2.5rem!important;margin-left:-2.5rem!important}.my-lg-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-lg-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-lg-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-lg-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-lg-n5{margin-top:-2rem!important;margin-bottom:-2rem!important}.my-lg-n6{margin-top:-2.5rem!important;margin-bottom:-2.5rem!important}.mt-lg-n1{margin-top:-.25rem!important}.mt-lg-n2{margin-top:-.5rem!important}.mt-lg-n3{margin-top:-1rem!important}.mt-lg-n4{margin-top:-1.5rem!important}.mt-lg-n5{margin-top:-2rem!important}.mt-lg-n6{margin-top:-2.5rem!important}.me-lg-n1{margin-right:-.25rem!important}.me-lg-n2{margin-right:-.5rem!important}.me-lg-n3{margin-right:-1rem!important}.me-lg-n4{margin-right:-1.5rem!important}.me-lg-n5{margin-right:-2rem!important}.me-lg-n6{margin-right:-2.5rem!important}.mb-lg-n1{margin-bottom:-.25rem!important}.mb-lg-n2{margin-bottom:-.5rem!important}.mb-lg-n3{margin-bottom:-1rem!important}.mb-lg-n4{margin-bottom:-1.5rem!important}.mb-lg-n5{margin-bottom:-2rem!important}.mb-lg-n6{margin-bottom:-2.5rem!important}.ms-lg-n1{margin-left:-.25rem!important}.ms-lg-n2{margin-left:-.5rem!important}.ms-lg-n3{margin-left:-1rem!important}.ms-lg-n4{margin-left:-1.5rem!important}.ms-lg-n5{margin-left:-2rem!important}.ms-lg-n6{margin-left:-2.5rem!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:2rem!important}.p-lg-6{padding:2.5rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:2rem!important;padding-left:2rem!important}.px-lg-6{padding-right:2.5rem!important;padding-left:2.5rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:2rem!important;padding-bottom:2rem!important}.py-lg-6{padding-top:2.5rem!important;padding-bottom:2.5rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:2rem!important}.pt-lg-6{padding-top:2.5rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:2rem!important}.pe-lg-6{padding-right:2.5rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:2rem!important}.pb-lg-6{padding-bottom:2.5rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:2rem!important}.ps-lg-6{padding-left:2.5rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:2rem!important}.gap-lg-6{gap:2.5rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:2rem!important}.row-gap-lg-6{row-gap:2.5rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-lg-6{-moz-column-gap:2.5rem!important;column-gap:2.5rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}.columns-lg-2{-moz-columns:2!important;columns:2!important}.columns-lg-3{-moz-columns:3!important;columns:3!important}.columns-lg-4{-moz-columns:4!important;columns:4!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:2rem!important}.m-xl-6{margin:2.5rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:2rem!important;margin-left:2rem!important}.mx-xl-6{margin-right:2.5rem!important;margin-left:2.5rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:2rem!important;margin-bottom:2rem!important}.my-xl-6{margin-top:2.5rem!important;margin-bottom:2.5rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:2rem!important}.mt-xl-6{margin-top:2.5rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:2rem!important}.me-xl-6{margin-right:2.5rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:2rem!important}.mb-xl-6{margin-bottom:2.5rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:2rem!important}.ms-xl-6{margin-left:2.5rem!important}.ms-xl-auto{margin-left:auto!important}.m-xl-n1{margin:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.m-xl-n3{margin:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.m-xl-n5{margin:-2rem!important}.m-xl-n6{margin:-2.5rem!important}.mx-xl-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-xl-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-xl-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-xl-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-xl-n5{margin-right:-2rem!important;margin-left:-2rem!important}.mx-xl-n6{margin-right:-2.5rem!important;margin-left:-2.5rem!important}.my-xl-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-xl-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-xl-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-xl-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-xl-n5{margin-top:-2rem!important;margin-bottom:-2rem!important}.my-xl-n6{margin-top:-2.5rem!important;margin-bottom:-2.5rem!important}.mt-xl-n1{margin-top:-.25rem!important}.mt-xl-n2{margin-top:-.5rem!important}.mt-xl-n3{margin-top:-1rem!important}.mt-xl-n4{margin-top:-1.5rem!important}.mt-xl-n5{margin-top:-2rem!important}.mt-xl-n6{margin-top:-2.5rem!important}.me-xl-n1{margin-right:-.25rem!important}.me-xl-n2{margin-right:-.5rem!important}.me-xl-n3{margin-right:-1rem!important}.me-xl-n4{margin-right:-1.5rem!important}.me-xl-n5{margin-right:-2rem!important}.me-xl-n6{margin-right:-2.5rem!important}.mb-xl-n1{margin-bottom:-.25rem!important}.mb-xl-n2{margin-bottom:-.5rem!important}.mb-xl-n3{margin-bottom:-1rem!important}.mb-xl-n4{margin-bottom:-1.5rem!important}.mb-xl-n5{margin-bottom:-2rem!important}.mb-xl-n6{margin-bottom:-2.5rem!important}.ms-xl-n1{margin-left:-.25rem!important}.ms-xl-n2{margin-left:-.5rem!important}.ms-xl-n3{margin-left:-1rem!important}.ms-xl-n4{margin-left:-1.5rem!important}.ms-xl-n5{margin-left:-2rem!important}.ms-xl-n6{margin-left:-2.5rem!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:2rem!important}.p-xl-6{padding:2.5rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:2rem!important;padding-left:2rem!important}.px-xl-6{padding-right:2.5rem!important;padding-left:2.5rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:2rem!important;padding-bottom:2rem!important}.py-xl-6{padding-top:2.5rem!important;padding-bottom:2.5rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:2rem!important}.pt-xl-6{padding-top:2.5rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:2rem!important}.pe-xl-6{padding-right:2.5rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:2rem!important}.pb-xl-6{padding-bottom:2.5rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:2rem!important}.ps-xl-6{padding-left:2.5rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:2rem!important}.gap-xl-6{gap:2.5rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:2rem!important}.row-gap-xl-6{row-gap:2.5rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-xl-6{-moz-column-gap:2.5rem!important;column-gap:2.5rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}.columns-xl-2{-moz-columns:2!important;columns:2!important}.columns-xl-3{-moz-columns:3!important;columns:3!important}.columns-xl-4{-moz-columns:4!important;columns:4!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:2rem!important}.m-xxl-6{margin:2.5rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:2rem!important;margin-left:2rem!important}.mx-xxl-6{margin-right:2.5rem!important;margin-left:2.5rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:2rem!important;margin-bottom:2rem!important}.my-xxl-6{margin-top:2.5rem!important;margin-bottom:2.5rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:2rem!important}.mt-xxl-6{margin-top:2.5rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:2rem!important}.me-xxl-6{margin-right:2.5rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:2rem!important}.mb-xxl-6{margin-bottom:2.5rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:2rem!important}.ms-xxl-6{margin-left:2.5rem!important}.ms-xxl-auto{margin-left:auto!important}.m-xxl-n1{margin:-.25rem!important}.m-xxl-n2{margin:-.5rem!important}.m-xxl-n3{margin:-1rem!important}.m-xxl-n4{margin:-1.5rem!important}.m-xxl-n5{margin:-2rem!important}.m-xxl-n6{margin:-2.5rem!important}.mx-xxl-n1{margin-right:-.25rem!important;margin-left:-.25rem!important}.mx-xxl-n2{margin-right:-.5rem!important;margin-left:-.5rem!important}.mx-xxl-n3{margin-right:-1rem!important;margin-left:-1rem!important}.mx-xxl-n4{margin-right:-1.5rem!important;margin-left:-1.5rem!important}.mx-xxl-n5{margin-right:-2rem!important;margin-left:-2rem!important}.mx-xxl-n6{margin-right:-2.5rem!important;margin-left:-2.5rem!important}.my-xxl-n1{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.my-xxl-n2{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.my-xxl-n3{margin-top:-1rem!important;margin-bottom:-1rem!important}.my-xxl-n4{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.my-xxl-n5{margin-top:-2rem!important;margin-bottom:-2rem!important}.my-xxl-n6{margin-top:-2.5rem!important;margin-bottom:-2.5rem!important}.mt-xxl-n1{margin-top:-.25rem!important}.mt-xxl-n2{margin-top:-.5rem!important}.mt-xxl-n3{margin-top:-1rem!important}.mt-xxl-n4{margin-top:-1.5rem!important}.mt-xxl-n5{margin-top:-2rem!important}.mt-xxl-n6{margin-top:-2.5rem!important}.me-xxl-n1{margin-right:-.25rem!important}.me-xxl-n2{margin-right:-.5rem!important}.me-xxl-n3{margin-right:-1rem!important}.me-xxl-n4{margin-right:-1.5rem!important}.me-xxl-n5{margin-right:-2rem!important}.me-xxl-n6{margin-right:-2.5rem!important}.mb-xxl-n1{margin-bottom:-.25rem!important}.mb-xxl-n2{margin-bottom:-.5rem!important}.mb-xxl-n3{margin-bottom:-1rem!important}.mb-xxl-n4{margin-bottom:-1.5rem!important}.mb-xxl-n5{margin-bottom:-2rem!important}.mb-xxl-n6{margin-bottom:-2.5rem!important}.ms-xxl-n1{margin-left:-.25rem!important}.ms-xxl-n2{margin-left:-.5rem!important}.ms-xxl-n3{margin-left:-1rem!important}.ms-xxl-n4{margin-left:-1.5rem!important}.ms-xxl-n5{margin-left:-2rem!important}.ms-xxl-n6{margin-left:-2.5rem!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:2rem!important}.p-xxl-6{padding:2.5rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:2rem!important;padding-left:2rem!important}.px-xxl-6{padding-right:2.5rem!important;padding-left:2.5rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:2rem!important;padding-bottom:2rem!important}.py-xxl-6{padding-top:2.5rem!important;padding-bottom:2.5rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:2rem!important}.pt-xxl-6{padding-top:2.5rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:2rem!important}.pe-xxl-6{padding-right:2.5rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:2rem!important}.pb-xxl-6{padding-bottom:2.5rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:2rem!important}.ps-xxl-6{padding-left:2.5rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:2rem!important}.gap-xxl-6{gap:2.5rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:2rem!important}.row-gap-xxl-6{row-gap:2.5rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-xxl-6{-moz-column-gap:2.5rem!important;column-gap:2.5rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}.columns-xxl-2{-moz-columns:2!important;columns:2!important}.columns-xxl-3{-moz-columns:3!important;columns:3!important}.columns-xxl-4{-moz-columns:4!important;columns:4!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}}:host,:root{--tblr-font-monospace:Monaco,Consolas,Liberation Mono,Courier New,monospace;--tblr-font-sans-serif:Inter Var,Inter,-apple-system,BlinkMacSystemFont,San Francisco,Segoe UI,Roboto,Helvetica Neue,sans-serif;--tblr-font-serif:Georgia,Times New Roman,times,serif;--tblr-font-comic:Comic Sans MS,Comic Sans,Chalkboard SE,Comic Neue,sans-serif,cursive;--tblr-gray-50:#f9fafb;--tblr-gray-100:#f3f4f6;--tblr-gray-200:#e5e7eb;--tblr-gray-300:#d1d5db;--tblr-gray-400:#9ca3af;--tblr-gray-500:#6b7280;--tblr-gray-600:#4b5563;--tblr-gray-700:#374151;--tblr-gray-800:#1f2937;--tblr-gray-900:#111827;--tblr-gray-950:#030712;--tblr-white:#ffffff;--tblr-black:#000000;--tblr-dark:#1f2937;--tblr-light:#f9fafb;--tblr-brand:#066fd1;--tblr-primary:#066fd1;--tblr-primary-rgb:6,111,209;--tblr-primary-fg:var(--tblr-light);--tblr-primary-darken:rgb(5.4, 99.9, 188.1);--tblr-primary-darken:color-mix(in oklab, var(--tblr-primary), transparent 20%);--tblr-primary-lt:rgb(230.1, 240.6, 250.4);--tblr-primary-lt:color-mix(in oklab, var(--tblr-primary) 10%, transparent);--tblr-primary-200:color-mix(in oklab, var(--tblr-primary) 20%, transparent);--tblr-primary-lt-rgb:230,241,250;--tblr-secondary:#6b7280;--tblr-secondary-rgb:107,114,128;--tblr-secondary-fg:var(--tblr-light);--tblr-secondary-darken:rgb(96.3, 102.6, 115.2);--tblr-secondary-darken:color-mix(in oklab, var(--tblr-secondary), transparent 20%);--tblr-secondary-lt:rgb(240.2, 240.9, 242.3);--tblr-secondary-lt:color-mix(in oklab, var(--tblr-secondary) 10%, transparent);--tblr-secondary-200:color-mix(in oklab, var(--tblr-secondary) 20%, transparent);--tblr-secondary-lt-rgb:240,241,242;--tblr-success:#2fb344;--tblr-success-rgb:47,179,68;--tblr-success-fg:var(--tblr-light);--tblr-success-darken:rgb(42.3, 161.1, 61.2);--tblr-success-darken:color-mix(in oklab, var(--tblr-success), transparent 20%);--tblr-success-lt:rgb(234.2, 247.4, 236.3);--tblr-success-lt:color-mix(in oklab, var(--tblr-success) 10%, transparent);--tblr-success-200:color-mix(in oklab, var(--tblr-success) 20%, transparent);--tblr-success-lt-rgb:234,247,236;--tblr-info:#4299e1;--tblr-info-rgb:66,153,225;--tblr-info-fg:var(--tblr-light);--tblr-info-darken:rgb(59.4, 137.7, 202.5);--tblr-info-darken:color-mix(in oklab, var(--tblr-info), transparent 20%);--tblr-info-lt:rgb(236.1, 244.8, 252);--tblr-info-lt:color-mix(in oklab, var(--tblr-info) 10%, transparent);--tblr-info-200:color-mix(in oklab, var(--tblr-info) 20%, transparent);--tblr-info-lt-rgb:236,245,252;--tblr-warning:#f59f00;--tblr-warning-rgb:245,159,0;--tblr-warning-fg:var(--tblr-light);--tblr-warning-darken:rgb(220.5, 143.1, 0);--tblr-warning-darken:color-mix(in oklab, var(--tblr-warning), transparent 20%);--tblr-warning-lt:rgb(254, 245.4, 229.5);--tblr-warning-lt:color-mix(in oklab, var(--tblr-warning) 10%, transparent);--tblr-warning-200:color-mix(in oklab, var(--tblr-warning) 20%, transparent);--tblr-warning-lt-rgb:254,245,230;--tblr-danger:#d63939;--tblr-danger-rgb:214,57,57;--tblr-danger-fg:var(--tblr-light);--tblr-danger-darken:rgb(192.6, 51.3, 51.3);--tblr-danger-darken:color-mix(in oklab, var(--tblr-danger), transparent 20%);--tblr-danger-lt:rgb(250.9, 235.2, 235.2);--tblr-danger-lt:color-mix(in oklab, var(--tblr-danger) 10%, transparent);--tblr-danger-200:color-mix(in oklab, var(--tblr-danger) 20%, transparent);--tblr-danger-lt-rgb:251,235,235;--tblr-light:#f9fafb;--tblr-light-rgb:249,250,251;--tblr-light-fg:var(--tblr-dark);--tblr-light-darken:rgb(224.1, 225, 225.9);--tblr-light-darken:color-mix(in oklab, var(--tblr-light), transparent 20%);--tblr-light-lt:rgb(254.4, 254.5, 254.6);--tblr-light-lt:color-mix(in oklab, var(--tblr-light) 10%, transparent);--tblr-light-200:color-mix(in oklab, var(--tblr-light) 20%, transparent);--tblr-light-lt-rgb:254,255,255;--tblr-dark:#1f2937;--tblr-dark-rgb:31,41,55;--tblr-dark-fg:var(--tblr-light);--tblr-dark-darken:rgb(27.9, 36.9, 49.5);--tblr-dark-darken:color-mix(in oklab, var(--tblr-dark), transparent 20%);--tblr-dark-lt:rgb(232.6, 233.6, 235);--tblr-dark-lt:color-mix(in oklab, var(--tblr-dark) 10%, transparent);--tblr-dark-200:color-mix(in oklab, var(--tblr-dark) 20%, transparent);--tblr-dark-lt-rgb:233,234,235;--tblr-muted:#6b7280;--tblr-muted-rgb:107,114,128;--tblr-muted-fg:var(--tblr-light);--tblr-muted-darken:rgb(96.3, 102.6, 115.2);--tblr-muted-darken:color-mix(in oklab, var(--tblr-muted), transparent 20%);--tblr-muted-lt:rgb(240.2, 240.9, 242.3);--tblr-muted-lt:color-mix(in oklab, var(--tblr-muted) 10%, transparent);--tblr-muted-200:color-mix(in oklab, var(--tblr-muted) 20%, transparent);--tblr-muted-lt-rgb:240,241,242;--tblr-blue:#066fd1;--tblr-blue-rgb:6,111,209;--tblr-blue-fg:var(--tblr-light);--tblr-blue-darken:rgb(5.4, 99.9, 188.1);--tblr-blue-darken:color-mix(in oklab, var(--tblr-blue), transparent 20%);--tblr-blue-lt:rgb(230.1, 240.6, 250.4);--tblr-blue-lt:color-mix(in oklab, var(--tblr-blue) 10%, transparent);--tblr-blue-200:color-mix(in oklab, var(--tblr-blue) 20%, transparent);--tblr-blue-lt-rgb:230,241,250;--tblr-azure:#4299e1;--tblr-azure-rgb:66,153,225;--tblr-azure-fg:var(--tblr-light);--tblr-azure-darken:rgb(59.4, 137.7, 202.5);--tblr-azure-darken:color-mix(in oklab, var(--tblr-azure), transparent 20%);--tblr-azure-lt:rgb(236.1, 244.8, 252);--tblr-azure-lt:color-mix(in oklab, var(--tblr-azure) 10%, transparent);--tblr-azure-200:color-mix(in oklab, var(--tblr-azure) 20%, transparent);--tblr-azure-lt-rgb:236,245,252;--tblr-indigo:#4263eb;--tblr-indigo-rgb:66,99,235;--tblr-indigo-fg:var(--tblr-light);--tblr-indigo-darken:rgb(59.4, 89.1, 211.5);--tblr-indigo-darken:color-mix(in oklab, var(--tblr-indigo), transparent 20%);--tblr-indigo-lt:rgb(236.1, 239.4, 253);--tblr-indigo-lt:color-mix(in oklab, var(--tblr-indigo) 10%, transparent);--tblr-indigo-200:color-mix(in oklab, var(--tblr-indigo) 20%, transparent);--tblr-indigo-lt-rgb:236,239,253;--tblr-purple:#ae3ec9;--tblr-purple-rgb:174,62,201;--tblr-purple-fg:var(--tblr-light);--tblr-purple-darken:rgb(156.6, 55.8, 180.9);--tblr-purple-darken:color-mix(in oklab, var(--tblr-purple), transparent 20%);--tblr-purple-lt:rgb(246.9, 235.7, 249.6);--tblr-purple-lt:color-mix(in oklab, var(--tblr-purple) 10%, transparent);--tblr-purple-200:color-mix(in oklab, var(--tblr-purple) 20%, transparent);--tblr-purple-lt-rgb:247,236,250;--tblr-pink:#d6336c;--tblr-pink-rgb:214,51,108;--tblr-pink-fg:var(--tblr-light);--tblr-pink-darken:rgb(192.6, 45.9, 97.2);--tblr-pink-darken:color-mix(in oklab, var(--tblr-pink), transparent 20%);--tblr-pink-lt:rgb(250.9, 234.6, 240.3);--tblr-pink-lt:color-mix(in oklab, var(--tblr-pink) 10%, transparent);--tblr-pink-200:color-mix(in oklab, var(--tblr-pink) 20%, transparent);--tblr-pink-lt-rgb:251,235,240;--tblr-red:#d63939;--tblr-red-rgb:214,57,57;--tblr-red-fg:var(--tblr-light);--tblr-red-darken:rgb(192.6, 51.3, 51.3);--tblr-red-darken:color-mix(in oklab, var(--tblr-red), transparent 20%);--tblr-red-lt:rgb(250.9, 235.2, 235.2);--tblr-red-lt:color-mix(in oklab, var(--tblr-red) 10%, transparent);--tblr-red-200:color-mix(in oklab, var(--tblr-red) 20%, transparent);--tblr-red-lt-rgb:251,235,235;--tblr-orange:#f76707;--tblr-orange-rgb:247,103,7;--tblr-orange-fg:var(--tblr-light);--tblr-orange-darken:rgb(222.3, 92.7, 6.3);--tblr-orange-darken:color-mix(in oklab, var(--tblr-orange), transparent 20%);--tblr-orange-lt:rgb(254.2, 239.8, 230.2);--tblr-orange-lt:color-mix(in oklab, var(--tblr-orange) 10%, transparent);--tblr-orange-200:color-mix(in oklab, var(--tblr-orange) 20%, transparent);--tblr-orange-lt-rgb:254,240,230;--tblr-yellow:#f59f00;--tblr-yellow-rgb:245,159,0;--tblr-yellow-fg:var(--tblr-light);--tblr-yellow-darken:rgb(220.5, 143.1, 0);--tblr-yellow-darken:color-mix(in oklab, var(--tblr-yellow), transparent 20%);--tblr-yellow-lt:rgb(254, 245.4, 229.5);--tblr-yellow-lt:color-mix(in oklab, var(--tblr-yellow) 10%, transparent);--tblr-yellow-200:color-mix(in oklab, var(--tblr-yellow) 20%, transparent);--tblr-yellow-lt-rgb:254,245,230;--tblr-lime:#74b816;--tblr-lime-rgb:116,184,22;--tblr-lime-fg:var(--tblr-light);--tblr-lime-darken:rgb(104.4, 165.6, 19.8);--tblr-lime-darken:color-mix(in oklab, var(--tblr-lime), transparent 20%);--tblr-lime-lt:rgb(241.1, 247.9, 231.7);--tblr-lime-lt:color-mix(in oklab, var(--tblr-lime) 10%, transparent);--tblr-lime-200:color-mix(in oklab, var(--tblr-lime) 20%, transparent);--tblr-lime-lt-rgb:241,248,232;--tblr-green:#2fb344;--tblr-green-rgb:47,179,68;--tblr-green-fg:var(--tblr-light);--tblr-green-darken:rgb(42.3, 161.1, 61.2);--tblr-green-darken:color-mix(in oklab, var(--tblr-green), transparent 20%);--tblr-green-lt:rgb(234.2, 247.4, 236.3);--tblr-green-lt:color-mix(in oklab, var(--tblr-green) 10%, transparent);--tblr-green-200:color-mix(in oklab, var(--tblr-green) 20%, transparent);--tblr-green-lt-rgb:234,247,236;--tblr-teal:#0ca678;--tblr-teal-rgb:12,166,120;--tblr-teal-fg:var(--tblr-light);--tblr-teal-darken:rgb(10.8, 149.4, 108);--tblr-teal-darken:color-mix(in oklab, var(--tblr-teal), transparent 20%);--tblr-teal-lt:rgb(230.7, 246.1, 241.5);--tblr-teal-lt:color-mix(in oklab, var(--tblr-teal) 10%, transparent);--tblr-teal-200:color-mix(in oklab, var(--tblr-teal) 20%, transparent);--tblr-teal-lt-rgb:231,246,242;--tblr-cyan:#17a2b8;--tblr-cyan-rgb:23,162,184;--tblr-cyan-fg:var(--tblr-light);--tblr-cyan-darken:rgb(20.7, 145.8, 165.6);--tblr-cyan-darken:color-mix(in oklab, var(--tblr-cyan), transparent 20%);--tblr-cyan-lt:rgb(231.8, 245.7, 247.9);--tblr-cyan-lt:color-mix(in oklab, var(--tblr-cyan) 10%, transparent);--tblr-cyan-200:color-mix(in oklab, var(--tblr-cyan) 20%, transparent);--tblr-cyan-lt-rgb:232,246,248;--tblr-x:#000000;--tblr-x-rgb:0,0,0;--tblr-x-fg:var(--tblr-light);--tblr-x-darken:black;--tblr-x-darken:color-mix(in oklab, var(--tblr-x), transparent 20%);--tblr-x-lt:rgb(229.5, 229.5, 229.5);--tblr-x-lt:color-mix(in oklab, var(--tblr-x) 10%, transparent);--tblr-x-200:color-mix(in oklab, var(--tblr-x) 20%, transparent);--tblr-x-lt-rgb:230,230,230;--tblr-facebook:#1877f2;--tblr-facebook-rgb:24,119,242;--tblr-facebook-fg:var(--tblr-light);--tblr-facebook-darken:rgb(21.6, 107.1, 217.8);--tblr-facebook-darken:color-mix(in oklab, var(--tblr-facebook), transparent 20%);--tblr-facebook-lt:rgb(231.9, 241.4, 253.7);--tblr-facebook-lt:color-mix(in oklab, var(--tblr-facebook) 10%, transparent);--tblr-facebook-200:color-mix(in oklab, var(--tblr-facebook) 20%, transparent);--tblr-facebook-lt-rgb:232,241,254;--tblr-twitter:#1da1f2;--tblr-twitter-rgb:29,161,242;--tblr-twitter-fg:var(--tblr-light);--tblr-twitter-darken:rgb(26.1, 144.9, 217.8);--tblr-twitter-darken:color-mix(in oklab, var(--tblr-twitter), transparent 20%);--tblr-twitter-lt:rgb(232.4, 245.6, 253.7);--tblr-twitter-lt:color-mix(in oklab, var(--tblr-twitter) 10%, transparent);--tblr-twitter-200:color-mix(in oklab, var(--tblr-twitter) 20%, transparent);--tblr-twitter-lt-rgb:232,246,254;--tblr-linkedin:#0a66c2;--tblr-linkedin-rgb:10,102,194;--tblr-linkedin-fg:var(--tblr-light);--tblr-linkedin-darken:rgb(9, 91.8, 174.6);--tblr-linkedin-darken:color-mix(in oklab, var(--tblr-linkedin), transparent 20%);--tblr-linkedin-lt:rgb(230.5, 239.7, 248.9);--tblr-linkedin-lt:color-mix(in oklab, var(--tblr-linkedin) 10%, transparent);--tblr-linkedin-200:color-mix(in oklab, var(--tblr-linkedin) 20%, transparent);--tblr-linkedin-lt-rgb:231,240,249;--tblr-google:#dc4e41;--tblr-google-rgb:220,78,65;--tblr-google-fg:var(--tblr-light);--tblr-google-darken:rgb(198, 70.2, 58.5);--tblr-google-darken:color-mix(in oklab, var(--tblr-google), transparent 20%);--tblr-google-lt:rgb(251.5, 237.3, 236);--tblr-google-lt:color-mix(in oklab, var(--tblr-google) 10%, transparent);--tblr-google-200:color-mix(in oklab, var(--tblr-google) 20%, transparent);--tblr-google-lt-rgb:252,237,236;--tblr-youtube:#ff0000;--tblr-youtube-rgb:255,0,0;--tblr-youtube-fg:var(--tblr-light);--tblr-youtube-darken:rgb(229.5, 0, 0);--tblr-youtube-darken:color-mix(in oklab, var(--tblr-youtube), transparent 20%);--tblr-youtube-lt:rgb(255, 229.5, 229.5);--tblr-youtube-lt:color-mix(in oklab, var(--tblr-youtube) 10%, transparent);--tblr-youtube-200:color-mix(in oklab, var(--tblr-youtube) 20%, transparent);--tblr-youtube-lt-rgb:255,230,230;--tblr-vimeo:#1ab7ea;--tblr-vimeo-rgb:26,183,234;--tblr-vimeo-fg:var(--tblr-light);--tblr-vimeo-darken:rgb(23.4, 164.7, 210.6);--tblr-vimeo-darken:color-mix(in oklab, var(--tblr-vimeo), transparent 20%);--tblr-vimeo-lt:rgb(232.1, 247.8, 252.9);--tblr-vimeo-lt:color-mix(in oklab, var(--tblr-vimeo) 10%, transparent);--tblr-vimeo-200:color-mix(in oklab, var(--tblr-vimeo) 20%, transparent);--tblr-vimeo-lt-rgb:232,248,253;--tblr-dribbble:#ea4c89;--tblr-dribbble-rgb:234,76,137;--tblr-dribbble-fg:var(--tblr-light);--tblr-dribbble-darken:rgb(210.6, 68.4, 123.3);--tblr-dribbble-darken:color-mix(in oklab, var(--tblr-dribbble), transparent 20%);--tblr-dribbble-lt:rgb(252.9, 237.1, 243.2);--tblr-dribbble-lt:color-mix(in oklab, var(--tblr-dribbble) 10%, transparent);--tblr-dribbble-200:color-mix(in oklab, var(--tblr-dribbble) 20%, transparent);--tblr-dribbble-lt-rgb:253,237,243;--tblr-github:#181717;--tblr-github-rgb:24,23,23;--tblr-github-fg:var(--tblr-light);--tblr-github-darken:rgb(21.6, 20.7, 20.7);--tblr-github-darken:color-mix(in oklab, var(--tblr-github), transparent 20%);--tblr-github-lt:rgb(231.9, 231.8, 231.8);--tblr-github-lt:color-mix(in oklab, var(--tblr-github) 10%, transparent);--tblr-github-200:color-mix(in oklab, var(--tblr-github) 20%, transparent);--tblr-github-lt-rgb:232,232,232;--tblr-instagram:#e4405f;--tblr-instagram-rgb:228,64,95;--tblr-instagram-fg:var(--tblr-light);--tblr-instagram-darken:rgb(205.2, 57.6, 85.5);--tblr-instagram-darken:color-mix(in oklab, var(--tblr-instagram), transparent 20%);--tblr-instagram-lt:rgb(252.3, 235.9, 239);--tblr-instagram-lt:color-mix(in oklab, var(--tblr-instagram) 10%, transparent);--tblr-instagram-200:color-mix(in oklab, var(--tblr-instagram) 20%, transparent);--tblr-instagram-lt-rgb:252,236,239;--tblr-pinterest:#bd081c;--tblr-pinterest-rgb:189,8,28;--tblr-pinterest-fg:var(--tblr-light);--tblr-pinterest-darken:rgb(170.1, 7.2, 25.2);--tblr-pinterest-darken:color-mix(in oklab, var(--tblr-pinterest), transparent 20%);--tblr-pinterest-lt:rgb(248.4, 230.3, 232.3);--tblr-pinterest-lt:color-mix(in oklab, var(--tblr-pinterest) 10%, transparent);--tblr-pinterest-200:color-mix(in oklab, var(--tblr-pinterest) 20%, transparent);--tblr-pinterest-lt-rgb:248,230,232;--tblr-vk:#6383a8;--tblr-vk-rgb:99,131,168;--tblr-vk-fg:var(--tblr-light);--tblr-vk-darken:rgb(89.1, 117.9, 151.2);--tblr-vk-darken:color-mix(in oklab, var(--tblr-vk), transparent 20%);--tblr-vk-lt:rgb(239.4, 242.6, 246.3);--tblr-vk-lt:color-mix(in oklab, var(--tblr-vk) 10%, transparent);--tblr-vk-200:color-mix(in oklab, var(--tblr-vk) 20%, transparent);--tblr-vk-lt-rgb:239,243,246;--tblr-rss:#ffa500;--tblr-rss-rgb:255,165,0;--tblr-rss-fg:var(--tblr-light);--tblr-rss-darken:rgb(229.5, 148.5, 0);--tblr-rss-darken:color-mix(in oklab, var(--tblr-rss), transparent 20%);--tblr-rss-lt:rgb(255, 246, 229.5);--tblr-rss-lt:color-mix(in oklab, var(--tblr-rss) 10%, transparent);--tblr-rss-200:color-mix(in oklab, var(--tblr-rss) 20%, transparent);--tblr-rss-lt-rgb:255,246,230;--tblr-flickr:#0063dc;--tblr-flickr-rgb:0,99,220;--tblr-flickr-fg:var(--tblr-light);--tblr-flickr-darken:rgb(0, 89.1, 198);--tblr-flickr-darken:color-mix(in oklab, var(--tblr-flickr), transparent 20%);--tblr-flickr-lt:rgb(229.5, 239.4, 251.5);--tblr-flickr-lt:color-mix(in oklab, var(--tblr-flickr) 10%, transparent);--tblr-flickr-200:color-mix(in oklab, var(--tblr-flickr) 20%, transparent);--tblr-flickr-lt-rgb:230,239,252;--tblr-bitbucket:#0052cc;--tblr-bitbucket-rgb:0,82,204;--tblr-bitbucket-fg:var(--tblr-light);--tblr-bitbucket-darken:rgb(0, 73.8, 183.6);--tblr-bitbucket-darken:color-mix(in oklab, var(--tblr-bitbucket), transparent 20%);--tblr-bitbucket-lt:rgb(229.5, 237.7, 249.9);--tblr-bitbucket-lt:color-mix(in oklab, var(--tblr-bitbucket) 10%, transparent);--tblr-bitbucket-200:color-mix(in oklab, var(--tblr-bitbucket) 20%, transparent);--tblr-bitbucket-lt-rgb:230,238,250;--tblr-tabler:#066fd1;--tblr-tabler-rgb:6,111,209;--tblr-tabler-fg:var(--tblr-light);--tblr-tabler-darken:rgb(5.4, 99.9, 188.1);--tblr-tabler-darken:color-mix(in oklab, var(--tblr-tabler), transparent 20%);--tblr-tabler-lt:rgb(230.1, 240.6, 250.4);--tblr-tabler-lt:color-mix(in oklab, var(--tblr-tabler) 10%, transparent);--tblr-tabler-200:color-mix(in oklab, var(--tblr-tabler) 20%, transparent);--tblr-tabler-lt-rgb:230,241,250;--tblr-gray-50-fg:var(--tblr-body-color);--tblr-gray-100-fg:var(--tblr-body-color);--tblr-gray-200-fg:var(--tblr-body-color);--tblr-gray-300-fg:var(--tblr-body-color);--tblr-gray-400-fg:var(--tblr-white);--tblr-gray-500-fg:var(--tblr-white);--tblr-gray-600-fg:var(--tblr-white);--tblr-gray-700-fg:var(--tblr-white);--tblr-gray-800-fg:var(--tblr-white);--tblr-gray-900-fg:var(--tblr-white);--tblr-gray-950-fg:var(--tblr-white);--tblr-spacer-0:0;--tblr-spacer-1:0.25rem;--tblr-spacer-2:0.5rem;--tblr-spacer-3:1rem;--tblr-spacer-4:1.5rem;--tblr-spacer-5:2rem;--tblr-spacer-6:2.5rem;--tblr-font-weight-light:300;--tblr-font-weight-normal:400;--tblr-font-weight-medium:500;--tblr-font-weight-bold:600;--tblr-font-weight-black:700;--tblr-font-weight-headings:var(--tblr-font-weight-bold);--tblr-font-size-h1:1.5rem;--tblr-font-size-h2:1.25rem;--tblr-font-size-h3:1rem;--tblr-font-size-h4:0.875rem;--tblr-font-size-h5:0.75rem;--tblr-font-size-h6:0.625rem;--tblr-line-height-h1:2rem;--tblr-line-height-h2:1.75rem;--tblr-line-height-h3:1.5rem;--tblr-line-height-h4:1.25rem;--tblr-line-height-h5:1rem;--tblr-line-height-h6:1rem;--tblr-shadow:rgba(var(--tblr-body-color-rgb), 0.04) 0 2px 4px 0;--tblr-shadow-border:inset 0 0 0 1px var(--tblr-border-color-translucent);--tblr-shadow-transparent:0 0 0 0 transparent;--tblr-shadow-input:0 1px 1px rgba(var(--tblr-body-color-rgb), 0.06);--tblr-shadow-card:0 0 4px rgba(var(--tblr-body-color-rgb), 0.04);--tblr-shadow-card-hover:rgba(var(--tblr-body-color-rgb), 0.16) 0 2px 16px 0;--tblr-shadow-dropdown:0 16px 24px 2px rgba(0, 0, 0, 0.07),0 6px 30px 5px rgba(0, 0, 0, 0.06),0 8px 10px -5px rgba(0, 0, 0, 0.1);--tblr-border-radius-scale:1;--tblr-border-radius-0:calc(0 * var(--tblr-border-radius-scale, 1));--tblr-border-radius-sm:calc(4px * var(--tblr-border-radius-scale, 1));--tblr-border-radius-md:calc(6px * var(--tblr-border-radius-scale, 1));--tblr-border-radius-lg:calc(8px * var(--tblr-border-radius-scale, 1));--tblr-border-radius-pill:calc(100rem * var(--tblr-border-radius-scale, 1));--tblr-border-radius:var(--tblr-border-radius-md);--tblr-backdrop-opacity:24%;--tblr-backdrop-bg:var(--tblr-bg-surface-dark);--tblr-backdrop-bg-dark:color-mix(in srgb, var(--tblr-color-dark), transparent var(--tblr-backdrop-opacity));--tblr-backdrop-bg-light:color-mix(in srgb, var(--tblr-color-light), transparent var(--tblr-backdrop-opacity));--tblr-backdrop-blur:4px;--tblr-backdrop-filter:blur(var(--tblr-backdrop-blur))}:host,:root{font-size:16px;height:100%}@media (min-width:992px){:host,:root{margin-left:calc(100vw - 100%);margin-right:0}}:host,:root,[data-bs-theme=light]{color-scheme:light;--tblr-spacer:var(--tblr-spacer-2);--tblr-bg-surface:var(--tblr-bg-surface-primary);--tblr-bg-surface-primary:var(--tblr-white);--tblr-bg-surface-secondary:var(--tblr-gray-50);--tblr-bg-surface-tertiary:var(--tblr-gray-50);--tblr-bg-surface-dark:var(--tblr-gray-900);--tblr-bg-surface-inverted:var(--tblr-gray-900);--tblr-bg-forms:var(--tblr-bg-surface);--tblr-text-inverted:var(--tblr-gray-100);--tblr-body-color:var(--tblr-gray-700);--tblr-body-bg:var(--tblr-bg-surface-secondary);--tblr-link-color:var(--tblr-primary);--tblr-link-hover-color:color-mix(in srgb, var(--tblr-primary), #000 20%);--tblr-secondary:var(--tblr-gray-500);--tblr-tertiary:var(--tblr-gray-400);--tblr-border-color:#e5e7eb;--tblr-border-color-translucent:rgba(4, 32, 69, 0.1);--tblr-border-dark-color:#9ca3af;--tblr-border-dark-color-translucent:rgba(4, 32, 69, 0.27);--tblr-border-active-color:rgb(169.16, 173.22, 181.34);--tblr-icon-color:var(--tblr-gray-400);--tblr-active-bg:rgba(var(--tblr-primary-rgb), 0.04);--tblr-disabled-bg:var(--tblr-bg-surface-secondary);--tblr-disabled-color:color-mix(in srgb, var(--tblr-body-color) 40%, transparent);--tblr-code-color:light-dark(var(--tblr-gray-600), var(--tblr-gray-400));--tblr-code-bg:light-dark(var(--tblr-gray-100), var(--tblr-gray-900));--tblr-dark-mode-border-color:rgb(45.7069767442, 60.4511627907, 81.0930232558);--tblr-dark-mode-border-color-translucent:rgba(72, 110, 149, 0.14);--tblr-dark-mode-border-active-color:rgb(53.0604651163, 70.176744186, 94.1395348837);--tblr-dark-mode-border-dark-color:rgb(38.3534883721, 50.7255813953, 68.0465116279);--tblr-page-padding:var(--tblr-spacer-3);--tblr-page-padding-y:var(--tblr-spacer-4)}@media (max-width:991.98px){:host,:root,[data-bs-theme=light]{--tblr-page-padding:var(--tblr-spacer-2)}}@keyframes pulse{0%{transform:scale(1)}14%{transform:scale(1.25)}28%{transform:scale(1)}42%{transform:scale(1.25)}70%{transform:scale(1)}}@keyframes tada{0%{transform:scale3d(1,1,1)}10%,5%{transform:scale3d(.9,.9,.9) rotate3d(0,0,1,-5deg)}15%,25%,35%,45%{transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,5deg)}20%,30%,40%{transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-5deg)}50%{transform:scale3d(1,1,1)}}@keyframes rotate-360{from{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes blink{from{opacity:0}50%{opacity:1}to{opacity:0}}@keyframes shake{0%{transform:scaleX(1)}20%{transform:scale3d(.9,.9,.9) rotate(-5deg)}50%,70%,90%{transform:scale3d(1.25,1.25,1.25) rotate(5deg)}60%,80%{transform:scale3d(1.25,1.25,1.25) rotate(-5deg)}to{transform:scaleX(1)}}body{letter-spacing:0;touch-action:manipulation;text-rendering:optimizeLegibility;font-feature-settings:"liga" 0,"cv03","cv04","cv11";position:relative;min-height:100%;height:100%;padding:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media print{body{background:0 0}}*{scrollbar-color:color-mix(in srgb,var(--tblr-scrollbar-color,var(--tblr-body-color)) 20%,transparent) transparent}::-webkit-scrollbar{width:1rem;height:1rem;-webkit-transition:background .3s;transition:background .3s}@media (prefers-reduced-motion:reduce){::-webkit-scrollbar{-webkit-transition:none;transition:none}}::-webkit-scrollbar-thumb{border-radius:1rem;border:5px solid transparent;box-shadow:inset 0 0 0 1rem color-mix(in srgb,var(--tblr-scrollbar-color,var(--tblr-body-color)) 20%,transparent)}::-webkit-scrollbar-track{background:0 0}:hover::-webkit-scrollbar-thumb{box-shadow:inset 0 0 0 1rem color-mix(in srgb,var(--tblr-scrollbar-color,var(--tblr-body-color)) 40%,transparent)}::-webkit-scrollbar-corner{background:0 0}.layout-fluid .container,.layout-fluid [class*=" container-"],.layout-fluid [class^=container-]{max-width:100%}.layout-boxed{--tblr-theme-boxed-border-radius:0;--tblr-theme-boxed-width:1320px}@media (min-width:768px){.layout-boxed{background:#1f2937 linear-gradient(to right,rgba(255,255,255,.1),transparent) fixed;padding:1rem;--tblr-theme-boxed-border-radius:6px}}.layout-boxed .page{margin:0 auto;max-width:var(--tblr-theme-boxed-width);border-radius:var(--tblr-theme-boxed-border-radius);color:var(--tblr-body-color)}@media (min-width:768px){.layout-boxed .page{border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);background:var(--tblr-body-bg)}}.layout-boxed .page>.navbar:first-child{border-top-left-radius:var(--tblr-theme-boxed-border-radius);border-top-right-radius:var(--tblr-theme-boxed-border-radius)}.navbar{--tblr-navbar-bg:var(--tblr-bg-surface);--tblr-navbar-border-width:var(--tblr-border-width);--tblr-navbar-active-border-color:var(--tblr-primary);--tblr-navbar-active-bg:rgba(0, 0, 0, 0.2);--tblr-navbar-border-color:var(--tblr-border-color);--tblr-navbar-hover-color:var(--tblr-body-color);align-items:stretch;min-height:3.5rem;box-shadow:inset 0 calc(-1 * var(--tblr-navbar-border-width)) 0 0 var(--tblr-navbar-border-color);background:var(--tblr-navbar-bg);color:var(--tblr-navbar-color)}.navbar-collapse .navbar{flex-grow:1}.navbar.collapsing{min-height:0}.navbar .dropdown-menu{position:absolute;z-index:1030}.navbar .navbar-nav{min-height:3rem}.navbar .navbar-nav .nav-link{position:relative;min-width:2.5rem;min-height:2.5rem;justify-content:center;border-radius:var(--tblr-border-radius)}.navbar .navbar-nav .nav-link .badge{position:absolute;top:.375rem;right:.375rem;transform:translate(50%,-50%)}@media (max-width:575.98px){.navbar-expand-sm .navbar-collapse{flex-direction:column}.navbar-expand-sm .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-expand-sm .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-expand-sm .navbar-collapse .navbar-nav .nav-link{padding:.5rem calc(calc(var(--tblr-page-padding) * 2)/ 2);justify-content:flex-start}.navbar-expand-sm .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-expand-sm .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 1.75rem);color:inherit}.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-item.disabled{color:var(--tblr-disabled-color);pointer-events:none;background-color:transparent}.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 3.25rem)}.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 4.75rem)}.navbar-expand-sm .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-expand-sm .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:576px){.navbar-expand-sm .navbar-collapse{width:auto;flex:1 1 auto}.navbar-expand-sm .nav-item.active{position:relative}.navbar-expand-sm .nav-item.active .nav-link{color:var(--tblr-navbar-active-color)}.navbar-expand-sm .nav-item.active:after{content:"";position:absolute;left:0;right:0;bottom:-.25rem;border:0 var(--tblr-border-style) var(--tblr-navbar-active-border-color);border-bottom-width:2px}.navbar-expand-sm.navbar-vertical{box-shadow:inset calc(-1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-sm.navbar-vertical.navbar-end,.navbar-expand-sm.navbar-vertical.navbar-right{box-shadow:inset calc(1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-sm.navbar-vertical~.navbar,.navbar-expand-sm.navbar-vertical~.page-wrapper{margin-left:15rem}.navbar-expand-sm.navbar-vertical.navbar-end~.navbar,.navbar-expand-sm.navbar-vertical.navbar-end~.page-wrapper,.navbar-expand-sm.navbar-vertical.navbar-right~.navbar,.navbar-expand-sm.navbar-vertical.navbar-right~.page-wrapper{margin-left:0;margin-right:15rem}}@media (max-width:767.98px){.navbar-expand-md .navbar-collapse{flex-direction:column}.navbar-expand-md .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-expand-md .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-expand-md .navbar-collapse .navbar-nav .nav-link{padding:.5rem calc(calc(var(--tblr-page-padding) * 2)/ 2);justify-content:flex-start}.navbar-expand-md .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-expand-md .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 1.75rem);color:inherit}.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-item.disabled{color:var(--tblr-disabled-color);pointer-events:none;background-color:transparent}.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 3.25rem)}.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 4.75rem)}.navbar-expand-md .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-expand-md .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:768px){.navbar-expand-md .navbar-collapse{width:auto;flex:1 1 auto}.navbar-expand-md .nav-item.active{position:relative}.navbar-expand-md .nav-item.active .nav-link{color:var(--tblr-navbar-active-color)}.navbar-expand-md .nav-item.active:after{content:"";position:absolute;left:0;right:0;bottom:-.25rem;border:0 var(--tblr-border-style) var(--tblr-navbar-active-border-color);border-bottom-width:2px}.navbar-expand-md.navbar-vertical{box-shadow:inset calc(-1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-md.navbar-vertical.navbar-end,.navbar-expand-md.navbar-vertical.navbar-right{box-shadow:inset calc(1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-md.navbar-vertical~.navbar,.navbar-expand-md.navbar-vertical~.page-wrapper{margin-left:15rem}.navbar-expand-md.navbar-vertical.navbar-end~.navbar,.navbar-expand-md.navbar-vertical.navbar-end~.page-wrapper,.navbar-expand-md.navbar-vertical.navbar-right~.navbar,.navbar-expand-md.navbar-vertical.navbar-right~.page-wrapper{margin-left:0;margin-right:15rem}}@media (max-width:991.98px){.navbar-expand-lg .navbar-collapse{flex-direction:column}.navbar-expand-lg .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-expand-lg .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-expand-lg .navbar-collapse .navbar-nav .nav-link{padding:.5rem calc(calc(var(--tblr-page-padding) * 2)/ 2);justify-content:flex-start}.navbar-expand-lg .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-expand-lg .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 1.75rem);color:inherit}.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item.disabled{color:var(--tblr-disabled-color);pointer-events:none;background-color:transparent}.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 3.25rem)}.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 4.75rem)}.navbar-expand-lg .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-expand-lg .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:992px){.navbar-expand-lg .navbar-collapse{width:auto;flex:1 1 auto}.navbar-expand-lg .nav-item.active{position:relative}.navbar-expand-lg .nav-item.active .nav-link{color:var(--tblr-navbar-active-color)}.navbar-expand-lg .nav-item.active:after{content:"";position:absolute;left:0;right:0;bottom:-.25rem;border:0 var(--tblr-border-style) var(--tblr-navbar-active-border-color);border-bottom-width:2px}.navbar-expand-lg.navbar-vertical{box-shadow:inset calc(-1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-lg.navbar-vertical.navbar-end,.navbar-expand-lg.navbar-vertical.navbar-right{box-shadow:inset calc(1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-lg.navbar-vertical~.navbar,.navbar-expand-lg.navbar-vertical~.page-wrapper{margin-left:15rem}.navbar-expand-lg.navbar-vertical.navbar-end~.navbar,.navbar-expand-lg.navbar-vertical.navbar-end~.page-wrapper,.navbar-expand-lg.navbar-vertical.navbar-right~.navbar,.navbar-expand-lg.navbar-vertical.navbar-right~.page-wrapper{margin-left:0;margin-right:15rem}}@media (max-width:1199.98px){.navbar-expand-xl .navbar-collapse{flex-direction:column}.navbar-expand-xl .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-expand-xl .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-expand-xl .navbar-collapse .navbar-nav .nav-link{padding:.5rem calc(calc(var(--tblr-page-padding) * 2)/ 2);justify-content:flex-start}.navbar-expand-xl .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-expand-xl .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 1.75rem);color:inherit}.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-item.disabled{color:var(--tblr-disabled-color);pointer-events:none;background-color:transparent}.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 3.25rem)}.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 4.75rem)}.navbar-expand-xl .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-expand-xl .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:1200px){.navbar-expand-xl .navbar-collapse{width:auto;flex:1 1 auto}.navbar-expand-xl .nav-item.active{position:relative}.navbar-expand-xl .nav-item.active .nav-link{color:var(--tblr-navbar-active-color)}.navbar-expand-xl .nav-item.active:after{content:"";position:absolute;left:0;right:0;bottom:-.25rem;border:0 var(--tblr-border-style) var(--tblr-navbar-active-border-color);border-bottom-width:2px}.navbar-expand-xl.navbar-vertical{box-shadow:inset calc(-1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-xl.navbar-vertical.navbar-end,.navbar-expand-xl.navbar-vertical.navbar-right{box-shadow:inset calc(1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-xl.navbar-vertical~.navbar,.navbar-expand-xl.navbar-vertical~.page-wrapper{margin-left:15rem}.navbar-expand-xl.navbar-vertical.navbar-end~.navbar,.navbar-expand-xl.navbar-vertical.navbar-end~.page-wrapper,.navbar-expand-xl.navbar-vertical.navbar-right~.navbar,.navbar-expand-xl.navbar-vertical.navbar-right~.page-wrapper{margin-left:0;margin-right:15rem}}@media (max-width:1399.98px){.navbar-expand-xxl .navbar-collapse{flex-direction:column}.navbar-expand-xxl .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-expand-xxl .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-expand-xxl .navbar-collapse .navbar-nav .nav-link{padding:.5rem calc(calc(var(--tblr-page-padding) * 2)/ 2);justify-content:flex-start}.navbar-expand-xxl .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-expand-xxl .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 1.75rem);color:inherit}.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-item.disabled{color:var(--tblr-disabled-color);pointer-events:none;background-color:transparent}.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 3.25rem)}.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 4.75rem)}.navbar-expand-xxl .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-expand-xxl .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:1400px){.navbar-expand-xxl .navbar-collapse{width:auto;flex:1 1 auto}.navbar-expand-xxl .nav-item.active{position:relative}.navbar-expand-xxl .nav-item.active .nav-link{color:var(--tblr-navbar-active-color)}.navbar-expand-xxl .nav-item.active:after{content:"";position:absolute;left:0;right:0;bottom:-.25rem;border:0 var(--tblr-border-style) var(--tblr-navbar-active-border-color);border-bottom-width:2px}.navbar-expand-xxl.navbar-vertical{box-shadow:inset calc(-1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-xxl.navbar-vertical.navbar-end,.navbar-expand-xxl.navbar-vertical.navbar-right{box-shadow:inset calc(1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand-xxl.navbar-vertical~.navbar,.navbar-expand-xxl.navbar-vertical~.page-wrapper{margin-left:15rem}.navbar-expand-xxl.navbar-vertical.navbar-end~.navbar,.navbar-expand-xxl.navbar-vertical.navbar-end~.page-wrapper,.navbar-expand-xxl.navbar-vertical.navbar-right~.navbar,.navbar-expand-xxl.navbar-vertical.navbar-right~.page-wrapper{margin-left:0;margin-right:15rem}}.navbar-expand .navbar-collapse{flex-direction:column}.navbar-expand .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-expand .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-expand .navbar-collapse .navbar-nav .nav-link{padding:.5rem calc(calc(var(--tblr-page-padding) * 2)/ 2);justify-content:flex-start}.navbar-expand .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-expand .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-expand .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 1.75rem);color:inherit}.navbar-expand .navbar-collapse .dropdown-menu .dropdown-item.disabled{color:var(--tblr-disabled-color);pointer-events:none;background-color:transparent}.navbar-expand .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-expand .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-expand .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 3.25rem)}.navbar-expand .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 4.75rem)}.navbar-expand .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-expand .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}.navbar-expand .navbar-collapse{width:auto;flex:1 1 auto}.navbar-expand .nav-item.active{position:relative}.navbar-expand .nav-item.active .nav-link{color:var(--tblr-navbar-active-color)}.navbar-expand .nav-item.active:after{content:"";position:absolute;left:0;right:0;bottom:-.25rem;border:0 var(--tblr-border-style) var(--tblr-navbar-active-border-color);border-bottom-width:2px}.navbar-expand.navbar-vertical{box-shadow:inset calc(-1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand.navbar-vertical.navbar-end,.navbar-expand.navbar-vertical.navbar-right{box-shadow:inset calc(1 * var(--tblr-navbar-border-width)) 0 0 0 var(--tblr-navbar-border-color)}.navbar-expand.navbar-vertical~.navbar,.navbar-expand.navbar-vertical~.page-wrapper{margin-left:15rem}.navbar-expand.navbar-vertical.navbar-end~.navbar,.navbar-expand.navbar-vertical.navbar-end~.page-wrapper,.navbar-expand.navbar-vertical.navbar-right~.navbar,.navbar-expand.navbar-vertical.navbar-right~.page-wrapper{margin-left:0;margin-right:15rem}.navbar-brand{display:inline-flex;align-items:center;font-weight:var(--tblr-font-weight-bold);margin:0;line-height:1;gap:.5rem}.navbar-brand-image{height:2rem;width:auto}.navbar-toggler{border:0;width:2rem;height:2rem;position:relative;display:flex;align-items:center;justify-content:center}.navbar-toggler-icon{height:2px;width:1.25em;background:currentColor;border-radius:10px;transition:top .2s .2s,bottom .2s .2s,transform .2s,opacity 0s .2s;position:relative}@media (prefers-reduced-motion:reduce){.navbar-toggler-icon{transition:none}}.navbar-toggler-icon:after,.navbar-toggler-icon:before{content:"";display:block;height:inherit;width:inherit;border-radius:inherit;background:inherit;position:absolute;left:0;transition:inherit}@media (prefers-reduced-motion:reduce){.navbar-toggler-icon:after,.navbar-toggler-icon:before{transition:none}}.navbar-toggler-icon:before{top:-.45em}.navbar-toggler-icon:after{bottom:-.45em}.navbar-toggler[aria-expanded=true] .navbar-toggler-icon{transform:rotate(45deg);transition:top .3s,bottom .3s,transform .3s .3s,opacity 0s .3s}@media (prefers-reduced-motion:reduce){.navbar-toggler[aria-expanded=true] .navbar-toggler-icon{transition:none}}.navbar-toggler[aria-expanded=true] .navbar-toggler-icon:before{top:0;transform:rotate(-90deg)}.navbar-toggler[aria-expanded=true] .navbar-toggler-icon:after{bottom:0;opacity:0}.navbar-transparent{--tblr-navbar-border-color:transparent!important;background:0 0!important}.navbar-nav{--tblr-nav-link-hover-bg:color-mix(in srgb, var(--tblr-nav-link-color) 4%, transparent);margin:0;padding:0;align-items:stretch}.navbar-nav .nav-item{display:flex;flex-direction:column;justify-content:center}.navbar-side{margin:0;display:flex;flex-direction:row;align-items:center;justify-content:space-around}@media (min-width:576px){.navbar-vertical.navbar-expand-sm{width:15rem;position:fixed;top:0;left:0;bottom:0;z-index:1030;align-items:flex-start;transition:transform .3s;overflow-y:scroll;padding:0}}@media (min-width:576px) and (prefers-reduced-motion:reduce){.navbar-vertical.navbar-expand-sm{transition:none}}@media (min-width:576px){.navbar-vertical.navbar-expand-sm.navbar-end,.navbar-vertical.navbar-expand-sm.navbar-right{left:auto;right:0}.navbar-vertical.navbar-expand-sm .navbar-brand{padding:.75rem 0;justify-content:center}.navbar-vertical.navbar-expand-sm .navbar-collapse{align-items:stretch}.navbar-vertical.navbar-expand-sm .navbar-nav{flex-direction:column;flex-grow:1;min-height:auto}.navbar-vertical.navbar-expand-sm .navbar-nav .nav-link{padding-top:.5rem;padding-bottom:.5rem}.navbar-vertical.navbar-expand-sm>[class^=container]{flex-direction:column;align-items:stretch;min-height:100%;justify-content:flex-start;padding:0}.navbar-vertical.navbar-expand-sm~.page{padding-left:15rem}.navbar-vertical.navbar-expand-sm~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand-sm.navbar-end~.page,.navbar-vertical.navbar-expand-sm.navbar-right~.page{padding-left:0;padding-right:15rem}.navbar-vertical.navbar-expand-sm .navbar-collapse{flex-direction:column}.navbar-vertical.navbar-expand-sm .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-vertical.navbar-expand-sm .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-vertical.navbar-expand-sm .navbar-collapse .navbar-nav .nav-link{padding:.5rem calc(calc(var(--tblr-page-padding) * 2)/ 2);justify-content:flex-start}.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 1.75rem);color:inherit}.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-item.disabled{color:var(--tblr-disabled-color);pointer-events:none;background-color:transparent}.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 3.25rem)}.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 4.75rem)}.navbar-vertical.navbar-expand-sm .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-vertical.navbar-expand-sm .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:768px){.navbar-vertical.navbar-expand-md{width:15rem;position:fixed;top:0;left:0;bottom:0;z-index:1030;align-items:flex-start;transition:transform .3s;overflow-y:scroll;padding:0}}@media (min-width:768px) and (prefers-reduced-motion:reduce){.navbar-vertical.navbar-expand-md{transition:none}}@media (min-width:768px){.navbar-vertical.navbar-expand-md.navbar-end,.navbar-vertical.navbar-expand-md.navbar-right{left:auto;right:0}.navbar-vertical.navbar-expand-md .navbar-brand{padding:.75rem 0;justify-content:center}.navbar-vertical.navbar-expand-md .navbar-collapse{align-items:stretch}.navbar-vertical.navbar-expand-md .navbar-nav{flex-direction:column;flex-grow:1;min-height:auto}.navbar-vertical.navbar-expand-md .navbar-nav .nav-link{padding-top:.5rem;padding-bottom:.5rem}.navbar-vertical.navbar-expand-md>[class^=container]{flex-direction:column;align-items:stretch;min-height:100%;justify-content:flex-start;padding:0}.navbar-vertical.navbar-expand-md~.page{padding-left:15rem}.navbar-vertical.navbar-expand-md~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand-md.navbar-end~.page,.navbar-vertical.navbar-expand-md.navbar-right~.page{padding-left:0;padding-right:15rem}.navbar-vertical.navbar-expand-md .navbar-collapse{flex-direction:column}.navbar-vertical.navbar-expand-md .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-vertical.navbar-expand-md .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-vertical.navbar-expand-md .navbar-collapse .navbar-nav .nav-link{padding:.5rem calc(calc(var(--tblr-page-padding) * 2)/ 2);justify-content:flex-start}.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 1.75rem);color:inherit}.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-item.disabled{color:var(--tblr-disabled-color);pointer-events:none;background-color:transparent}.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 3.25rem)}.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 4.75rem)}.navbar-vertical.navbar-expand-md .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-vertical.navbar-expand-md .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:992px){.navbar-vertical.navbar-expand-lg{width:15rem;position:fixed;top:0;left:0;bottom:0;z-index:1030;align-items:flex-start;transition:transform .3s;overflow-y:scroll;padding:0}}@media (min-width:992px) and (prefers-reduced-motion:reduce){.navbar-vertical.navbar-expand-lg{transition:none}}@media (min-width:992px){.navbar-vertical.navbar-expand-lg.navbar-end,.navbar-vertical.navbar-expand-lg.navbar-right{left:auto;right:0}.navbar-vertical.navbar-expand-lg .navbar-brand{padding:.75rem 0;justify-content:center}.navbar-vertical.navbar-expand-lg .navbar-collapse{align-items:stretch}.navbar-vertical.navbar-expand-lg .navbar-nav{flex-direction:column;flex-grow:1;min-height:auto}.navbar-vertical.navbar-expand-lg .navbar-nav .nav-link{padding-top:.5rem;padding-bottom:.5rem}.navbar-vertical.navbar-expand-lg>[class^=container]{flex-direction:column;align-items:stretch;min-height:100%;justify-content:flex-start;padding:0}.navbar-vertical.navbar-expand-lg~.page{padding-left:15rem}.navbar-vertical.navbar-expand-lg~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand-lg.navbar-end~.page,.navbar-vertical.navbar-expand-lg.navbar-right~.page{padding-left:0;padding-right:15rem}.navbar-vertical.navbar-expand-lg .navbar-collapse{flex-direction:column}.navbar-vertical.navbar-expand-lg .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-vertical.navbar-expand-lg .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-vertical.navbar-expand-lg .navbar-collapse .navbar-nav .nav-link{padding:.5rem calc(calc(var(--tblr-page-padding) * 2)/ 2);justify-content:flex-start}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 1.75rem);color:inherit}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item.disabled{color:var(--tblr-disabled-color);pointer-events:none;background-color:transparent}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 3.25rem)}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 4.75rem)}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-vertical.navbar-expand-lg .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:1200px){.navbar-vertical.navbar-expand-xl{width:15rem;position:fixed;top:0;left:0;bottom:0;z-index:1030;align-items:flex-start;transition:transform .3s;overflow-y:scroll;padding:0}}@media (min-width:1200px) and (prefers-reduced-motion:reduce){.navbar-vertical.navbar-expand-xl{transition:none}}@media (min-width:1200px){.navbar-vertical.navbar-expand-xl.navbar-end,.navbar-vertical.navbar-expand-xl.navbar-right{left:auto;right:0}.navbar-vertical.navbar-expand-xl .navbar-brand{padding:.75rem 0;justify-content:center}.navbar-vertical.navbar-expand-xl .navbar-collapse{align-items:stretch}.navbar-vertical.navbar-expand-xl .navbar-nav{flex-direction:column;flex-grow:1;min-height:auto}.navbar-vertical.navbar-expand-xl .navbar-nav .nav-link{padding-top:.5rem;padding-bottom:.5rem}.navbar-vertical.navbar-expand-xl>[class^=container]{flex-direction:column;align-items:stretch;min-height:100%;justify-content:flex-start;padding:0}.navbar-vertical.navbar-expand-xl~.page{padding-left:15rem}.navbar-vertical.navbar-expand-xl~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand-xl.navbar-end~.page,.navbar-vertical.navbar-expand-xl.navbar-right~.page{padding-left:0;padding-right:15rem}.navbar-vertical.navbar-expand-xl .navbar-collapse{flex-direction:column}.navbar-vertical.navbar-expand-xl .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-vertical.navbar-expand-xl .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-vertical.navbar-expand-xl .navbar-collapse .navbar-nav .nav-link{padding:.5rem calc(calc(var(--tblr-page-padding) * 2)/ 2);justify-content:flex-start}.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 1.75rem);color:inherit}.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-item.disabled{color:var(--tblr-disabled-color);pointer-events:none;background-color:transparent}.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 3.25rem)}.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 4.75rem)}.navbar-vertical.navbar-expand-xl .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-vertical.navbar-expand-xl .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}@media (min-width:1400px){.navbar-vertical.navbar-expand-xxl{width:15rem;position:fixed;top:0;left:0;bottom:0;z-index:1030;align-items:flex-start;transition:transform .3s;overflow-y:scroll;padding:0}}@media (min-width:1400px) and (prefers-reduced-motion:reduce){.navbar-vertical.navbar-expand-xxl{transition:none}}@media (min-width:1400px){.navbar-vertical.navbar-expand-xxl.navbar-end,.navbar-vertical.navbar-expand-xxl.navbar-right{left:auto;right:0}.navbar-vertical.navbar-expand-xxl .navbar-brand{padding:.75rem 0;justify-content:center}.navbar-vertical.navbar-expand-xxl .navbar-collapse{align-items:stretch}.navbar-vertical.navbar-expand-xxl .navbar-nav{flex-direction:column;flex-grow:1;min-height:auto}.navbar-vertical.navbar-expand-xxl .navbar-nav .nav-link{padding-top:.5rem;padding-bottom:.5rem}.navbar-vertical.navbar-expand-xxl>[class^=container]{flex-direction:column;align-items:stretch;min-height:100%;justify-content:flex-start;padding:0}.navbar-vertical.navbar-expand-xxl~.page{padding-left:15rem}.navbar-vertical.navbar-expand-xxl~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand-xxl.navbar-end~.page,.navbar-vertical.navbar-expand-xxl.navbar-right~.page{padding-left:0;padding-right:15rem}.navbar-vertical.navbar-expand-xxl .navbar-collapse{flex-direction:column}.navbar-vertical.navbar-expand-xxl .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-vertical.navbar-expand-xxl .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-vertical.navbar-expand-xxl .navbar-collapse .navbar-nav .nav-link{padding:.5rem calc(calc(var(--tblr-page-padding) * 2)/ 2);justify-content:flex-start}.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 1.75rem);color:inherit}.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-item.disabled{color:var(--tblr-disabled-color);pointer-events:none;background-color:transparent}.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 3.25rem)}.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 4.75rem)}.navbar-vertical.navbar-expand-xxl .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-vertical.navbar-expand-xxl .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}}.navbar-vertical.navbar-expand{width:15rem;position:fixed;top:0;left:0;bottom:0;z-index:1030;align-items:flex-start;transition:transform .3s;overflow-y:scroll;padding:0}@media (prefers-reduced-motion:reduce){.navbar-vertical.navbar-expand{transition:none}}.navbar-vertical.navbar-expand.navbar-end,.navbar-vertical.navbar-expand.navbar-right{left:auto;right:0}.navbar-vertical.navbar-expand .navbar-brand{padding:.75rem 0;justify-content:center}.navbar-vertical.navbar-expand .navbar-collapse{align-items:stretch}.navbar-vertical.navbar-expand .navbar-nav{flex-direction:column;flex-grow:1;min-height:auto}.navbar-vertical.navbar-expand .navbar-nav .nav-link{padding-top:.5rem;padding-bottom:.5rem}.navbar-vertical.navbar-expand>[class^=container]{flex-direction:column;align-items:stretch;min-height:100%;justify-content:flex-start;padding:0}.navbar-vertical.navbar-expand~.page{padding-left:15rem}.navbar-vertical.navbar-expand~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand.navbar-end~.page,.navbar-vertical.navbar-expand.navbar-right~.page{padding-left:0;padding-right:15rem}.navbar-vertical.navbar-expand .navbar-collapse{flex-direction:column}.navbar-vertical.navbar-expand .navbar-collapse [class^=container]{flex-direction:column;align-items:stretch;padding:0}.navbar-vertical.navbar-expand .navbar-collapse .navbar-nav{margin-left:0;margin-right:0}.navbar-vertical.navbar-expand .navbar-collapse .navbar-nav .nav-link{padding:.5rem calc(calc(var(--tblr-page-padding) * 2)/ 2);justify-content:flex-start}.navbar-vertical.navbar-expand .navbar-collapse .dropdown-menu-columns{flex-direction:column}.navbar-vertical.navbar-expand .navbar-collapse .dropdown-menu{padding:0;background:0 0;position:static;color:inherit;box-shadow:none;border:none;min-width:0;margin:0}.navbar-vertical.navbar-expand .navbar-collapse .dropdown-menu .dropdown-item{min-width:0;display:flex;width:auto;padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 1.75rem);color:inherit}.navbar-vertical.navbar-expand .navbar-collapse .dropdown-menu .dropdown-item.disabled{color:var(--tblr-disabled-color);pointer-events:none;background-color:transparent}.navbar-vertical.navbar-expand .navbar-collapse .dropdown-menu .dropdown-item.active,.navbar-vertical.navbar-expand .navbar-collapse .dropdown-menu .dropdown-item:active{background:var(--tblr-navbar-active-bg)}.navbar-vertical.navbar-expand .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 3.25rem)}.navbar-vertical.navbar-expand .navbar-collapse .dropdown-menu .dropdown-menu .dropdown-menu .dropdown-item{padding-left:calc(calc(calc(var(--tblr-page-padding) * 2)/ 2) + 4.75rem)}.navbar-vertical.navbar-expand .navbar-collapse .dropdown-toggle:after{margin-left:auto}.navbar-vertical.navbar-expand .navbar-collapse .nav-item.active:after{border-bottom-width:0;border-left-width:3px;right:auto;top:0;bottom:0}.navbar-overlap:after{content:"";height:9rem;position:absolute;top:100%;left:0;right:0;background:inherit;z-index:-1;box-shadow:inherit}.page{display:flex;flex-direction:column;position:relative;min-height:100%}.page-center{justify-content:center}.page-wrapper{flex:1;display:flex;flex-direction:column}@media print{.page-wrapper{margin:0!important}}.page-wrapper-full .page-body:first-child{margin:0;border-top:0}.page-body{margin-top:var(--tblr-page-padding-y);margin-bottom:var(--tblr-page-padding-y);display:flex;flex-direction:column;flex:1}.page-body-card{background:var(--tblr-bg-surface);border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent);padding:var(--tblr-page-padding) 0;margin-bottom:0;flex:1}.page-body~.page-body-card{margin-top:0}.page-cover{background:no-repeat center/cover;min-height:9rem}@media (min-width:768px){.page-cover{min-height:12rem}}@media (min-width:992px){.page-cover{min-height:15rem}}.page-cover-overlay{position:relative}.page-cover-overlay:after{content:"";position:absolute;top:0;left:0;right:0;bottom:0;background-image:linear-gradient(180deg,rgba(0,0,0,0) 0,rgba(0,0,0,.6) 100%)}.page-header{display:flex;flex-wrap:wrap;min-height:2.25rem;flex-direction:column;justify-content:center;max-width:100%}.page-wrapper .page-header{margin:var(--tblr-page-padding-y) 0 0}.page-header-border{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);padding:var(--tblr-page-padding-y) 0;margin:0!important;background-color:var(--tblr-bg-surface)}.page-pretitle{font-size:.75rem;font-weight:var(--tblr-font-weight-medium);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-secondary)}.page-title{margin:0;font-size:var(--tblr-font-size-h2);line-height:var(--tblr-line-height-h2);font-weight:var(--tblr-font-weight-headings);color:inherit;display:flex;align-items:center}.page-title svg{width:1.5rem;height:1.5rem;margin-right:.25rem}.page-title-lg{font-size:1.5rem;line-height:2rem}.page-subtitle{margin-top:.25rem;color:var(--tblr-secondary)}.page-cover{--tblr-page-cover-blur:20px;--tblr-page-cover-padding:1rem;min-height:6rem;padding:var(--tblr-page-cover-padding) 0;position:relative;overflow:hidden}.page-cover-img{position:absolute;top:calc(-2 * var(--tblr-page-cover-blur,0));left:calc(-2 * var(--tblr-page-cover-blur,0));right:calc(-2 * var(--tblr-page-cover-blur,0));bottom:calc(-2 * var(--tblr-page-cover-blur,0));pointer-events:none;filter:blur(var(--tblr-page-cover-blur));-o-object-fit:cover;object-fit:cover;background-size:cover;background-position:center;z-index:-1}.page-tabs{margin-top:.5rem;position:relative}.page-header-tabs .nav-bordered{border:0}.page-header-tabs+.page-body-card{margin-top:0}.footer{border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);background-color:var(--tblr-bg-surface);padding:2rem 0;color:var(--tblr-gray-500);margin-top:auto}.footer-transparent{background-color:transparent;border-top:0}:root:not(.theme-dark):not([data-bs-theme=dark]) .hide-theme-light{display:none!important}:root:not(.theme-dark):not([data-bs-theme=dark]) .img-dark{display:none!important}:root.theme-dark .hide-theme-dark,:root[data-bs-theme=dark] .hide-theme-dark,body[data-bs-theme=dark] [data-bs-theme=light]:root .hide-theme-dark{display:none!important}:root.theme-dark .img-light,:root[data-bs-theme=dark] .img-light,body[data-bs-theme=dark] [data-bs-theme=light]:root .img-light{display:none!important}[data-bs-theme=dark],body[data-bs-theme=dark] [data-bs-theme=light]{color-scheme:dark;--tblr-body-color:var(--tblr-gray-200);--tblr-secondary:var(--tblr-gray-400);--tblr-body-bg:var(--tblr-gray-900);--tblr-emphasis-color:#ffffff;--tblr-emphasis-color-rgb:255,255,255;--tblr-bg-forms:var(--tblr-gray-900);--tblr-bg-surface:var(--tblr-gray-800);--tblr-bg-surface-inverted:var(--tblr-gray-100);--tblr-bg-surface-secondary:var(--tblr-gray-900);--tblr-bg-surface-tertiary:var(--tblr-gray-800);--tblr-text-inverted:var(--tblr-gray-800);--tblr-link-color:var(--tblr-primary);--tblr-link-hover-color:color-mix(in srgb, var(--tblr-primary), black 20%);--tblr-active-bg:rgb(34.676744186, 45.8627906977, 61.523255814);--tblr-disabled-color:color-mix(in srgb, var(--tblr-body-color) 40%, transparent);--tblr-border-color:var(--tblr-gray-700);--tblr-border-color-translucent:var(--tblr-dark-mode-border-color-translucent);--tblr-border-dark-color:var(--tblr-dark-mode-border-dark-color);--tblr-border-active-color:var(--tblr-dark-mode-border-active-color);--tblr-btn-color:rgb(27.323255814, 36.1372093023, 48.476744186)}[data-bs-theme=dark] .navbar-brand-autodark .navbar-brand-image{filter:brightness(0) invert(1)}.accordion{--tblr-accordion-color:var(--tblr-body-color);--tblr-accordion-border-color:var(--tblr-border-color);--tblr-accordion-border-radius:var(--tblr-border-radius);--tblr-accordion-inner-border-radius:calc(var(--tblr-border-radius) - (var(--tblr-border-width)));--tblr-accordion-padding-x:1.25rem;--tblr-accordion-gap:0;--tblr-accordion-active-color:inherit;--tblr-accordion-btn-color:var(--tblr-accordion-color);--tblr-accordion-btn-bg:transparent;--tblr-accordion-btn-toggle-width:1.25rem;--tblr-accordion-btn-padding-x:var(--tblr-accordion-padding-x);--tblr-accordion-btn-padding-y:1rem;--tblr-accordion-btn-font-weight:var(--tblr-font-weight-medium);--tblr-accordion-body-padding-x:var(--tblr-accordion-padding-x);--tblr-accordion-body-padding-y:1rem;display:flex;flex-direction:column;gap:var(--tblr-accordion-gap)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--tblr-accordion-btn-padding-y) var(--tblr-accordion-padding-x);color:inherit;text-align:inherit;background-color:transparent;border:0;font-size:inherit;font-weight:var(--tblr-accordion-btn-font-weight);gap:.75rem}.accordion-button:not(.collapsed){border-bottom-color:transparent;box-shadow:none;color:var(--tblr-accordion-active-color)}.accordion-header{margin:0;position:relative;display:flex;gap:1rem;align-items:center;width:100%;color:var(--tblr-accordion-btn-color);text-align:left;background-color:transparent;border:0;overflow-anchor:none;transition:transform .3s}.accordion-header:hover{z-index:2}.accordion-header:focus{z-index:3;outline:0;box-shadow:var(--tblr-accordion-btn-focus-box-shadow)}.accordion-header:focus:not(:focus-visible){outline:0;box-shadow:none}.accordion-button-icon{color:var(--tblr-secondary)}.accordion-button-toggle{display:flex;line-height:1;transition:.3s transform;margin-left:auto;margin-right:0;color:var(--tblr-secondary);width:var(--tblr-accordion-btn-toggle-width);height:var(--tblr-accordion-btn-toggle-width)}.accordion-button:not(.collapsed) .accordion-button-toggle{transform:rotate(-180deg);color:var(--tblr-accordion-active-color)}.accordion-button-toggle path{transition:.3s opacity}.accordion-button:not(.collapsed) .accordion-button-toggle-plus path:first-child{opacity:0}.accordion-item{color:var(--tblr-accordion-color);border:var(--tblr-border-width) solid var(--tblr-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--tblr-accordion-border-radius);border-top-right-radius:var(--tblr-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header{border-top-left-radius:var(--tblr-accordion-inner-border-radius);border-top-right-radius:var(--tblr-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--tblr-accordion-border-radius);border-bottom-left-radius:var(--tblr-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header.collapsed{border-bottom-right-radius:var(--tblr-accordion-inner-border-radius);border-bottom-left-radius:var(--tblr-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--tblr-accordion-border-radius);border-bottom-left-radius:var(--tblr-accordion-border-radius)}.accordion-body{color:var(--tblr-secondary);padding:0 var(--tblr-accordion-body-padding-x) var(--tblr-accordion-body-padding-y)}.accordion-flush>.accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}.accordion-flush>.accordion-item>.accordion-collapse,.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}.accordion-tabs{--tblr-accordion-gap:0.75rem}.accordion-tabs>.accordion-item{border:var(--tblr-border-width) solid var(--tblr-accordion-border-color);border-radius:var(--tblr-accordion-border-radius)}.accordion-inverted .accordion-button-toggle{order:-1;margin-left:0}.alert{--tblr-alert-color:var(--tblr-body-color);--tblr-alert-bg:color-mix(in srgb, var(--tblr-alert-color) 10%, transparent);--tblr-alert-padding-x:1rem;--tblr-alert-padding-y:0.75rem;--tblr-alert-margin-bottom:1rem;--tblr-alert-border-color:color-mix(in srgb, var(--tblr-alert-color) 20%, transparent);--tblr-alert-border:var(--tblr-border-width) solid var(--tblr-alert-border-color);--tblr-alert-border-radius:var(--tblr-border-radius);--tblr-alert-link-color:inherit;--tblr-alert-heading-font-weight:var(--tblr-font-weight-medium);position:relative;padding:var(--tblr-alert-padding-y) var(--tblr-alert-padding-x);margin-bottom:var(--tblr-alert-margin-bottom);background-color:color-mix(in srgb,var(--tblr-alert-bg),var(--tblr-bg-surface));border-radius:var(--tblr-alert-border-radius);border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-alert-border-color);display:flex;flex-direction:row;gap:1rem}.alert-heading{color:inherit;margin-bottom:.25rem;font-weight:var(--tblr-alert-heading-font-weight)}.alert-description{color:var(--tblr-secondary)}.alert-icon{color:var(--tblr-alert-color);width:1.25rem!important;height:1.25rem!important}.alert-action{color:var(--tblr-alert-color);text-decoration:underline}.alert-action:hover{text-decoration:none}.alert-list{margin:0}.alert-link{font-weight:var(--tblr-font-weight-bold);color:var(--tblr-alert-link-color)}.alert-link,.alert-link:hover{color:var(--tblr-alert-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:calc(var(--tblr-alert-padding-x)/ 2 - 1px);right:calc(var(--tblr-alert-padding-y)/ 2 - 1px);z-index:1;padding:calc(var(--tblr-alert-padding-y) * 1.25) var(--tblr-alert-padding-x)}.alert-important{border-color:var(--tblr-alert-color);background-color:var(--tblr-alert-color);color:var(--tblr-white)}.alert-important .alert-description{color:inherit}.alert-important .alert-icon{color:inherit}.alert-minor{background:0 0;border-color:var(--tblr-border-color)}.alert-primary{--tblr-alert-color:var(--tblr-primary)}.alert-secondary{--tblr-alert-color:var(--tblr-secondary)}.alert-success{--tblr-alert-color:var(--tblr-success)}.alert-info{--tblr-alert-color:var(--tblr-info)}.alert-warning{--tblr-alert-color:var(--tblr-warning)}.alert-danger{--tblr-alert-color:var(--tblr-danger)}.alert-light{--tblr-alert-color:var(--tblr-light)}.alert-dark{--tblr-alert-color:var(--tblr-dark)}.alert-muted{--tblr-alert-color:var(--tblr-muted)}.alert-blue{--tblr-alert-color:var(--tblr-blue)}.alert-azure{--tblr-alert-color:var(--tblr-azure)}.alert-indigo{--tblr-alert-color:var(--tblr-indigo)}.alert-purple{--tblr-alert-color:var(--tblr-purple)}.alert-pink{--tblr-alert-color:var(--tblr-pink)}.alert-red{--tblr-alert-color:var(--tblr-red)}.alert-orange{--tblr-alert-color:var(--tblr-orange)}.alert-yellow{--tblr-alert-color:var(--tblr-yellow)}.alert-lime{--tblr-alert-color:var(--tblr-lime)}.alert-green{--tblr-alert-color:var(--tblr-green)}.alert-teal{--tblr-alert-color:var(--tblr-teal)}.alert-cyan{--tblr-alert-color:var(--tblr-cyan)}.avatar{--tblr-avatar-size:var(--tblr-avatar-list-size, 2.5rem);--tblr-avatar-status-size:0.75rem;--tblr-avatar-bg:var(--tblr-bg-surface-secondary);--tblr-avatar-box-shadow-color:var(--tblr-border-color-translucent);--tblr-avatar-box-shadow:inset 0 0 0 1px var(--tblr-avatar-box-shadow-color);--tblr-avatar-font-size:1rem;--tblr-avatar-icon-size:1.5rem;--tblr-avatar-brand-size:1.25rem;position:relative;width:var(--tblr-avatar-size);height:var(--tblr-avatar-size);font-size:var(--tblr-avatar-font-size);font-weight:var(--tblr-font-weight-medium);line-height:1;display:inline-flex;align-items:center;justify-content:center;color:var(--tblr-secondary);text-align:center;text-transform:uppercase;vertical-align:bottom;-webkit-user-select:none;-moz-user-select:none;user-select:none;background:var(--tblr-avatar-bg) no-repeat center/cover;border-radius:var(--tblr-border-radius);box-shadow:var(--tblr-avatar-box-shadow);transition:color .3s,background-color .3s,box-shadow .3s}.avatar .icon{width:var(--tblr-avatar-icon-size);height:var(--tblr-avatar-icon-size)}.avatar .badge{position:absolute;right:0;bottom:0;border-radius:100rem;box-shadow:0 0 0 calc(var(--tblr-avatar-status-size)/ 4) var(--tblr-bg-surface)}a.avatar{cursor:pointer}a.avatar:hover{color:var(--tblr-primary);--tblr-avatar-box-shadow-color:var(--tblr-primary)}.avatar-rounded{border-radius:100rem}.avatar-xxs{--tblr-avatar-size:1rem;--tblr-avatar-status-size:0.25rem;--tblr-avatar-font-size:0.5rem;--tblr-avatar-icon-size:0.5rem;--tblr-avatar-brand-size:0.5rem}.avatar-xxs .badge:empty{width:.25rem;height:.25rem}.avatar-xs{--tblr-avatar-size:1.25rem;--tblr-avatar-status-size:0.375rem;--tblr-avatar-font-size:0.625rem;--tblr-avatar-icon-size:0.75rem;--tblr-avatar-brand-size:0.75rem}.avatar-xs .badge:empty{width:.375rem;height:.375rem}.avatar-sm{--tblr-avatar-size:2rem;--tblr-avatar-status-size:0.5rem;--tblr-avatar-font-size:0.75rem;--tblr-avatar-icon-size:1.5rem;--tblr-avatar-brand-size:1rem}.avatar-sm .badge:empty{width:.5rem;height:.5rem}.avatar-md{--tblr-avatar-size:2.5rem;--tblr-avatar-status-size:0.75rem;--tblr-avatar-font-size:0.875rem;--tblr-avatar-icon-size:1.5rem;--tblr-avatar-brand-size:1.25rem}.avatar-md .badge:empty{width:.75rem;height:.75rem}.avatar-lg{--tblr-avatar-size:3rem;--tblr-avatar-status-size:0.75rem;--tblr-avatar-font-size:1.25rem;--tblr-avatar-icon-size:2rem;--tblr-avatar-brand-size:1.25rem}.avatar-lg .badge:empty{width:.75rem;height:.75rem}.avatar-xl{--tblr-avatar-size:5rem;--tblr-avatar-status-size:1rem;--tblr-avatar-font-size:2rem;--tblr-avatar-icon-size:3rem;--tblr-avatar-brand-size:1.25rem}.avatar-xl .badge:empty{width:1rem;height:1rem}.avatar-2xl{--tblr-avatar-size:7rem;--tblr-avatar-status-size:1rem;--tblr-avatar-font-size:3rem;--tblr-avatar-icon-size:5rem;--tblr-avatar-brand-size:2rem}.avatar-2xl .badge:empty{width:1rem;height:1rem}.avatar-list{--tblr-avatar-list-size:2.5rem;--tblr-list-gap:0.5rem;display:flex;flex-wrap:wrap;gap:var(--tblr-list-gap)}.avatar-list a.avatar:hover{z-index:1}.avatar-list-stacked{display:block;--tblr-list-gap:0}.avatar-list-stacked .avatar{margin-right:calc(-.5 * var(--tblr-avatar-size))!important;box-shadow:var(--tblr-avatar-box-shadow),0 0 0 2px var(--tblr-card-bg,var(--tblr-bg-surface))}.avatar-list-xxs{--tblr-avatar-list-size:1rem}.avatar-list-xs{--tblr-avatar-list-size:1.25rem}.avatar-list-sm{--tblr-avatar-list-size:2rem}.avatar-list-md{--tblr-avatar-list-size:2.5rem}.avatar-list-lg{--tblr-avatar-list-size:3rem}.avatar-list-xl{--tblr-avatar-list-size:5rem}.avatar-list-2xl{--tblr-avatar-list-size:7rem}.avatar-upload{border:var(--tblr-border-width) dashed var(--tblr-border-color);background:var(--tblr-bg-forms);box-shadow:none;flex-direction:column;transition:color .3s,background-color .3s}@media (prefers-reduced-motion:reduce){.avatar-upload{transition:none}}.avatar-upload svg{width:1.5rem;height:1.5rem;stroke-width:1}.avatar-upload:hover{border-color:var(--tblr-primary);color:var(--tblr-primary);text-decoration:none}.avatar-upload-text{font-size:.625rem;line-height:1;margin-top:.25rem}.avatar-cover{margin-top:calc(-.5 * var(--tblr-avatar-size));box-shadow:0 0 0 .25rem var(--tblr-card-bg,var(--tblr-body-bg))}.avatar-brand{width:var(--tblr-avatar-brand-size);height:var(--tblr-avatar-brand-size);position:absolute;right:-2px;bottom:-2px;z-index:1000;background:var(--tblr-bg-surface);border-radius:var(--tblr-border-radius);border:1px solid var(--tblr-border-color)}.badge{--tblr-badge-padding-x:0.5em;--tblr-badge-padding-y:0.25em;--tblr-badge-font-size:0.85714285em;--tblr-badge-font-weight:var(--tblr-font-weight-medium);--tblr-badge-color:var(--tblr-secondary);--tblr-badge-border-radius:var(--tblr-border-radius);--tblr-badge-icon-size:1em;--tblr-badge-line-height:1;display:inline-flex;padding:var(--tblr-badge-padding-y) var(--tblr-badge-padding-x);font-weight:var(--tblr-badge-font-weight);font-size:var(--tblr-badge-font-size);color:var(--tblr-badge-color);text-align:center;white-space:nowrap;justify-content:center;align-items:center;gap:.25rem;background:var(--tblr-bg-surface-secondary);overflow:hidden;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--tblr-border-width) var(--tblr-border-style) transparent;border-radius:var(--tblr-badge-border-radius);min-width:calc(1em + var(--tblr-badge-padding-y) * 2 + 2px);letter-spacing:.04em;vertical-align:bottom;line-height:var(--tblr-badge-line-height)}a.badge{background:var(--tblr-bg-surface-secondary)}.badge .icon{width:1em;height:1em;font-size:var(--tblr-badge-icon-size);stroke-width:2}.badge-dot,.badge:empty{display:inline-block;width:10px;height:10px;min-width:0;min-height:auto;padding:0;border-radius:100rem;vertical-align:baseline}.badge-outline{background-color:transparent;border:var(--tblr-border-width) var(--tblr-border-style) currentColor}.badge-pill{border-radius:100rem}.badges-list{--tblr-list-gap:0.5rem;display:flex;flex-wrap:wrap;gap:var(--tblr-list-gap)}.badge-notification{position:absolute!important;top:0!important;right:0!important;transform:translate(50%,-50%);z-index:1}.badge-blink{animation:blink 2s infinite}.badge-sm{--tblr-badge-font-size:0.71428571em;--tblr-badge-icon-size:1em;--tblr-badge-padding-y:2px;--tblr-badge-padding-x:0.25rem}.badge-lg{--tblr-badge-font-size:1em;--tblr-badge-icon-size:1em;--tblr-badge-padding-y:0.25rem;--tblr-badge-padding-x:0.5rem}.badge-icononly{--tblr-badge-padding-x:0}.breadcrumb{--tblr-breadcrumb-padding-x:0;--tblr-breadcrumb-padding-y:0;--tblr-breadcrumb-margin-bottom:1rem;--tblr-breadcrumb-font-size: ;--tblr-breadcrumb-bg: ;--tblr-breadcrumb-border-radius: ;--tblr-breadcrumb-divider-color:var(--tblr-gray-500);--tblr-breadcrumb-item-padding-x:0.5rem;--tblr-breadcrumb-item-active-color:inherit;--tblr-breadcrumb-item-active-font-weight:var(--tblr-font-weight-bold);--tblr-breadcrumb-item-disabled-color:var(--tblr-disabled-color);--tblr-breadcrumb-link-color:var(--tblr-link-color);display:flex;flex-wrap:wrap;font-size:var(--tblr-breadcrumb-font-size);list-style:none;background-color:var(--tblr-breadcrumb-bg);border-radius:var(--tblr-breadcrumb-border-radius);padding:0;margin:0;background:0 0}.breadcrumb a{color:var(--tblr-breadcrumb-link-color)}.breadcrumb a:hover{text-decoration:underline}.breadcrumb-muted{--tblr-breadcrumb-link-color:var(--tblr-secondary)}.breadcrumb-item.active{color:var(--tblr-breadcrumb-item-active-color);font-weight:var(--tblr-breadcrumb-item-active-font-weight)}.breadcrumb-item.active a{color:inherit;pointer-events:none}.breadcrumb-item.disabled{color:var(--tblr-breadcrumb-item-disabled-color)}.breadcrumb-item.disabled:before{color:inherit}.breadcrumb-item.disabled a{color:inherit;pointer-events:none}.breadcrumb-item+.breadcrumb-item{padding-left:var(--tblr-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--tblr-breadcrumb-item-padding-x);color:var(--tblr-breadcrumb-divider-color);content:var(--tblr-breadcrumb-divider, "/")}.breadcrumb-dots{--tblr-breadcrumb-divider:"·"}.breadcrumb-arrows{--tblr-breadcrumb-divider:"›"}.breadcrumb-bullets{--tblr-breadcrumb-divider:"•"}.btn{--tblr-btn-icon-size:1.25rem;--tblr-btn-icon-color:inherit;--tblr-btn-bg:var(--tblr-bg-surface);--tblr-btn-color:var(--tblr-body-color);--tblr-btn-border-color:var(--tblr-border-color);--tblr-btn-hover-bg:var(--tblr-btn-bg);--tblr-btn-hover-border-color:var(--tblr-border-active-color);--tblr-btn-active-color:var(--tblr-primary);--tblr-btn-active-bg:rgba(var(--tblr-primary-rgb), 0.04);--tblr-btn-active-border-color:var(--tblr-primary);display:inline-flex;align-items:center;justify-content:center;white-space:nowrap;box-shadow:var(--tblr-btn-box-shadow);position:relative;min-width:calc(var(--tblr-btn-line-height) * 1 + var(--tblr-btn-padding-y) * 2 + var(--tblr-btn-border-width) * 2);min-height:calc(var(--tblr-btn-line-height) * 1 + var(--tblr-btn-padding-y) * 2 + var(--tblr-btn-border-width) * 2)}.btn .icon{width:var(--tblr-btn-icon-size);height:var(--tblr-btn-icon-size);min-width:var(--tblr-btn-icon-size);font-size:var(--tblr-btn-icon-size);margin:0 calc(var(--tblr-btn-padding-x)/ 2) 0 calc(var(--tblr-btn-padding-x)/ -4);vertical-align:bottom;color:var(--tblr-btn-icon-color)}.btn .avatar{width:var(--tblr-btn-icon-size);height:var(--tblr-btn-icon-size);margin:0 calc(var(--tblr-btn-padding-x)/ 2) 0 calc(var(--tblr-btn-padding-x)/ -4)}.btn .icon-end,.btn .icon-right{margin:0 calc(var(--tblr-btn-padding-x)/ -4) 0 calc(var(--tblr-btn-padding-x)/ 2)}.btn .badge{top:auto}.btn-check+.btn:hover{color:var(--tblr-btn-hover-color);background-color:var(--tblr-btn-hover-bg);border-color:var(--tblr-btn-hover-border-color)}.btn-link{color:rgb(6.711627907,124.1651162791,233.788372093);background-color:transparent;border-color:transparent;box-shadow:none}.btn-link .icon{color:inherit}.btn-link:hover{color:rgb(4.8,88.8,167.2);border-color:transparent}.btn-primary{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-primary-fg, #ffffff);--tblr-btn-bg:var(--tblr-primary);--tblr-btn-hover-color:var(--tblr-primary-fg);--tblr-btn-hover-bg:var(--tblr-primary-darken);--tblr-btn-active-color:var(--tblr-primary-fg);--tblr-btn-active-bg:var(--tblr-primary-darken);--tblr-btn-disabled-bg:var(--tblr-primary);--tblr-btn-disabled-color:var(--tblr-primary-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-primary,.btn-outline.btn-primary{--tblr-btn-color:var(--tblr-primary);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-primary);--tblr-btn-hover-color:var(--tblr-primary-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-primary);--tblr-btn-active-color:var(--tblr-primary-fg);--tblr-btn-active-bg:var(--tblr-primary);--tblr-btn-active-border-color:var(--tblr-primary);--tblr-btn-disabled-color:var(--tblr-primary);--tblr-btn-disabled-border-color:var(--tblr-primary)}.btn-ghost-primary,.btn-ghost.btn-primary{--tblr-btn-color:var(--tblr-primary);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-primary-fg);--tblr-btn-hover-bg:var(--tblr-primary);--tblr-btn-hover-border-color:var(--tblr-primary);--tblr-btn-active-color:var(--tblr-primary-fg);--tblr-btn-active-bg:var(--tblr-primary);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-primary);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-secondary{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-secondary-fg, #ffffff);--tblr-btn-bg:var(--tblr-secondary);--tblr-btn-hover-color:var(--tblr-secondary-fg);--tblr-btn-hover-bg:var(--tblr-secondary-darken);--tblr-btn-active-color:var(--tblr-secondary-fg);--tblr-btn-active-bg:var(--tblr-secondary-darken);--tblr-btn-disabled-bg:var(--tblr-secondary);--tblr-btn-disabled-color:var(--tblr-secondary-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-secondary,.btn-outline.btn-secondary{--tblr-btn-color:var(--tblr-secondary);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-secondary);--tblr-btn-hover-color:var(--tblr-secondary-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-secondary);--tblr-btn-active-color:var(--tblr-secondary-fg);--tblr-btn-active-bg:var(--tblr-secondary);--tblr-btn-active-border-color:var(--tblr-secondary);--tblr-btn-disabled-color:var(--tblr-secondary);--tblr-btn-disabled-border-color:var(--tblr-secondary)}.btn-ghost-secondary,.btn-ghost.btn-secondary{--tblr-btn-color:var(--tblr-secondary);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-secondary-fg);--tblr-btn-hover-bg:var(--tblr-secondary);--tblr-btn-hover-border-color:var(--tblr-secondary);--tblr-btn-active-color:var(--tblr-secondary-fg);--tblr-btn-active-bg:var(--tblr-secondary);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-secondary);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-success{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-success-fg, #ffffff);--tblr-btn-bg:var(--tblr-success);--tblr-btn-hover-color:var(--tblr-success-fg);--tblr-btn-hover-bg:var(--tblr-success-darken);--tblr-btn-active-color:var(--tblr-success-fg);--tblr-btn-active-bg:var(--tblr-success-darken);--tblr-btn-disabled-bg:var(--tblr-success);--tblr-btn-disabled-color:var(--tblr-success-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-success,.btn-outline.btn-success{--tblr-btn-color:var(--tblr-success);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-success);--tblr-btn-hover-color:var(--tblr-success-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-success);--tblr-btn-active-color:var(--tblr-success-fg);--tblr-btn-active-bg:var(--tblr-success);--tblr-btn-active-border-color:var(--tblr-success);--tblr-btn-disabled-color:var(--tblr-success);--tblr-btn-disabled-border-color:var(--tblr-success)}.btn-ghost-success,.btn-ghost.btn-success{--tblr-btn-color:var(--tblr-success);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-success-fg);--tblr-btn-hover-bg:var(--tblr-success);--tblr-btn-hover-border-color:var(--tblr-success);--tblr-btn-active-color:var(--tblr-success-fg);--tblr-btn-active-bg:var(--tblr-success);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-success);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-info{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-info-fg, #ffffff);--tblr-btn-bg:var(--tblr-info);--tblr-btn-hover-color:var(--tblr-info-fg);--tblr-btn-hover-bg:var(--tblr-info-darken);--tblr-btn-active-color:var(--tblr-info-fg);--tblr-btn-active-bg:var(--tblr-info-darken);--tblr-btn-disabled-bg:var(--tblr-info);--tblr-btn-disabled-color:var(--tblr-info-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-info,.btn-outline.btn-info{--tblr-btn-color:var(--tblr-info);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-info);--tblr-btn-hover-color:var(--tblr-info-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-info);--tblr-btn-active-color:var(--tblr-info-fg);--tblr-btn-active-bg:var(--tblr-info);--tblr-btn-active-border-color:var(--tblr-info);--tblr-btn-disabled-color:var(--tblr-info);--tblr-btn-disabled-border-color:var(--tblr-info)}.btn-ghost-info,.btn-ghost.btn-info{--tblr-btn-color:var(--tblr-info);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-info-fg);--tblr-btn-hover-bg:var(--tblr-info);--tblr-btn-hover-border-color:var(--tblr-info);--tblr-btn-active-color:var(--tblr-info-fg);--tblr-btn-active-bg:var(--tblr-info);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-info);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-warning{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-warning-fg, #ffffff);--tblr-btn-bg:var(--tblr-warning);--tblr-btn-hover-color:var(--tblr-warning-fg);--tblr-btn-hover-bg:var(--tblr-warning-darken);--tblr-btn-active-color:var(--tblr-warning-fg);--tblr-btn-active-bg:var(--tblr-warning-darken);--tblr-btn-disabled-bg:var(--tblr-warning);--tblr-btn-disabled-color:var(--tblr-warning-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-warning,.btn-outline.btn-warning{--tblr-btn-color:var(--tblr-warning);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-warning);--tblr-btn-hover-color:var(--tblr-warning-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-warning);--tblr-btn-active-color:var(--tblr-warning-fg);--tblr-btn-active-bg:var(--tblr-warning);--tblr-btn-active-border-color:var(--tblr-warning);--tblr-btn-disabled-color:var(--tblr-warning);--tblr-btn-disabled-border-color:var(--tblr-warning)}.btn-ghost-warning,.btn-ghost.btn-warning{--tblr-btn-color:var(--tblr-warning);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-warning-fg);--tblr-btn-hover-bg:var(--tblr-warning);--tblr-btn-hover-border-color:var(--tblr-warning);--tblr-btn-active-color:var(--tblr-warning-fg);--tblr-btn-active-bg:var(--tblr-warning);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-warning);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-danger{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-danger-fg, #ffffff);--tblr-btn-bg:var(--tblr-danger);--tblr-btn-hover-color:var(--tblr-danger-fg);--tblr-btn-hover-bg:var(--tblr-danger-darken);--tblr-btn-active-color:var(--tblr-danger-fg);--tblr-btn-active-bg:var(--tblr-danger-darken);--tblr-btn-disabled-bg:var(--tblr-danger);--tblr-btn-disabled-color:var(--tblr-danger-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-danger,.btn-outline.btn-danger{--tblr-btn-color:var(--tblr-danger);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-danger);--tblr-btn-hover-color:var(--tblr-danger-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-danger);--tblr-btn-active-color:var(--tblr-danger-fg);--tblr-btn-active-bg:var(--tblr-danger);--tblr-btn-active-border-color:var(--tblr-danger);--tblr-btn-disabled-color:var(--tblr-danger);--tblr-btn-disabled-border-color:var(--tblr-danger)}.btn-ghost-danger,.btn-ghost.btn-danger{--tblr-btn-color:var(--tblr-danger);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-danger-fg);--tblr-btn-hover-bg:var(--tblr-danger);--tblr-btn-hover-border-color:var(--tblr-danger);--tblr-btn-active-color:var(--tblr-danger-fg);--tblr-btn-active-bg:var(--tblr-danger);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-danger);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-light{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-light-fg, #ffffff);--tblr-btn-bg:var(--tblr-light);--tblr-btn-hover-color:var(--tblr-light-fg);--tblr-btn-hover-bg:var(--tblr-light-darken);--tblr-btn-active-color:var(--tblr-light-fg);--tblr-btn-active-bg:var(--tblr-light-darken);--tblr-btn-disabled-bg:var(--tblr-light);--tblr-btn-disabled-color:var(--tblr-light-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-light,.btn-outline.btn-light{--tblr-btn-color:var(--tblr-light);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-light);--tblr-btn-hover-color:var(--tblr-light-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-light);--tblr-btn-active-color:var(--tblr-light-fg);--tblr-btn-active-bg:var(--tblr-light);--tblr-btn-active-border-color:var(--tblr-light);--tblr-btn-disabled-color:var(--tblr-light);--tblr-btn-disabled-border-color:var(--tblr-light)}.btn-ghost-light,.btn-ghost.btn-light{--tblr-btn-color:var(--tblr-light);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-light-fg);--tblr-btn-hover-bg:var(--tblr-light);--tblr-btn-hover-border-color:var(--tblr-light);--tblr-btn-active-color:var(--tblr-light-fg);--tblr-btn-active-bg:var(--tblr-light);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-light);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-dark{--tblr-btn-border-color:var(--tblr-dark-mode-border-color);--tblr-btn-hover-border-color:var(--tblr-dark-mode-border-active-color);--tblr-btn-active-border-color:var(--tblr-dark-mode-border-active-color);--tblr-btn-color:var(--tblr-dark-fg, #ffffff);--tblr-btn-bg:var(--tblr-dark);--tblr-btn-hover-color:var(--tblr-dark-fg);--tblr-btn-hover-bg:var(--tblr-dark-darken);--tblr-btn-active-color:var(--tblr-dark-fg);--tblr-btn-active-bg:var(--tblr-dark-darken);--tblr-btn-disabled-bg:var(--tblr-dark);--tblr-btn-disabled-color:var(--tblr-dark-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-dark,.btn-outline.btn-dark{--tblr-btn-color:var(--tblr-dark);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-dark);--tblr-btn-hover-color:var(--tblr-dark-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-dark);--tblr-btn-active-color:var(--tblr-dark-fg);--tblr-btn-active-bg:var(--tblr-dark);--tblr-btn-active-border-color:var(--tblr-dark);--tblr-btn-disabled-color:var(--tblr-dark);--tblr-btn-disabled-border-color:var(--tblr-dark)}.btn-ghost-dark,.btn-ghost.btn-dark{--tblr-btn-color:var(--tblr-dark);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-dark-fg);--tblr-btn-hover-bg:var(--tblr-dark);--tblr-btn-hover-border-color:var(--tblr-dark);--tblr-btn-active-color:var(--tblr-dark-fg);--tblr-btn-active-bg:var(--tblr-dark);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-dark);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-muted{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-muted-fg, #ffffff);--tblr-btn-bg:var(--tblr-muted);--tblr-btn-hover-color:var(--tblr-muted-fg);--tblr-btn-hover-bg:var(--tblr-muted-darken);--tblr-btn-active-color:var(--tblr-muted-fg);--tblr-btn-active-bg:var(--tblr-muted-darken);--tblr-btn-disabled-bg:var(--tblr-muted);--tblr-btn-disabled-color:var(--tblr-muted-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-muted,.btn-outline.btn-muted{--tblr-btn-color:var(--tblr-muted);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-muted);--tblr-btn-hover-color:var(--tblr-muted-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-muted);--tblr-btn-active-color:var(--tblr-muted-fg);--tblr-btn-active-bg:var(--tblr-muted);--tblr-btn-active-border-color:var(--tblr-muted);--tblr-btn-disabled-color:var(--tblr-muted);--tblr-btn-disabled-border-color:var(--tblr-muted)}.btn-ghost-muted,.btn-ghost.btn-muted{--tblr-btn-color:var(--tblr-muted);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-muted-fg);--tblr-btn-hover-bg:var(--tblr-muted);--tblr-btn-hover-border-color:var(--tblr-muted);--tblr-btn-active-color:var(--tblr-muted-fg);--tblr-btn-active-bg:var(--tblr-muted);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-muted);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-blue{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-blue-fg, #ffffff);--tblr-btn-bg:var(--tblr-blue);--tblr-btn-hover-color:var(--tblr-blue-fg);--tblr-btn-hover-bg:var(--tblr-blue-darken);--tblr-btn-active-color:var(--tblr-blue-fg);--tblr-btn-active-bg:var(--tblr-blue-darken);--tblr-btn-disabled-bg:var(--tblr-blue);--tblr-btn-disabled-color:var(--tblr-blue-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-blue,.btn-outline.btn-blue{--tblr-btn-color:var(--tblr-blue);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-blue);--tblr-btn-hover-color:var(--tblr-blue-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-blue);--tblr-btn-active-color:var(--tblr-blue-fg);--tblr-btn-active-bg:var(--tblr-blue);--tblr-btn-active-border-color:var(--tblr-blue);--tblr-btn-disabled-color:var(--tblr-blue);--tblr-btn-disabled-border-color:var(--tblr-blue)}.btn-ghost-blue,.btn-ghost.btn-blue{--tblr-btn-color:var(--tblr-blue);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-blue-fg);--tblr-btn-hover-bg:var(--tblr-blue);--tblr-btn-hover-border-color:var(--tblr-blue);--tblr-btn-active-color:var(--tblr-blue-fg);--tblr-btn-active-bg:var(--tblr-blue);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-blue);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-azure{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-azure-fg, #ffffff);--tblr-btn-bg:var(--tblr-azure);--tblr-btn-hover-color:var(--tblr-azure-fg);--tblr-btn-hover-bg:var(--tblr-azure-darken);--tblr-btn-active-color:var(--tblr-azure-fg);--tblr-btn-active-bg:var(--tblr-azure-darken);--tblr-btn-disabled-bg:var(--tblr-azure);--tblr-btn-disabled-color:var(--tblr-azure-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-azure,.btn-outline.btn-azure{--tblr-btn-color:var(--tblr-azure);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-azure);--tblr-btn-hover-color:var(--tblr-azure-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-azure);--tblr-btn-active-color:var(--tblr-azure-fg);--tblr-btn-active-bg:var(--tblr-azure);--tblr-btn-active-border-color:var(--tblr-azure);--tblr-btn-disabled-color:var(--tblr-azure);--tblr-btn-disabled-border-color:var(--tblr-azure)}.btn-ghost-azure,.btn-ghost.btn-azure{--tblr-btn-color:var(--tblr-azure);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-azure-fg);--tblr-btn-hover-bg:var(--tblr-azure);--tblr-btn-hover-border-color:var(--tblr-azure);--tblr-btn-active-color:var(--tblr-azure-fg);--tblr-btn-active-bg:var(--tblr-azure);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-azure);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-indigo{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-indigo-fg, #ffffff);--tblr-btn-bg:var(--tblr-indigo);--tblr-btn-hover-color:var(--tblr-indigo-fg);--tblr-btn-hover-bg:var(--tblr-indigo-darken);--tblr-btn-active-color:var(--tblr-indigo-fg);--tblr-btn-active-bg:var(--tblr-indigo-darken);--tblr-btn-disabled-bg:var(--tblr-indigo);--tblr-btn-disabled-color:var(--tblr-indigo-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-indigo,.btn-outline.btn-indigo{--tblr-btn-color:var(--tblr-indigo);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-indigo);--tblr-btn-hover-color:var(--tblr-indigo-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-indigo);--tblr-btn-active-color:var(--tblr-indigo-fg);--tblr-btn-active-bg:var(--tblr-indigo);--tblr-btn-active-border-color:var(--tblr-indigo);--tblr-btn-disabled-color:var(--tblr-indigo);--tblr-btn-disabled-border-color:var(--tblr-indigo)}.btn-ghost-indigo,.btn-ghost.btn-indigo{--tblr-btn-color:var(--tblr-indigo);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-indigo-fg);--tblr-btn-hover-bg:var(--tblr-indigo);--tblr-btn-hover-border-color:var(--tblr-indigo);--tblr-btn-active-color:var(--tblr-indigo-fg);--tblr-btn-active-bg:var(--tblr-indigo);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-indigo);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-purple{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-purple-fg, #ffffff);--tblr-btn-bg:var(--tblr-purple);--tblr-btn-hover-color:var(--tblr-purple-fg);--tblr-btn-hover-bg:var(--tblr-purple-darken);--tblr-btn-active-color:var(--tblr-purple-fg);--tblr-btn-active-bg:var(--tblr-purple-darken);--tblr-btn-disabled-bg:var(--tblr-purple);--tblr-btn-disabled-color:var(--tblr-purple-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-purple,.btn-outline.btn-purple{--tblr-btn-color:var(--tblr-purple);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-purple);--tblr-btn-hover-color:var(--tblr-purple-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-purple);--tblr-btn-active-color:var(--tblr-purple-fg);--tblr-btn-active-bg:var(--tblr-purple);--tblr-btn-active-border-color:var(--tblr-purple);--tblr-btn-disabled-color:var(--tblr-purple);--tblr-btn-disabled-border-color:var(--tblr-purple)}.btn-ghost-purple,.btn-ghost.btn-purple{--tblr-btn-color:var(--tblr-purple);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-purple-fg);--tblr-btn-hover-bg:var(--tblr-purple);--tblr-btn-hover-border-color:var(--tblr-purple);--tblr-btn-active-color:var(--tblr-purple-fg);--tblr-btn-active-bg:var(--tblr-purple);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-purple);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-pink{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-pink-fg, #ffffff);--tblr-btn-bg:var(--tblr-pink);--tblr-btn-hover-color:var(--tblr-pink-fg);--tblr-btn-hover-bg:var(--tblr-pink-darken);--tblr-btn-active-color:var(--tblr-pink-fg);--tblr-btn-active-bg:var(--tblr-pink-darken);--tblr-btn-disabled-bg:var(--tblr-pink);--tblr-btn-disabled-color:var(--tblr-pink-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-pink,.btn-outline.btn-pink{--tblr-btn-color:var(--tblr-pink);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-pink);--tblr-btn-hover-color:var(--tblr-pink-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-pink);--tblr-btn-active-color:var(--tblr-pink-fg);--tblr-btn-active-bg:var(--tblr-pink);--tblr-btn-active-border-color:var(--tblr-pink);--tblr-btn-disabled-color:var(--tblr-pink);--tblr-btn-disabled-border-color:var(--tblr-pink)}.btn-ghost-pink,.btn-ghost.btn-pink{--tblr-btn-color:var(--tblr-pink);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-pink-fg);--tblr-btn-hover-bg:var(--tblr-pink);--tblr-btn-hover-border-color:var(--tblr-pink);--tblr-btn-active-color:var(--tblr-pink-fg);--tblr-btn-active-bg:var(--tblr-pink);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-pink);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-red{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-red-fg, #ffffff);--tblr-btn-bg:var(--tblr-red);--tblr-btn-hover-color:var(--tblr-red-fg);--tblr-btn-hover-bg:var(--tblr-red-darken);--tblr-btn-active-color:var(--tblr-red-fg);--tblr-btn-active-bg:var(--tblr-red-darken);--tblr-btn-disabled-bg:var(--tblr-red);--tblr-btn-disabled-color:var(--tblr-red-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-red,.btn-outline.btn-red{--tblr-btn-color:var(--tblr-red);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-red);--tblr-btn-hover-color:var(--tblr-red-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-red);--tblr-btn-active-color:var(--tblr-red-fg);--tblr-btn-active-bg:var(--tblr-red);--tblr-btn-active-border-color:var(--tblr-red);--tblr-btn-disabled-color:var(--tblr-red);--tblr-btn-disabled-border-color:var(--tblr-red)}.btn-ghost-red,.btn-ghost.btn-red{--tblr-btn-color:var(--tblr-red);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-red-fg);--tblr-btn-hover-bg:var(--tblr-red);--tblr-btn-hover-border-color:var(--tblr-red);--tblr-btn-active-color:var(--tblr-red-fg);--tblr-btn-active-bg:var(--tblr-red);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-red);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-orange{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-orange-fg, #ffffff);--tblr-btn-bg:var(--tblr-orange);--tblr-btn-hover-color:var(--tblr-orange-fg);--tblr-btn-hover-bg:var(--tblr-orange-darken);--tblr-btn-active-color:var(--tblr-orange-fg);--tblr-btn-active-bg:var(--tblr-orange-darken);--tblr-btn-disabled-bg:var(--tblr-orange);--tblr-btn-disabled-color:var(--tblr-orange-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-orange,.btn-outline.btn-orange{--tblr-btn-color:var(--tblr-orange);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-orange);--tblr-btn-hover-color:var(--tblr-orange-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-orange);--tblr-btn-active-color:var(--tblr-orange-fg);--tblr-btn-active-bg:var(--tblr-orange);--tblr-btn-active-border-color:var(--tblr-orange);--tblr-btn-disabled-color:var(--tblr-orange);--tblr-btn-disabled-border-color:var(--tblr-orange)}.btn-ghost-orange,.btn-ghost.btn-orange{--tblr-btn-color:var(--tblr-orange);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-orange-fg);--tblr-btn-hover-bg:var(--tblr-orange);--tblr-btn-hover-border-color:var(--tblr-orange);--tblr-btn-active-color:var(--tblr-orange-fg);--tblr-btn-active-bg:var(--tblr-orange);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-orange);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-yellow{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-yellow-fg, #ffffff);--tblr-btn-bg:var(--tblr-yellow);--tblr-btn-hover-color:var(--tblr-yellow-fg);--tblr-btn-hover-bg:var(--tblr-yellow-darken);--tblr-btn-active-color:var(--tblr-yellow-fg);--tblr-btn-active-bg:var(--tblr-yellow-darken);--tblr-btn-disabled-bg:var(--tblr-yellow);--tblr-btn-disabled-color:var(--tblr-yellow-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-yellow,.btn-outline.btn-yellow{--tblr-btn-color:var(--tblr-yellow);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-yellow);--tblr-btn-hover-color:var(--tblr-yellow-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-yellow);--tblr-btn-active-color:var(--tblr-yellow-fg);--tblr-btn-active-bg:var(--tblr-yellow);--tblr-btn-active-border-color:var(--tblr-yellow);--tblr-btn-disabled-color:var(--tblr-yellow);--tblr-btn-disabled-border-color:var(--tblr-yellow)}.btn-ghost-yellow,.btn-ghost.btn-yellow{--tblr-btn-color:var(--tblr-yellow);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-yellow-fg);--tblr-btn-hover-bg:var(--tblr-yellow);--tblr-btn-hover-border-color:var(--tblr-yellow);--tblr-btn-active-color:var(--tblr-yellow-fg);--tblr-btn-active-bg:var(--tblr-yellow);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-yellow);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-lime{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-lime-fg, #ffffff);--tblr-btn-bg:var(--tblr-lime);--tblr-btn-hover-color:var(--tblr-lime-fg);--tblr-btn-hover-bg:var(--tblr-lime-darken);--tblr-btn-active-color:var(--tblr-lime-fg);--tblr-btn-active-bg:var(--tblr-lime-darken);--tblr-btn-disabled-bg:var(--tblr-lime);--tblr-btn-disabled-color:var(--tblr-lime-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-lime,.btn-outline.btn-lime{--tblr-btn-color:var(--tblr-lime);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-lime);--tblr-btn-hover-color:var(--tblr-lime-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-lime);--tblr-btn-active-color:var(--tblr-lime-fg);--tblr-btn-active-bg:var(--tblr-lime);--tblr-btn-active-border-color:var(--tblr-lime);--tblr-btn-disabled-color:var(--tblr-lime);--tblr-btn-disabled-border-color:var(--tblr-lime)}.btn-ghost-lime,.btn-ghost.btn-lime{--tblr-btn-color:var(--tblr-lime);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-lime-fg);--tblr-btn-hover-bg:var(--tblr-lime);--tblr-btn-hover-border-color:var(--tblr-lime);--tblr-btn-active-color:var(--tblr-lime-fg);--tblr-btn-active-bg:var(--tblr-lime);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-lime);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-green{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-green-fg, #ffffff);--tblr-btn-bg:var(--tblr-green);--tblr-btn-hover-color:var(--tblr-green-fg);--tblr-btn-hover-bg:var(--tblr-green-darken);--tblr-btn-active-color:var(--tblr-green-fg);--tblr-btn-active-bg:var(--tblr-green-darken);--tblr-btn-disabled-bg:var(--tblr-green);--tblr-btn-disabled-color:var(--tblr-green-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-green,.btn-outline.btn-green{--tblr-btn-color:var(--tblr-green);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-green);--tblr-btn-hover-color:var(--tblr-green-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-green);--tblr-btn-active-color:var(--tblr-green-fg);--tblr-btn-active-bg:var(--tblr-green);--tblr-btn-active-border-color:var(--tblr-green);--tblr-btn-disabled-color:var(--tblr-green);--tblr-btn-disabled-border-color:var(--tblr-green)}.btn-ghost-green,.btn-ghost.btn-green{--tblr-btn-color:var(--tblr-green);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-green-fg);--tblr-btn-hover-bg:var(--tblr-green);--tblr-btn-hover-border-color:var(--tblr-green);--tblr-btn-active-color:var(--tblr-green-fg);--tblr-btn-active-bg:var(--tblr-green);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-green);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-teal{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-teal-fg, #ffffff);--tblr-btn-bg:var(--tblr-teal);--tblr-btn-hover-color:var(--tblr-teal-fg);--tblr-btn-hover-bg:var(--tblr-teal-darken);--tblr-btn-active-color:var(--tblr-teal-fg);--tblr-btn-active-bg:var(--tblr-teal-darken);--tblr-btn-disabled-bg:var(--tblr-teal);--tblr-btn-disabled-color:var(--tblr-teal-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-teal,.btn-outline.btn-teal{--tblr-btn-color:var(--tblr-teal);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-teal);--tblr-btn-hover-color:var(--tblr-teal-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-teal);--tblr-btn-active-color:var(--tblr-teal-fg);--tblr-btn-active-bg:var(--tblr-teal);--tblr-btn-active-border-color:var(--tblr-teal);--tblr-btn-disabled-color:var(--tblr-teal);--tblr-btn-disabled-border-color:var(--tblr-teal)}.btn-ghost-teal,.btn-ghost.btn-teal{--tblr-btn-color:var(--tblr-teal);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-teal-fg);--tblr-btn-hover-bg:var(--tblr-teal);--tblr-btn-hover-border-color:var(--tblr-teal);--tblr-btn-active-color:var(--tblr-teal-fg);--tblr-btn-active-bg:var(--tblr-teal);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-teal);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-cyan{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-cyan-fg, #ffffff);--tblr-btn-bg:var(--tblr-cyan);--tblr-btn-hover-color:var(--tblr-cyan-fg);--tblr-btn-hover-bg:var(--tblr-cyan-darken);--tblr-btn-active-color:var(--tblr-cyan-fg);--tblr-btn-active-bg:var(--tblr-cyan-darken);--tblr-btn-disabled-bg:var(--tblr-cyan);--tblr-btn-disabled-color:var(--tblr-cyan-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-cyan,.btn-outline.btn-cyan{--tblr-btn-color:var(--tblr-cyan);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-cyan);--tblr-btn-hover-color:var(--tblr-cyan-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-cyan);--tblr-btn-active-color:var(--tblr-cyan-fg);--tblr-btn-active-bg:var(--tblr-cyan);--tblr-btn-active-border-color:var(--tblr-cyan);--tblr-btn-disabled-color:var(--tblr-cyan);--tblr-btn-disabled-border-color:var(--tblr-cyan)}.btn-ghost-cyan,.btn-ghost.btn-cyan{--tblr-btn-color:var(--tblr-cyan);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-cyan-fg);--tblr-btn-hover-bg:var(--tblr-cyan);--tblr-btn-hover-border-color:var(--tblr-cyan);--tblr-btn-active-color:var(--tblr-cyan-fg);--tblr-btn-active-bg:var(--tblr-cyan);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-cyan);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-x{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-x-fg, #ffffff);--tblr-btn-bg:var(--tblr-x);--tblr-btn-hover-color:var(--tblr-x-fg);--tblr-btn-hover-bg:var(--tblr-x-darken);--tblr-btn-active-color:var(--tblr-x-fg);--tblr-btn-active-bg:var(--tblr-x-darken);--tblr-btn-disabled-bg:var(--tblr-x);--tblr-btn-disabled-color:var(--tblr-x-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-x,.btn-outline.btn-x{--tblr-btn-color:var(--tblr-x);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-x);--tblr-btn-hover-color:var(--tblr-x-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-x);--tblr-btn-active-color:var(--tblr-x-fg);--tblr-btn-active-bg:var(--tblr-x);--tblr-btn-active-border-color:var(--tblr-x);--tblr-btn-disabled-color:var(--tblr-x);--tblr-btn-disabled-border-color:var(--tblr-x)}.btn-ghost-x,.btn-ghost.btn-x{--tblr-btn-color:var(--tblr-x);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-x-fg);--tblr-btn-hover-bg:var(--tblr-x);--tblr-btn-hover-border-color:var(--tblr-x);--tblr-btn-active-color:var(--tblr-x-fg);--tblr-btn-active-bg:var(--tblr-x);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-x);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-facebook{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-facebook-fg, #ffffff);--tblr-btn-bg:var(--tblr-facebook);--tblr-btn-hover-color:var(--tblr-facebook-fg);--tblr-btn-hover-bg:var(--tblr-facebook-darken);--tblr-btn-active-color:var(--tblr-facebook-fg);--tblr-btn-active-bg:var(--tblr-facebook-darken);--tblr-btn-disabled-bg:var(--tblr-facebook);--tblr-btn-disabled-color:var(--tblr-facebook-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-facebook,.btn-outline.btn-facebook{--tblr-btn-color:var(--tblr-facebook);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-facebook);--tblr-btn-hover-color:var(--tblr-facebook-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-facebook);--tblr-btn-active-color:var(--tblr-facebook-fg);--tblr-btn-active-bg:var(--tblr-facebook);--tblr-btn-active-border-color:var(--tblr-facebook);--tblr-btn-disabled-color:var(--tblr-facebook);--tblr-btn-disabled-border-color:var(--tblr-facebook)}.btn-ghost-facebook,.btn-ghost.btn-facebook{--tblr-btn-color:var(--tblr-facebook);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-facebook-fg);--tblr-btn-hover-bg:var(--tblr-facebook);--tblr-btn-hover-border-color:var(--tblr-facebook);--tblr-btn-active-color:var(--tblr-facebook-fg);--tblr-btn-active-bg:var(--tblr-facebook);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-facebook);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-twitter{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-twitter-fg, #ffffff);--tblr-btn-bg:var(--tblr-twitter);--tblr-btn-hover-color:var(--tblr-twitter-fg);--tblr-btn-hover-bg:var(--tblr-twitter-darken);--tblr-btn-active-color:var(--tblr-twitter-fg);--tblr-btn-active-bg:var(--tblr-twitter-darken);--tblr-btn-disabled-bg:var(--tblr-twitter);--tblr-btn-disabled-color:var(--tblr-twitter-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-twitter,.btn-outline.btn-twitter{--tblr-btn-color:var(--tblr-twitter);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-twitter);--tblr-btn-hover-color:var(--tblr-twitter-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-twitter);--tblr-btn-active-color:var(--tblr-twitter-fg);--tblr-btn-active-bg:var(--tblr-twitter);--tblr-btn-active-border-color:var(--tblr-twitter);--tblr-btn-disabled-color:var(--tblr-twitter);--tblr-btn-disabled-border-color:var(--tblr-twitter)}.btn-ghost-twitter,.btn-ghost.btn-twitter{--tblr-btn-color:var(--tblr-twitter);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-twitter-fg);--tblr-btn-hover-bg:var(--tblr-twitter);--tblr-btn-hover-border-color:var(--tblr-twitter);--tblr-btn-active-color:var(--tblr-twitter-fg);--tblr-btn-active-bg:var(--tblr-twitter);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-twitter);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-linkedin{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-linkedin-fg, #ffffff);--tblr-btn-bg:var(--tblr-linkedin);--tblr-btn-hover-color:var(--tblr-linkedin-fg);--tblr-btn-hover-bg:var(--tblr-linkedin-darken);--tblr-btn-active-color:var(--tblr-linkedin-fg);--tblr-btn-active-bg:var(--tblr-linkedin-darken);--tblr-btn-disabled-bg:var(--tblr-linkedin);--tblr-btn-disabled-color:var(--tblr-linkedin-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-linkedin,.btn-outline.btn-linkedin{--tblr-btn-color:var(--tblr-linkedin);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-linkedin);--tblr-btn-hover-color:var(--tblr-linkedin-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-linkedin);--tblr-btn-active-color:var(--tblr-linkedin-fg);--tblr-btn-active-bg:var(--tblr-linkedin);--tblr-btn-active-border-color:var(--tblr-linkedin);--tblr-btn-disabled-color:var(--tblr-linkedin);--tblr-btn-disabled-border-color:var(--tblr-linkedin)}.btn-ghost-linkedin,.btn-ghost.btn-linkedin{--tblr-btn-color:var(--tblr-linkedin);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-linkedin-fg);--tblr-btn-hover-bg:var(--tblr-linkedin);--tblr-btn-hover-border-color:var(--tblr-linkedin);--tblr-btn-active-color:var(--tblr-linkedin-fg);--tblr-btn-active-bg:var(--tblr-linkedin);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-linkedin);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-google{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-google-fg, #ffffff);--tblr-btn-bg:var(--tblr-google);--tblr-btn-hover-color:var(--tblr-google-fg);--tblr-btn-hover-bg:var(--tblr-google-darken);--tblr-btn-active-color:var(--tblr-google-fg);--tblr-btn-active-bg:var(--tblr-google-darken);--tblr-btn-disabled-bg:var(--tblr-google);--tblr-btn-disabled-color:var(--tblr-google-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-google,.btn-outline.btn-google{--tblr-btn-color:var(--tblr-google);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-google);--tblr-btn-hover-color:var(--tblr-google-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-google);--tblr-btn-active-color:var(--tblr-google-fg);--tblr-btn-active-bg:var(--tblr-google);--tblr-btn-active-border-color:var(--tblr-google);--tblr-btn-disabled-color:var(--tblr-google);--tblr-btn-disabled-border-color:var(--tblr-google)}.btn-ghost-google,.btn-ghost.btn-google{--tblr-btn-color:var(--tblr-google);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-google-fg);--tblr-btn-hover-bg:var(--tblr-google);--tblr-btn-hover-border-color:var(--tblr-google);--tblr-btn-active-color:var(--tblr-google-fg);--tblr-btn-active-bg:var(--tblr-google);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-google);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-youtube{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-youtube-fg, #ffffff);--tblr-btn-bg:var(--tblr-youtube);--tblr-btn-hover-color:var(--tblr-youtube-fg);--tblr-btn-hover-bg:var(--tblr-youtube-darken);--tblr-btn-active-color:var(--tblr-youtube-fg);--tblr-btn-active-bg:var(--tblr-youtube-darken);--tblr-btn-disabled-bg:var(--tblr-youtube);--tblr-btn-disabled-color:var(--tblr-youtube-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-youtube,.btn-outline.btn-youtube{--tblr-btn-color:var(--tblr-youtube);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-youtube);--tblr-btn-hover-color:var(--tblr-youtube-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-youtube);--tblr-btn-active-color:var(--tblr-youtube-fg);--tblr-btn-active-bg:var(--tblr-youtube);--tblr-btn-active-border-color:var(--tblr-youtube);--tblr-btn-disabled-color:var(--tblr-youtube);--tblr-btn-disabled-border-color:var(--tblr-youtube)}.btn-ghost-youtube,.btn-ghost.btn-youtube{--tblr-btn-color:var(--tblr-youtube);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-youtube-fg);--tblr-btn-hover-bg:var(--tblr-youtube);--tblr-btn-hover-border-color:var(--tblr-youtube);--tblr-btn-active-color:var(--tblr-youtube-fg);--tblr-btn-active-bg:var(--tblr-youtube);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-youtube);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-vimeo{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-vimeo-fg, #ffffff);--tblr-btn-bg:var(--tblr-vimeo);--tblr-btn-hover-color:var(--tblr-vimeo-fg);--tblr-btn-hover-bg:var(--tblr-vimeo-darken);--tblr-btn-active-color:var(--tblr-vimeo-fg);--tblr-btn-active-bg:var(--tblr-vimeo-darken);--tblr-btn-disabled-bg:var(--tblr-vimeo);--tblr-btn-disabled-color:var(--tblr-vimeo-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-vimeo,.btn-outline.btn-vimeo{--tblr-btn-color:var(--tblr-vimeo);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-vimeo);--tblr-btn-hover-color:var(--tblr-vimeo-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-vimeo);--tblr-btn-active-color:var(--tblr-vimeo-fg);--tblr-btn-active-bg:var(--tblr-vimeo);--tblr-btn-active-border-color:var(--tblr-vimeo);--tblr-btn-disabled-color:var(--tblr-vimeo);--tblr-btn-disabled-border-color:var(--tblr-vimeo)}.btn-ghost-vimeo,.btn-ghost.btn-vimeo{--tblr-btn-color:var(--tblr-vimeo);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-vimeo-fg);--tblr-btn-hover-bg:var(--tblr-vimeo);--tblr-btn-hover-border-color:var(--tblr-vimeo);--tblr-btn-active-color:var(--tblr-vimeo-fg);--tblr-btn-active-bg:var(--tblr-vimeo);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-vimeo);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-dribbble{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-dribbble-fg, #ffffff);--tblr-btn-bg:var(--tblr-dribbble);--tblr-btn-hover-color:var(--tblr-dribbble-fg);--tblr-btn-hover-bg:var(--tblr-dribbble-darken);--tblr-btn-active-color:var(--tblr-dribbble-fg);--tblr-btn-active-bg:var(--tblr-dribbble-darken);--tblr-btn-disabled-bg:var(--tblr-dribbble);--tblr-btn-disabled-color:var(--tblr-dribbble-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-dribbble,.btn-outline.btn-dribbble{--tblr-btn-color:var(--tblr-dribbble);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-dribbble);--tblr-btn-hover-color:var(--tblr-dribbble-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-dribbble);--tblr-btn-active-color:var(--tblr-dribbble-fg);--tblr-btn-active-bg:var(--tblr-dribbble);--tblr-btn-active-border-color:var(--tblr-dribbble);--tblr-btn-disabled-color:var(--tblr-dribbble);--tblr-btn-disabled-border-color:var(--tblr-dribbble)}.btn-ghost-dribbble,.btn-ghost.btn-dribbble{--tblr-btn-color:var(--tblr-dribbble);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-dribbble-fg);--tblr-btn-hover-bg:var(--tblr-dribbble);--tblr-btn-hover-border-color:var(--tblr-dribbble);--tblr-btn-active-color:var(--tblr-dribbble-fg);--tblr-btn-active-bg:var(--tblr-dribbble);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-dribbble);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-github{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-github-fg, #ffffff);--tblr-btn-bg:var(--tblr-github);--tblr-btn-hover-color:var(--tblr-github-fg);--tblr-btn-hover-bg:var(--tblr-github-darken);--tblr-btn-active-color:var(--tblr-github-fg);--tblr-btn-active-bg:var(--tblr-github-darken);--tblr-btn-disabled-bg:var(--tblr-github);--tblr-btn-disabled-color:var(--tblr-github-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-github,.btn-outline.btn-github{--tblr-btn-color:var(--tblr-github);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-github);--tblr-btn-hover-color:var(--tblr-github-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-github);--tblr-btn-active-color:var(--tblr-github-fg);--tblr-btn-active-bg:var(--tblr-github);--tblr-btn-active-border-color:var(--tblr-github);--tblr-btn-disabled-color:var(--tblr-github);--tblr-btn-disabled-border-color:var(--tblr-github)}.btn-ghost-github,.btn-ghost.btn-github{--tblr-btn-color:var(--tblr-github);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-github-fg);--tblr-btn-hover-bg:var(--tblr-github);--tblr-btn-hover-border-color:var(--tblr-github);--tblr-btn-active-color:var(--tblr-github-fg);--tblr-btn-active-bg:var(--tblr-github);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-github);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-instagram{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-instagram-fg, #ffffff);--tblr-btn-bg:var(--tblr-instagram);--tblr-btn-hover-color:var(--tblr-instagram-fg);--tblr-btn-hover-bg:var(--tblr-instagram-darken);--tblr-btn-active-color:var(--tblr-instagram-fg);--tblr-btn-active-bg:var(--tblr-instagram-darken);--tblr-btn-disabled-bg:var(--tblr-instagram);--tblr-btn-disabled-color:var(--tblr-instagram-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-instagram,.btn-outline.btn-instagram{--tblr-btn-color:var(--tblr-instagram);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-instagram);--tblr-btn-hover-color:var(--tblr-instagram-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-instagram);--tblr-btn-active-color:var(--tblr-instagram-fg);--tblr-btn-active-bg:var(--tblr-instagram);--tblr-btn-active-border-color:var(--tblr-instagram);--tblr-btn-disabled-color:var(--tblr-instagram);--tblr-btn-disabled-border-color:var(--tblr-instagram)}.btn-ghost-instagram,.btn-ghost.btn-instagram{--tblr-btn-color:var(--tblr-instagram);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-instagram-fg);--tblr-btn-hover-bg:var(--tblr-instagram);--tblr-btn-hover-border-color:var(--tblr-instagram);--tblr-btn-active-color:var(--tblr-instagram-fg);--tblr-btn-active-bg:var(--tblr-instagram);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-instagram);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-pinterest{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-pinterest-fg, #ffffff);--tblr-btn-bg:var(--tblr-pinterest);--tblr-btn-hover-color:var(--tblr-pinterest-fg);--tblr-btn-hover-bg:var(--tblr-pinterest-darken);--tblr-btn-active-color:var(--tblr-pinterest-fg);--tblr-btn-active-bg:var(--tblr-pinterest-darken);--tblr-btn-disabled-bg:var(--tblr-pinterest);--tblr-btn-disabled-color:var(--tblr-pinterest-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-pinterest,.btn-outline.btn-pinterest{--tblr-btn-color:var(--tblr-pinterest);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-pinterest);--tblr-btn-hover-color:var(--tblr-pinterest-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-pinterest);--tblr-btn-active-color:var(--tblr-pinterest-fg);--tblr-btn-active-bg:var(--tblr-pinterest);--tblr-btn-active-border-color:var(--tblr-pinterest);--tblr-btn-disabled-color:var(--tblr-pinterest);--tblr-btn-disabled-border-color:var(--tblr-pinterest)}.btn-ghost-pinterest,.btn-ghost.btn-pinterest{--tblr-btn-color:var(--tblr-pinterest);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-pinterest-fg);--tblr-btn-hover-bg:var(--tblr-pinterest);--tblr-btn-hover-border-color:var(--tblr-pinterest);--tblr-btn-active-color:var(--tblr-pinterest-fg);--tblr-btn-active-bg:var(--tblr-pinterest);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-pinterest);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-vk{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-vk-fg, #ffffff);--tblr-btn-bg:var(--tblr-vk);--tblr-btn-hover-color:var(--tblr-vk-fg);--tblr-btn-hover-bg:var(--tblr-vk-darken);--tblr-btn-active-color:var(--tblr-vk-fg);--tblr-btn-active-bg:var(--tblr-vk-darken);--tblr-btn-disabled-bg:var(--tblr-vk);--tblr-btn-disabled-color:var(--tblr-vk-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-vk,.btn-outline.btn-vk{--tblr-btn-color:var(--tblr-vk);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-vk);--tblr-btn-hover-color:var(--tblr-vk-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-vk);--tblr-btn-active-color:var(--tblr-vk-fg);--tblr-btn-active-bg:var(--tblr-vk);--tblr-btn-active-border-color:var(--tblr-vk);--tblr-btn-disabled-color:var(--tblr-vk);--tblr-btn-disabled-border-color:var(--tblr-vk)}.btn-ghost-vk,.btn-ghost.btn-vk{--tblr-btn-color:var(--tblr-vk);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-vk-fg);--tblr-btn-hover-bg:var(--tblr-vk);--tblr-btn-hover-border-color:var(--tblr-vk);--tblr-btn-active-color:var(--tblr-vk-fg);--tblr-btn-active-bg:var(--tblr-vk);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-vk);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-rss{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-rss-fg, #ffffff);--tblr-btn-bg:var(--tblr-rss);--tblr-btn-hover-color:var(--tblr-rss-fg);--tblr-btn-hover-bg:var(--tblr-rss-darken);--tblr-btn-active-color:var(--tblr-rss-fg);--tblr-btn-active-bg:var(--tblr-rss-darken);--tblr-btn-disabled-bg:var(--tblr-rss);--tblr-btn-disabled-color:var(--tblr-rss-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-rss,.btn-outline.btn-rss{--tblr-btn-color:var(--tblr-rss);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-rss);--tblr-btn-hover-color:var(--tblr-rss-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-rss);--tblr-btn-active-color:var(--tblr-rss-fg);--tblr-btn-active-bg:var(--tblr-rss);--tblr-btn-active-border-color:var(--tblr-rss);--tblr-btn-disabled-color:var(--tblr-rss);--tblr-btn-disabled-border-color:var(--tblr-rss)}.btn-ghost-rss,.btn-ghost.btn-rss{--tblr-btn-color:var(--tblr-rss);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-rss-fg);--tblr-btn-hover-bg:var(--tblr-rss);--tblr-btn-hover-border-color:var(--tblr-rss);--tblr-btn-active-color:var(--tblr-rss-fg);--tblr-btn-active-bg:var(--tblr-rss);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-rss);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-flickr{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-flickr-fg, #ffffff);--tblr-btn-bg:var(--tblr-flickr);--tblr-btn-hover-color:var(--tblr-flickr-fg);--tblr-btn-hover-bg:var(--tblr-flickr-darken);--tblr-btn-active-color:var(--tblr-flickr-fg);--tblr-btn-active-bg:var(--tblr-flickr-darken);--tblr-btn-disabled-bg:var(--tblr-flickr);--tblr-btn-disabled-color:var(--tblr-flickr-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-flickr,.btn-outline.btn-flickr{--tblr-btn-color:var(--tblr-flickr);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-flickr);--tblr-btn-hover-color:var(--tblr-flickr-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-flickr);--tblr-btn-active-color:var(--tblr-flickr-fg);--tblr-btn-active-bg:var(--tblr-flickr);--tblr-btn-active-border-color:var(--tblr-flickr);--tblr-btn-disabled-color:var(--tblr-flickr);--tblr-btn-disabled-border-color:var(--tblr-flickr)}.btn-ghost-flickr,.btn-ghost.btn-flickr{--tblr-btn-color:var(--tblr-flickr);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-flickr-fg);--tblr-btn-hover-bg:var(--tblr-flickr);--tblr-btn-hover-border-color:var(--tblr-flickr);--tblr-btn-active-color:var(--tblr-flickr-fg);--tblr-btn-active-bg:var(--tblr-flickr);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-flickr);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-bitbucket{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-bitbucket-fg, #ffffff);--tblr-btn-bg:var(--tblr-bitbucket);--tblr-btn-hover-color:var(--tblr-bitbucket-fg);--tblr-btn-hover-bg:var(--tblr-bitbucket-darken);--tblr-btn-active-color:var(--tblr-bitbucket-fg);--tblr-btn-active-bg:var(--tblr-bitbucket-darken);--tblr-btn-disabled-bg:var(--tblr-bitbucket);--tblr-btn-disabled-color:var(--tblr-bitbucket-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-bitbucket,.btn-outline.btn-bitbucket{--tblr-btn-color:var(--tblr-bitbucket);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-bitbucket);--tblr-btn-hover-color:var(--tblr-bitbucket-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-bitbucket);--tblr-btn-active-color:var(--tblr-bitbucket-fg);--tblr-btn-active-bg:var(--tblr-bitbucket);--tblr-btn-active-border-color:var(--tblr-bitbucket);--tblr-btn-disabled-color:var(--tblr-bitbucket);--tblr-btn-disabled-border-color:var(--tblr-bitbucket)}.btn-ghost-bitbucket,.btn-ghost.btn-bitbucket{--tblr-btn-color:var(--tblr-bitbucket);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-bitbucket-fg);--tblr-btn-hover-bg:var(--tblr-bitbucket);--tblr-btn-hover-border-color:var(--tblr-bitbucket);--tblr-btn-active-color:var(--tblr-bitbucket-fg);--tblr-btn-active-bg:var(--tblr-bitbucket);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-bitbucket);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-tabler{--tblr-btn-border-color:transparent;--tblr-btn-hover-border-color:transparent;--tblr-btn-active-border-color:transparent;--tblr-btn-color:var(--tblr-tabler-fg, #ffffff);--tblr-btn-bg:var(--tblr-tabler);--tblr-btn-hover-color:var(--tblr-tabler-fg);--tblr-btn-hover-bg:var(--tblr-tabler-darken);--tblr-btn-active-color:var(--tblr-tabler-fg);--tblr-btn-active-bg:var(--tblr-tabler-darken);--tblr-btn-disabled-bg:var(--tblr-tabler);--tblr-btn-disabled-color:var(--tblr-tabler-fg);--tblr-btn-box-shadow:var(--tblr-shadow-input)}.btn-outline-tabler,.btn-outline.btn-tabler{--tblr-btn-color:var(--tblr-tabler);--tblr-btn-bg:transparent;--tblr-btn-border-color:var(--tblr-tabler);--tblr-btn-hover-color:var(--tblr-tabler-fg);--tblr-btn-hover-border-color:transparent;--tblr-btn-hover-bg:var(--tblr-tabler);--tblr-btn-active-color:var(--tblr-tabler-fg);--tblr-btn-active-bg:var(--tblr-tabler);--tblr-btn-active-border-color:var(--tblr-tabler);--tblr-btn-disabled-color:var(--tblr-tabler);--tblr-btn-disabled-border-color:var(--tblr-tabler)}.btn-ghost-tabler,.btn-ghost.btn-tabler{--tblr-btn-color:var(--tblr-tabler);--tblr-btn-bg:transparent;--tblr-btn-border-color:transparent;--tblr-btn-hover-color:var(--tblr-tabler-fg);--tblr-btn-hover-bg:var(--tblr-tabler);--tblr-btn-hover-border-color:var(--tblr-tabler);--tblr-btn-active-color:var(--tblr-tabler-fg);--tblr-btn-active-bg:var(--tblr-tabler);--tblr-btn-active-border-color:transparent;--tblr-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--tblr-btn-disabled-color:var(--tblr-tabler);--tblr-btn-disabled-bg:transparent;--tblr-btn-disabled-border-color:transparent;--tblr-gradient:none;--tblr-btn-box-shadow:none}.btn-group-sm>.btn,.btn-sm{--tblr-btn-line-height:1.3333333333;--tblr-btn-icon-size:1rem}.btn-group-lg>.btn,.btn-lg{--tblr-btn-line-height:1.5rem;--tblr-btn-icon-size:1.5rem}.btn-group-xl>.btn,.btn-xl{--tblr-btn-line-height:2;--tblr-btn-icon-size:2rem;--tblr-btn-padding-y:0.6875rem;--tblr-btn-padding-x:2rem;--tblr-btn-font-size:1.5rem}.btn-pill{padding-right:1.5em;padding-left:1.5em;border-radius:10rem}.btn-pill[class*=btn-icon]{padding:.375rem 15px}.btn-square{border-radius:0}.btn-action,.btn-icon{padding-left:0;padding-right:0}.btn-action .icon,.btn-icon .icon{margin:calc(-1 * var(--tblr-btn-padding-x))}.btn-list{--tblr-list-gap:0.5rem;display:flex;flex-wrap:wrap;gap:var(--tblr-list-gap)}.btn-floating{position:fixed;z-index:1030;bottom:1rem;left:1rem;box-shadow:var(--tblr-shadow-dropdown)}.btn-loading{position:relative;color:transparent!important;text-shadow:none!important;pointer-events:none}.btn-loading>*{opacity:0}.btn-loading:after{content:"";display:inline-block;vertical-align:text-bottom;border:2px var(--tblr-border-style) currentColor;border-right-color:transparent;border-radius:100rem;color:var(--tblr-btn-color);position:absolute;width:var(--tblr-btn-icon-size);height:var(--tblr-btn-icon-size);left:calc(50% - var(--tblr-btn-icon-size)/ 2);top:calc(50% - var(--tblr-btn-icon-size)/ 2);animation:spinner-border .75s linear infinite}.btn-action{--tblr-border-color:transparent;color:var(--tblr-secondary);border-radius:var(--tblr-border-radius);background:0 0;box-shadow:none}.btn-action:after{content:none}.btn-action:focus{outline:0;box-shadow:none}.btn-action.show,.btn-action:hover{color:var(--tblr-body-color);background:var(--tblr-active-bg);border-color:transparent}.btn-action.show{color:var(--tblr-primary)}.btn-actions{display:flex}.btn-animate-icon .icon{transition:transform .3s ease}.btn-animate-icon:focus-visible .icon,.btn-animate-icon:hover .icon{transform:translateX(4px)}.btn-animate-icon.btn-animate-icon-rotate:focus-visible .icon,.btn-animate-icon.btn-animate-icon-rotate:hover .icon{transform:rotate(90deg)}.btn-animate-icon.btn-animate-icon-move-start:focus-visible .icon,.btn-animate-icon.btn-animate-icon-move-start:hover .icon{transform:translateX(-4px)}.btn-animate-icon.btn-animate-icon-pulse:focus-visible .icon,.btn-animate-icon.btn-animate-icon-pulse:hover .icon{transform:none;animation:pulse .9s}.btn-animate-icon.btn-animate-icon-shake:focus-visible .icon,.btn-animate-icon.btn-animate-icon-shake:hover .icon{transform:none;animation:shake .9s}.btn-animate-icon.btn-animate-icon-tada:focus-visible .icon,.btn-animate-icon.btn-animate-icon-tada:hover .icon{transform:none;animation:tada .9s}.btn-group,.btn-group-vertical{box-shadow:var(--tblr-shadow-input)}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group>.btn-check:checked+.btn,.btn-group>.btn.active,.btn-group>.btn:active{z-index:5}.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:focus+.btn,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.calendar{display:block;font-size:.765625rem;border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);border-radius:var(--tblr-border-radius)}.calendar-nav{display:flex;align-items:center}.calendar-title{flex:1;text-align:center}.calendar-body,.calendar-header{display:flex;flex-wrap:wrap;justify-content:flex-start;padding:.5rem 0}.calendar-header{color:var(--tblr-secondary)}.calendar-date{flex:0 0 14.2857142857%;max-width:14.2857142857%;padding:.2rem;text-align:center;border:0}.calendar-date.next-month,.calendar-date.prev-month{opacity:.25}.calendar-date .date-item{position:relative;display:inline-block;width:1.4rem;height:1.4rem;line-height:1.4rem;color:#66758c;text-align:center;text-decoration:none;white-space:nowrap;vertical-align:middle;cursor:pointer;background:0 0;border:var(--tblr-border-width) var(--tblr-border-style) transparent;border-radius:100rem;outline:0;transition:background .3s,border .3s,box-shadow .32s,color .3s}@media (prefers-reduced-motion:reduce){.calendar-date .date-item{transition:none}}.calendar-date .date-item:hover{color:var(--tblr-primary);text-decoration:none;background:#fefeff;border-color:var(--tblr-border-color)}.calendar-date .date-today{color:var(--tblr-primary);border-color:var(--tblr-border-color)}.calendar-range{position:relative}.calendar-range:before{position:absolute;top:50%;right:0;left:0;height:1.4rem;content:"";background:rgba(var(--tblr-primary-rgb),.1);transform:translateY(-50%)}.calendar-range.range-end .date-item,.calendar-range.range-start .date-item{color:#fff;background:var(--tblr-primary);border-color:var(--tblr-primary)}.calendar-range.range-start:before{left:50%}.calendar-range.range-end:before{right:50%}.carousel-indicators-vertical{left:auto;top:0;margin:0 1rem 0 0;flex-direction:column}.carousel-indicators-vertical [data-bs-target]{margin:3px 0 3px;width:3px;height:30px;border:0;border-left:10px var(--tblr-border-style) transparent;border-right:10px var(--tblr-border-style) transparent}.carousel-indicators-dot [data-bs-target]{width:.5rem;height:.5rem;border-radius:100rem;border:10px var(--tblr-border-style) transparent;margin:0}.carousel-indicators-thumb [data-bs-target]{width:2rem;height:auto;background:no-repeat center/cover;border:0;border-radius:var(--tblr-border-radius);box-shadow:rgba(var(--tblr-body-color-rgb),.04) 0 2px 4px 0;margin:0 3px;opacity:.75}@media (min-width:992px){.carousel-indicators-thumb [data-bs-target]{width:4rem}}.carousel-indicators-thumb [data-bs-target]:before{content:"";padding-top:var(--tblr-aspect-ratio,100%);display:block}.carousel-indicators-thumb.carousel-indicators-vertical [data-bs-target]{margin:3px 0}.carousel-caption-background{background:red;position:absolute;left:0;right:0;bottom:0;height:90%;background:linear-gradient(0deg,rgba(31,41,55,.9),rgba(31,41,55,0))}.card{transition:transform .3s ease-out,opacity .3s ease-out,box-shadow .3s ease-out}@media (prefers-reduced-motion:reduce){.card{transition:none}}@media print{.card{border:none;box-shadow:none}}a.card{color:inherit}a.card:hover{text-decoration:none;box-shadow:rgba(var(--tblr-body-color-rgb),.16) 0 2px 16px 0}.card .card{box-shadow:none}.card-borderless,.card-borderless .card-footer,.card-borderless .card-header{border-color:transparent}.card-stamp{--tblr-stamp-size:7rem;position:absolute;top:0;right:0;width:calc(var(--tblr-stamp-size) * 1);height:calc(var(--tblr-stamp-size) * 1);max-height:100%;border-top-right-radius:6px;opacity:.2;overflow:hidden;pointer-events:none}.card-stamp-lg{--tblr-stamp-size:13rem}.card-stamp-icon{background:var(--tblr-secondary);color:var(--tblr-card-bg,var(--tblr-bg-surface));display:flex;align-items:center;justify-content:center;border-radius:100rem;width:calc(var(--tblr-stamp-size) * 1);height:calc(var(--tblr-stamp-size) * 1);position:relative;top:calc(var(--tblr-stamp-size) * -.25);right:calc(var(--tblr-stamp-size) * -.25);font-size:calc(var(--tblr-stamp-size) * .75);transform:rotate(10deg)}.card-stamp-icon .icon{stroke-width:2;width:calc(var(--tblr-stamp-size) * .75);height:calc(var(--tblr-stamp-size) * .75)}.card-img,.card-img-start{border-top-left-radius:calc(var(--tblr-border-radius-lg) - (var(--tblr-border-width)));border-bottom-left-radius:calc(var(--tblr-border-radius-lg) - (var(--tblr-border-width)))}.card-img,.card-img-end{border-top-right-radius:calc(var(--tblr-border-radius-lg) - (var(--tblr-border-width)));border-bottom-right-radius:calc(var(--tblr-border-radius-lg) - (var(--tblr-border-width)))}.card-img-overlay{display:flex;flex-direction:column;justify-content:flex-end}.card-img-overlay-dark{background-image:linear-gradient(180deg,rgba(0,0,0,0) 0,rgba(0,0,0,.6) 100%)}.card-inactive{pointer-events:none;box-shadow:none}.card-inactive .card-body{opacity:.64}.card-active{--tblr-card-border-color:var(--tblr-primary);--tblr-card-bg:var(--tblr-active-bg)}.card-btn{display:flex;align-items:center;justify-content:center;padding:1rem 1.25rem;text-align:center;transition:background .3s;border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);flex:1;color:inherit;font-weight:var(--tblr-font-weight-medium)}@media (prefers-reduced-motion:reduce){.card-btn{transition:none}}.card-btn:hover{text-decoration:none;background:rgba(var(--tblr-primary-rgb),.04)}.card-btn+.card-btn{border-left:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}.card-stacked{--tblr-card-stacked-offset:.25rem;position:relative}.card-stacked:after{position:absolute;top:calc(-1 * var(--tblr-card-stacked-offset));right:var(--tblr-card-stacked-offset);left:var(--tblr-card-stacked-offset);height:var(--tblr-card-stacked-offset);content:"";background:var(--tblr-card-bg,var(--tblr-bg-surface));border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-card-border-color);border-radius:var(--tblr-card-border-radius) var(--tblr-card-border-radius) 0 0}.card-cover{position:relative;padding:1rem 1.25rem;background:#666 no-repeat center/cover}.card-cover:before{position:absolute;top:0;right:0;bottom:0;left:0;content:"";background:rgba(31,41,55,.48)}.card-cover:first-child,.card-cover:first-child:before{border-radius:6px 6px 0 0}.card-cover-blurred:before{-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.card-actions{margin:-.5rem -.5rem -.5rem auto;padding-left:.5rem}.card-actions a{text-decoration:none}.card-header{color:inherit;display:flex;align-items:center;background:0 0}.card-header:first-child{border-radius:var(--tblr-card-border-radius) var(--tblr-card-border-radius) 0 0}.card-header-light{border-bottom-color:transparent;background:var(--tblr-bg-surface-tertiary)}.card-header-tabs{background:var(--tblr-bg-surface-tertiary);flex:1;margin:calc(var(--tblr-card-cap-padding-y) * -1) calc(var(--tblr-card-cap-padding-x) * -1) calc(var(--tblr-card-cap-padding-y) * -1);padding:calc(var(--tblr-card-cap-padding-y) * .5) calc(var(--tblr-card-cap-padding-x) * .5) 0;border-radius:var(--tblr-card-border-radius) var(--tblr-card-border-radius) 0 0}.card-header-pills{flex:1;margin-top:-.5rem;margin-bottom:-.5rem}.card-rotate-left,.card-rotate-start{transform:rotate(-1.5deg)}.card-rotate-end,.card-rotate-right{transform:rotate(1.5deg)}.card-link{color:inherit}.card-link:hover{color:inherit;text-decoration:none;box-shadow:0 1px 6px 0 rgba(0,0,0,.08)}.card-link-rotate:hover{transform:rotate(1.5deg);opacity:1}.card-link-pop:hover{transform:translateY(-2px);opacity:1}.card-footer{margin-top:auto}.card-footer:last-child{border-radius:0 0 var(--tblr-card-border-radius) var(--tblr-card-border-radius)}.card-footer-transparent{background:0 0;border-color:transparent;padding-top:0}.card-footer-borderless{border-top:none}.card-progress{height:.25rem}.card-progress:last-child{border-radius:0 0 2px 2px}.card-progress:first-child{border-radius:2px 2px 0 0}.card-meta{color:var(--tblr-secondary)}.card-title{display:block;margin:0 0 1rem;font-size:1rem;font-weight:var(--tblr-font-weight-medium);color:inherit;line-height:1.5rem}a.card-title:hover{color:inherit}.card-header .card-title{margin:0}.card-subtitle{margin-bottom:1.25rem;color:var(--tblr-secondary);font-weight:400}.card-header .card-subtitle{margin:0}.card-title .card-subtitle{margin:0 0 0 .25rem;font-size:.875rem}.card-body{position:relative}.card-body>:last-child{margin-bottom:0}.card-sm>.card-body{padding:1rem}@media (min-width:768px){.card-md>.card-body{padding:2.5rem}}@media (min-width:768px){.card-lg>.card-body{padding:2rem}}@media (min-width:992px){.card-lg>.card-body{padding:4rem}}@media print{.card-body{padding:0}}.card-body+.card-body{border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}.card-body-scrollable{overflow:auto}.card-options{top:1.5rem;right:.75rem;display:flex;margin-left:auto}.card-options-link{display:inline-block;min-width:1rem;margin-left:.25rem;color:var(--tblr-secondary)}.card-status-top{position:absolute;top:0;right:0;left:0;height:2px;border-radius:var(--tblr-card-border-radius) var(--tblr-card-border-radius) 0 0}.card-status-start{position:absolute;right:auto;bottom:0;width:2px;height:100%;border-radius:var(--tblr-card-border-radius) 0 0 var(--tblr-card-border-radius)}.card-status-bottom{position:absolute;top:initial;bottom:0;width:100%;height:2px;border-radius:0 0 var(--tblr-card-border-radius) var(--tblr-card-border-radius)}.card-table{margin-bottom:0!important}.card-table tr td:first-child,.card-table tr th:first-child{padding-left:1.25rem;border-left:0}.card-table tr td:last-child,.card-table tr th:last-child{padding-right:1.25rem;border-right:0}.card-table tbody tr:first-child,.card-table tfoot tr:first-child,.card-table thead tr:first-child{border-top:0}.card-table tbody tr:first-child td,.card-table tbody tr:first-child th,.card-table tfoot tr:first-child td,.card-table tfoot tr:first-child th,.card-table thead tr:first-child td,.card-table thead tr:first-child th{border-top:0}.card-body+.card-table{border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-table-border-color)}.card-code{padding:0}.card-code .highlight{margin:0;border:0}.card-code pre{margin:0!important;border:0!important}.card-chart{position:relative;z-index:1;height:3.5rem}.card-avatar{margin-left:auto;margin-right:auto;box-shadow:0 0 0 .25rem var(--tblr-card-bg,var(--tblr-bg-surface));margin-top:calc(-1 * var(--tblr-avatar-size) * .5)}.card-body+.card-list-group{border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}.card-list-group .list-group-item{padding-right:1.25rem;padding-left:1.25rem;border-right:0;border-left:0;border-radius:0}.card-list-group .list-group-item:last-child{border-bottom:0}.card-list-group .list-group-item:first-child{border-top:0}.card-tabs .nav-tabs{position:relative;z-index:1000;border-bottom:0}.card-tabs .nav-tabs .nav-link{background:var(--tblr-bg-surface-tertiary);border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)}.card-tabs .nav-tabs .nav-link.active,.card-tabs .nav-tabs .nav-link:active,.card-tabs .nav-tabs .nav-link:hover{border-color:var(--tblr-border-color-translucent);color:var(--tblr-body-color)}.card-tabs .nav-tabs .nav-link.active{color:inherit;background:var(--tblr-card-bg,var(--tblr-bg-surface));border-bottom-color:transparent}.card-tabs .nav-tabs .nav-item:not(:first-child) .nav-link{border-top-left-radius:0}.card-tabs .nav-tabs .nav-item:not(:last-child) .nav-link{border-top-right-radius:0}.card-tabs .nav-tabs .nav-item+.nav-item{margin-left:calc(-1 * var(--tblr-border-width))}.card-tabs .nav-tabs-bottom{margin-bottom:0}.card-tabs .nav-tabs-bottom .nav-link{margin-bottom:0}.card-tabs .nav-tabs-bottom .nav-link.active{border-top-color:transparent}.card-tabs .nav-tabs-bottom .nav-item{margin-top:calc(-1 * var(--tblr-border-width));margin-bottom:0}.card-tabs .nav-tabs-bottom .nav-item .nav-link{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent);border-radius:0 0 var(--tblr-border-radius-lg) var(--tblr-border-radius-lg)}.card-tabs .nav-tabs-bottom .nav-item:not(:first-child) .nav-link{border-bottom-left-radius:0}.card-tabs .nav-tabs-bottom .nav-item:not(:last-child) .nav-link{border-bottom-right-radius:0}.card-tabs .card{border-bottom-left-radius:0}.card-tabs .nav-tabs+.tab-content .card{border-bottom-left-radius:var(--tblr-card-border-radius);border-top-left-radius:0}.card-note{--tblr-card-bg:#fff7dd;--tblr-card-border-color:#fff1c9}.btn-close{--tblr-btn-close-color:currentColor;--tblr-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%231f2937'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e");--tblr-btn-close-opacity:0.4;--tblr-btn-close-hover-opacity:0.75;--tblr-btn-close-focus-shadow:0 0 0 0.25rem rgba(var(--tblr-primary-rgb), 0.25);--tblr-btn-close-focus-opacity:1;--tblr-btn-close-disabled-opacity:0.25;--tblr-btn-close-size:1em;width:var(--tblr-btn-close-size);height:var(--tblr-btn-close-size);padding:.25em .25em;color:var(--tblr-btn-close-color);-webkit-mask:var(--tblr-btn-close-bg) no-repeat center/calc(var(--tblr-btn-close-size) * 0.75);mask:var(--tblr-btn-close-bg) no-repeat center/calc(var(--tblr-btn-close-size) * 0.75);background-color:var(--tblr-btn-close-color);border:0;border-radius:var(--tblr-border-radius);opacity:var(--tblr-btn-close-opacity);cursor:pointer;display:block}.btn-close:hover{color:var(--tblr-btn-close-color);text-decoration:none;opacity:var(--tblr-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--tblr-btn-close-focus-shadow);opacity:var(--tblr-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--tblr-btn-close-disabled-opacity)}.dropdown-menu{-webkit-user-select:none;-moz-user-select:none;user-select:none;background-clip:border-box}.dropdown-menu.card{padding:0;min-width:25rem;display:none}.dropdown-menu.card.show{display:flex}.dropdown-item{min-width:11rem;display:flex;align-items:center;margin:0;line-height:1.4285714286;gap:.5rem}.dropdown-item-icon{width:1.25rem!important;height:1.25rem!important;margin-right:.5rem;color:var(--tblr-secondary);opacity:.7;text-align:center}.dropdown-item-indicator{margin-right:.5rem;margin-left:-.25rem;height:1.25rem;display:inline-flex;line-height:1;vertical-align:bottom;align-items:center}.dropdown-header{font-size:.75rem;font-weight:var(--tblr-font-weight-medium);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-secondary);padding-bottom:.25rem;pointer-events:none}.dropdown-menu-scrollable{height:auto;max-height:13rem;overflow-x:hidden}.dropdown-menu-column{min-width:11rem}.dropdown-menu-column .dropdown-item{min-width:0}.dropdown-menu-columns{display:flex;flex:0 0.25rem}.dropdown-menu-arrow:before{content:"";position:absolute;top:-.25rem;left:.75rem;display:block;background:inherit;width:14px;height:14px;transform:rotate(45deg);transform-origin:center;border:1px solid;border-color:inherit;z-index:-1;clip:rect(0,9px,9px,0)}.dropdown-menu-arrow.dropdown-menu-end:before{right:.75rem;left:auto}.dropend>.dropdown-menu{margin-top:calc(-.25rem - 1px);margin-left:-.25rem}.dropend .dropdown-toggle:after{margin-left:auto}.dropdown-menu-card{padding:0;min-width:20rem}.dropdown-menu-card>.card{margin:0;border:0;box-shadow:none}.datagrid{--tblr-datagrid-padding:1.5rem;--tblr-datagrid-item-width:15rem;display:grid;grid-gap:var(--tblr-datagrid-padding);grid-template-columns:repeat(auto-fit,minmax(var(--tblr-datagrid-item-width),1fr))}.datagrid-title{font-size:.75rem;font-weight:var(--tblr-font-weight-medium);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-secondary);margin-bottom:.25rem}.empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:1rem;text-align:center}@media (min-width:768px){.empty{padding:3rem}}.empty-icon{margin:0 0 1rem;width:3rem;height:3rem;line-height:1;color:var(--tblr-secondary)}.empty-icon svg{width:100%;height:100%}.empty-img{margin:0 0 2rem;line-height:1}.empty-header{margin:0 0 1rem;font-size:4rem;font-weight:var(--tblr-font-weight-light);line-height:1;color:var(--tblr-secondary)}.empty-title{font-size:1.25rem;line-height:1.75rem;font-weight:var(--tblr-font-weight-bold)}.empty-subtitle,.empty-title{margin:0 0 .5rem}.empty-action{margin-top:1.5rem}.empty-bordered{border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);border-radius:var(--tblr-border-radius)}.row>*{min-width:0}.col-separator{border-left:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}.container-slim{--tblr-gutter-x:calc(var(--tblr-page-padding) * 2);--tblr-gutter-y:0;width:100%;padding-right:calc(var(--tblr-gutter-x) * .5);padding-left:calc(var(--tblr-gutter-x) * .5);margin-right:auto;margin-left:auto;max-width:16rem}.container-tight{--tblr-gutter-x:calc(var(--tblr-page-padding) * 2);--tblr-gutter-y:0;width:100%;padding-right:calc(var(--tblr-gutter-x) * .5);padding-left:calc(var(--tblr-gutter-x) * .5);margin-right:auto;margin-left:auto;max-width:30rem}.container-narrow{--tblr-gutter-x:calc(var(--tblr-page-padding) * 2);--tblr-gutter-y:0;width:100%;padding-right:calc(var(--tblr-gutter-x) * .5);padding-left:calc(var(--tblr-gutter-x) * .5);margin-right:auto;margin-left:auto;max-width:61.875rem}.row-0{margin-right:0;margin-left:0}.row-0>.col,.row-0>[class*=col-]{padding-right:0;padding-left:0}.row-0 .card{margin-bottom:0}.row-sm{margin-right:-.375rem;margin-left:-.375rem}.row-sm>.col,.row-sm>[class*=col-]{padding-right:.375rem;padding-left:.375rem}.row-sm .card{margin-bottom:.75rem}.row-md{margin-right:-1.5rem;margin-left:-1.5rem}.row-md>.col,.row-md>[class*=col-]{padding-right:1.5rem;padding-left:1.5rem}.row-md .card{margin-bottom:3rem}.row-lg{margin-right:-3rem;margin-left:-3rem}.row-lg>.col,.row-lg>[class*=col-]{padding-right:3rem;padding-left:3rem}.row-lg .card{margin-bottom:6rem}.row-deck>.col,.row-deck>[class*=col-]{display:flex;align-items:stretch}.row-deck>.col .card,.row-deck>[class*=col-] .card{flex:1 1 auto}.row-cards{--tblr-gutter-x:var(--tblr-page-padding);--tblr-gutter-y:var(--tblr-page-padding);min-width:0}.row-cards .row-cards{flex:1}.space-y{display:flex;flex-direction:column;gap:1rem}.space-x{display:flex;gap:1rem}.space-y-0{display:flex;flex-direction:column;gap:0}.space-x-0{display:flex;gap:0}.space-y-1{display:flex;flex-direction:column;gap:.25rem}.space-x-1{display:flex;gap:.25rem}.space-y-2{display:flex;flex-direction:column;gap:.5rem}.space-x-2{display:flex;gap:.5rem}.space-y-3{display:flex;flex-direction:column;gap:1rem}.space-x-3{display:flex;gap:1rem}.space-y-4{display:flex;flex-direction:column;gap:1.5rem}.space-x-4{display:flex;gap:1.5rem}.space-y-5{display:flex;flex-direction:column;gap:2rem}.space-x-5{display:flex;gap:2rem}.space-y-6{display:flex;flex-direction:column;gap:2.5rem}.space-x-6{display:flex;gap:2.5rem}.divide-y>:not(template)~:not(template){border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.divide-y>:not(template):not(:first-child){padding-top:1rem!important}.divide-y>:not(template):not(:last-child){padding-bottom:1rem!important}.divide-x>:not(template)~:not(template){border-left:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.divide-x>:not(template):not(:first-child){padding-left:1rem!important}.divide-x>:not(template):not(:last-child){padding-right:1rem!important}.divide-y-0>:not(template)~:not(template){border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.divide-y-0>:not(template):not(:first-child){padding-top:0!important}.divide-y-0>:not(template):not(:last-child){padding-bottom:0!important}.divide-x-0>:not(template)~:not(template){border-left:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.divide-x-0>:not(template):not(:first-child){padding-left:0!important}.divide-x-0>:not(template):not(:last-child){padding-right:0!important}.divide-y-1>:not(template)~:not(template){border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.divide-y-1>:not(template):not(:first-child){padding-top:.25rem!important}.divide-y-1>:not(template):not(:last-child){padding-bottom:.25rem!important}.divide-x-1>:not(template)~:not(template){border-left:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.divide-x-1>:not(template):not(:first-child){padding-left:.25rem!important}.divide-x-1>:not(template):not(:last-child){padding-right:.25rem!important}.divide-y-2>:not(template)~:not(template){border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.divide-y-2>:not(template):not(:first-child){padding-top:.5rem!important}.divide-y-2>:not(template):not(:last-child){padding-bottom:.5rem!important}.divide-x-2>:not(template)~:not(template){border-left:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.divide-x-2>:not(template):not(:first-child){padding-left:.5rem!important}.divide-x-2>:not(template):not(:last-child){padding-right:.5rem!important}.divide-y-3>:not(template)~:not(template){border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.divide-y-3>:not(template):not(:first-child){padding-top:1rem!important}.divide-y-3>:not(template):not(:last-child){padding-bottom:1rem!important}.divide-x-3>:not(template)~:not(template){border-left:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.divide-x-3>:not(template):not(:first-child){padding-left:1rem!important}.divide-x-3>:not(template):not(:last-child){padding-right:1rem!important}.divide-y-4>:not(template)~:not(template){border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.divide-y-4>:not(template):not(:first-child){padding-top:1.5rem!important}.divide-y-4>:not(template):not(:last-child){padding-bottom:1.5rem!important}.divide-x-4>:not(template)~:not(template){border-left:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.divide-x-4>:not(template):not(:first-child){padding-left:1.5rem!important}.divide-x-4>:not(template):not(:last-child){padding-right:1.5rem!important}.divide-y-5>:not(template)~:not(template){border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.divide-y-5>:not(template):not(:first-child){padding-top:2rem!important}.divide-y-5>:not(template):not(:last-child){padding-bottom:2rem!important}.divide-x-5>:not(template)~:not(template){border-left:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.divide-x-5>:not(template):not(:first-child){padding-left:2rem!important}.divide-x-5>:not(template):not(:last-child){padding-right:2rem!important}.divide-y-6>:not(template)~:not(template){border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.divide-y-6>:not(template):not(:first-child){padding-top:2.5rem!important}.divide-y-6>:not(template):not(:last-child){padding-bottom:2.5rem!important}.divide-x-6>:not(template)~:not(template){border-left:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)!important}.divide-x-6>:not(template):not(:first-child){padding-left:2.5rem!important}.divide-x-6>:not(template):not(:last-child){padding-right:2.5rem!important}.divide-y-fill{display:flex;flex-direction:column;height:100%}.divide-y-fill>:not(template){flex:1;display:flex;justify-content:center;flex-direction:column}.icon{--tblr-icon-size:1.25rem;width:var(--tblr-icon-size);height:var(--tblr-icon-size);font-size:var(--tblr-icon-size);vertical-align:bottom;stroke-width:1.5}.icon:hover{text-decoration:none}.icon-inline{--tblr-icon-size:1rem;vertical-align:-.2rem}.icon-filled{fill:currentColor}.icon-sm{--tblr-icon-size:1rem;stroke-width:1}.icon-md{--tblr-icon-size:2.5rem;stroke-width:1}.icon-lg{--tblr-icon-size:3.5rem;stroke-width:1}.icon-pulse{transition:all .15s ease 0s;animation:pulse 2s ease infinite;animation-fill-mode:both}.icon-tada{transition:all .15s ease 0s;animation:tada 3s ease infinite;animation-fill-mode:both}.icon-rotate{transition:all .15s ease 0s;animation:rotate-360 3s linear infinite;animation-fill-mode:both}.img-responsive{--tblr-img-responsive-ratio:75%;background:no-repeat center/cover;padding-top:var(--tblr-img-responsive-ratio)}.img-responsive-grid{padding-top:calc(var(--tblr-img-responsive-ratio) - var(--tblr-gutter-y)/ 2)}.img-responsive-1x1{--tblr-img-responsive-ratio:100%}.img-responsive-2x1{--tblr-img-responsive-ratio:50%}.img-responsive-1x2{--tblr-img-responsive-ratio:200%}.img-responsive-3x1{--tblr-img-responsive-ratio:33.3333333333%}.img-responsive-1x3{--tblr-img-responsive-ratio:300%}.img-responsive-4x1{--tblr-img-responsive-ratio:25%}.img-responsive-1x4{--tblr-img-responsive-ratio:400%}.img-responsive-4x3{--tblr-img-responsive-ratio:75%}.img-responsive-3x4{--tblr-img-responsive-ratio:133.3333333333%}.img-responsive-16x9{--tblr-img-responsive-ratio:56.25%}.img-responsive-9x16{--tblr-img-responsive-ratio:177.7777777778%}.img-responsive-21x9{--tblr-img-responsive-ratio:42.8571428571%}.img-responsive-9x21{--tblr-img-responsive-ratio:233.3333333333%}.img-bg{background:no-repeat center/cover}textarea[cols]{height:auto}.col-form-label,.form-label{display:block;font-weight:var(--tblr-font-weight-medium)}.col-form-label.required:after,.form-label.required:after{content:"*";margin-left:.25rem;color:#d63939}.form-label-description{float:right;font-weight:var(--tblr-font-weight-normal);color:var(--tblr-secondary)}.form-hint{display:block;color:var(--tblr-secondary)}.form-hint:last-child{margin-bottom:0}.form-hint+.form-control{margin-top:.25rem}.form-label+.form-hint{margin-top:-.25rem}.form-control+.form-hint,.form-select+.form-hint,.input-group+.form-hint{margin-top:.5rem;color:var(--tblr-secondary)}.form-select:-moz-focusring{color:var(--tblr-body-color)}.form-control:-webkit-autofill{box-shadow:0 0 0 1000px var(--tblr-bg-surface-secondary) inset;color:var(--tblr-body-color);-webkit-text-fill-color:var(--tblr-body-color)}.form-control.disabled,.form-control:disabled{color:var(--tblr-secondary);-webkit-user-select:none;-moz-user-select:none;user-select:none}.form-control[size]{width:auto}.form-control-light{background-color:var(--tblr-gray-100);border-color:transparent}.form-control-dark{background-color:rgba(0,0,0,.1);color:#fff;border-color:transparent}.form-control-dark:focus{background-color:rgba(0,0,0,.1);box-shadow:none;border-color:rgba(255,255,255,.24)}.form-control-dark::-moz-placeholder{color:rgba(255,255,255,.6)}.form-control-dark::placeholder{color:rgba(255,255,255,.6)}.form-control-rounded{border-radius:10rem}.form-control-flush{padding:0;background:0 0!important;border-color:transparent!important;resize:none;box-shadow:none!important;line-height:inherit}.form-footer{margin-top:2rem}.form-fieldset{padding:1rem;margin-bottom:1rem;background:var(--tblr-bg-surface-secondary);border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);border-radius:var(--tblr-border-radius)}fieldset:empty{display:none}.form-help{display:inline-flex;font-weight:var(--tblr-font-weight-bold);align-items:center;justify-content:center;width:1.125rem;height:1.125rem;font-size:.75rem;color:var(--tblr-secondary);text-align:center;text-decoration:none;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background:var(--tblr-gray-100);border-radius:100rem;transition:background-color .3s,color .3s}@media (prefers-reduced-motion:reduce){.form-help{transition:none}}.form-help:hover,.form-help[aria-describedby]{color:#fff;background:var(--tblr-primary)}.input-group{box-shadow:var(--tblr-shadow-input);border-radius:var(--tblr-border-radius)}.input-group .btn,.input-group .form-control{box-shadow:none}.input-group-link{font-size:.75rem}.input-group-flat:focus-within{box-shadow:0 0 0 .25rem rgba(var(--tblr-primary-rgb),.25);border-radius:var(--tblr-border-radius)}.input-group-flat:focus-within .form-control,.input-group-flat:focus-within .input-group-text{border-color:rgb(130.5,183,232)!important}.input-group-flat .form-control:focus{border-color:var(--tblr-border-color);box-shadow:none}.input-group-flat .form-control:not(:last-child){border-right:0}.input-group-flat .form-control:not(:first-child){border-left:0}.input-group-flat .input-group-text{background:var(--tblr-bg-forms);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.input-group-flat .input-group-text{transition:none}}.input-group-flat .input-group-text:first-child{padding-right:0}.input-group-flat .input-group-text:last-child{padding-left:0}.form-file-button{margin-left:0;border-left:0}label[for=floating-input]{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media (max-width:575.98px){.form-control,.form-select{font-size:1rem}}.input-icon{position:relative}.input-icon .form-control:not(:last-child),.input-icon .form-select:not(:last-child){padding-right:2.5rem}.input-icon .form-control:not(:first-child),.input-icon .form-select:not(:last-child){padding-left:2.5rem}.input-icon-addon{position:absolute;top:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;min-width:2.5rem;color:var(--tblr-icon-color);pointer-events:none;font-size:1.2em}.input-icon-addon:last-child{right:0;left:auto}.form-colorinput{position:relative;display:inline-block;margin:0;line-height:1;cursor:pointer}.form-colorinput-input{position:absolute;z-index:-1;opacity:0}.form-colorinput-color{display:block;width:1.5rem;height:1.5rem;color:#fff;border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent);border-radius:var(--tblr-border-radius);box-shadow:0 1px 2px 0 rgba(0,0,0,.05)}.form-colorinput-color:before{position:absolute;top:0;left:0;width:100%;height:100%;content:"";background:no-repeat center center/1.25rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8.5l2.5 2.5l5.5 -5.5'/%3e%3c/svg%3e");opacity:0;transition:opacity .3s}@media (prefers-reduced-motion:reduce){.form-colorinput-color:before{transition:none}}.form-colorinput-input:checked~.form-colorinput-color:before{opacity:1}.form-colorinput-input:focus~.form-colorinput-color{border-color:var(--tblr-primary);box-shadow:0 0 0 .25rem rgba(var(--tblr-primary-rgb),.25)}.form-colorinput-light .form-colorinput-color:before{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16'%3e%3cpath fill='none' stroke='%231f2937' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8.5l2.5 2.5l5.5 -5.5'/%3e%3c/svg%3e")}.form-imagecheck{--tblr-form-imagecheck-radius:var(--tblr-border-radius);position:relative;margin:0;cursor:pointer}.form-imagecheck-input{position:absolute;z-index:-1;opacity:0}.form-imagecheck-figure{position:relative;display:block;margin:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);border-radius:var(--tblr-form-imagecheck-radius)}.form-imagecheck-input:focus~.form-imagecheck-figure{border-color:var(--tblr-primary);box-shadow:0 0 0 .25rem rgba(var(--tblr-primary-rgb),.25)}.form-imagecheck-input:checked~.form-imagecheck-figure{border-color:var(--tblr-primary)}.form-imagecheck-figure:before{position:absolute;top:.25rem;left:.25rem;z-index:1;display:block;width:1.25rem;height:1.25rem;color:#fff;pointer-events:none;content:"";-webkit-user-select:none;-moz-user-select:none;user-select:none;background:var(--tblr-bg-forms);border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);border-radius:var(--tblr-border-radius);transition:opacity .3s}@media (prefers-reduced-motion:reduce){.form-imagecheck-figure:before{transition:none}}.form-imagecheck-input:checked~.form-imagecheck-figure:before{background-color:var(--tblr-primary);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8.5l2.5 2.5l5.5 -5.5'/%3e%3c/svg%3e");background-repeat:repeat;background-position:center;background-size:1.25rem;border-color:var(--tblr-border-color-translucent)}.form-imagecheck-input[type=radio]~.form-imagecheck-figure:before{border-radius:50%}.form-imagecheck-input[type=radio]:checked~.form-imagecheck-figure:before{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3ccircle r='3' fill='%23ffffff' cx='8' cy='8' /%3e%3c/svg%3e")}.form-imagecheck-image{max-width:100%;display:block;opacity:.64;transition:opacity .3s}@media (prefers-reduced-motion:reduce){.form-imagecheck-image{transition:none}}.form-imagecheck-image:first-child{border-top-left-radius:calc(var(--tblr-form-imagecheck-radius) - 1px);border-top-right-radius:calc(var(--tblr-form-imagecheck-radius) - 1px)}.form-imagecheck-image:last-child{border-bottom-right-radius:calc(var(--tblr-form-imagecheck-radius) - 1px);border-bottom-left-radius:calc(var(--tblr-form-imagecheck-radius) - 1px)}.form-imagecheck-input:checked~.form-imagecheck-figure .form-imagecheck-image,.form-imagecheck-input:focus~.form-imagecheck-figure .form-imagecheck-image,.form-imagecheck:hover .form-imagecheck-image{opacity:1}.form-imagecheck-caption{padding:.25rem;font-size:.765625rem;color:var(--tblr-secondary);text-align:center;transition:color .3s}@media (prefers-reduced-motion:reduce){.form-imagecheck-caption{transition:none}}.form-imagecheck-input:checked~.form-imagecheck-figure .form-imagecheck-caption,.form-imagecheck-input:focus~.form-imagecheck-figure .form-imagecheck-caption,.form-imagecheck:hover .form-imagecheck-caption{color:var(--tblr-body-color)}.form-selectgroup{display:inline-flex;margin:0 -.5rem -.5rem 0;flex-wrap:wrap}.form-selectgroup .form-selectgroup-item{margin:0 .5rem .5rem 0}.form-selectgroup-vertical{flex-direction:column}.form-selectgroup-item{display:block;position:relative}.form-selectgroup-input{position:absolute;top:0;left:0;z-index:-1;opacity:0}.form-selectgroup-label{position:relative;display:block;min-width:calc(1.25rem + 1.125rem + calc(var(--tblr-border-width) * 2));margin:0;padding:.5625rem 1rem;font-size:.875rem;line-height:1.25rem;color:var(--tblr-secondary);background:var(--tblr-bg-forms);text-align:center;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);border-radius:var(--tblr-border-radius);box-shadow:var(--tblr-shadow-input);transition:border-color .3s,background .3s,color .3s}@media (prefers-reduced-motion:reduce){.form-selectgroup-label{transition:none}}.form-selectgroup-label .icon:only-child{margin:0 -.25rem}.form-selectgroup-label:hover{color:var(--tblr-body-color)}.form-selectgroup-check{display:inline-block;width:1.25rem;height:1.25rem;border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent);vertical-align:middle;box-shadow:var(--tblr-shadow-input)}.form-selectgroup-input[type=checkbox]+.form-selectgroup-label .form-selectgroup-check{border-radius:var(--tblr-border-radius)}.form-selectgroup-input[type=radio]+.form-selectgroup-label .form-selectgroup-check{border-radius:50%}.form-selectgroup-input:checked+.form-selectgroup-label .form-selectgroup-check{background-color:var(--tblr-primary);background-repeat:repeat;background-position:center;background-size:1.25rem;border-color:var(--tblr-border-color-translucent)}.form-selectgroup-input[type=checkbox]:checked+.form-selectgroup-label .form-selectgroup-check{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8.5l2.5 2.5l5.5 -5.5'/%3e%3c/svg%3e")}.form-selectgroup-input[type=radio]:checked+.form-selectgroup-label .form-selectgroup-check{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3ccircle r='3' fill='%23ffffff' cx='8' cy='8' /%3e%3c/svg%3e")}.form-selectgroup-check-floated{position:absolute;top:.5625rem;right:.5625rem}.form-selectgroup-input:checked+.form-selectgroup-label{z-index:1;color:var(--tblr-primary);background:rgba(var(--tblr-primary-rgb),.04);border-color:var(--tblr-primary)}.form-selectgroup-input:focus+.form-selectgroup-label{z-index:2;color:var(--tblr-primary);border-color:var(--tblr-primary);box-shadow:0 0 0 .25rem rgba(var(--tblr-primary-rgb),.25)}.form-selectgroup-boxes .form-selectgroup-label{text-align:left;padding:1.25rem 1rem;color:inherit}.form-selectgroup-boxes .form-selectgroup-input:checked+.form-selectgroup-label{color:inherit}.form-selectgroup-boxes .form-selectgroup-input:checked+.form-selectgroup-label .form-selectgroup-title{color:var(--tblr-primary)}.form-selectgroup-boxes .form-selectgroup-input:checked+.form-selectgroup-label .form-selectgroup-label-content{opacity:1}.form-selectgroup-pills{flex-wrap:wrap;align-items:flex-start}.form-selectgroup-pills .form-selectgroup-item{flex-grow:0}.form-selectgroup-pills .form-selectgroup-label{border-radius:50px}.form-control-color::-webkit-color-swatch{border:none}[type=search]::-webkit-search-cancel-button{-webkit-appearance:none}.form-control::file-selector-button{background-color:var(--tblr-btn-color,var(--tblr-tertiary-bg))}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--tblr-btn-color,var(--tblr-secondary-bg))}.form-check{-webkit-user-select:none;-moz-user-select:none;user-select:none}.form-check.form-check-highlight .form-check-input:not(:checked)~.form-check-label{color:var(--tblr-secondary)}.form-check .form-check-label-off{color:var(--tblr-secondary)}.form-check .form-check-input:checked~.form-check-label-off{display:none}.form-check .form-check-input:not(:checked)~.form-check-label-on{display:none}.form-check-input{background-size:1.25rem;margin-top:0;box-shadow:var(--tblr-shadow-input)}.form-switch .form-check-input{transition:background-color .3s,background-position .3s}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-check-label{display:block}.form-check-label.required:after{content:"*";margin-left:.25rem;color:#d63939}.form-check-description{display:block;color:var(--tblr-secondary);font-size:.75rem;margin-top:.25rem}.form-check-single{margin:0}.form-check-single .form-check-input{margin:0}.form-switch .form-check-input{height:1.25rem;margin-top:0}.form-switch-lg{padding-left:3.5rem;min-height:1.5rem}.form-switch-lg .form-check-input{height:1.5rem;width:2.75rem;background-size:1.5rem;margin-left:-3.5rem}.form-switch-lg .form-check-label{padding-top:.125rem}.form-check-input:checked{border:none}.form-control.is-invalid-lite,.form-control.is-valid-lite,.form-select.is-invalid-lite,.form-select.is-valid-lite{border-color:var(--tblr-border-color)!important}.legend{--tblr-legend-size:0.75em;display:inline-block;background:var(--tblr-border-color);width:var(--tblr-legend-size);height:var(--tblr-legend-size);border-radius:var(--tblr-border-radius-sm);border:1px solid var(--tblr-border-color-translucent)}.list-group{margin-left:0;margin-right:0}.list-group-header{background:var(--tblr-bg-surface-tertiary);padding:.5rem 1.25rem;font-size:.75rem;font-weight:var(--tblr-font-weight-medium);line-height:1;text-transform:uppercase;color:var(--tblr-gray-500);border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}.list-group-flush>.list-group-header:last-child{border-bottom-width:0}.list-group-item{background-color:inherit}.list-group-item.active{background-color:rgba(var(--tblr-secondary-rgb),.08);border-left-color:#066fd1;border-left-width:2px}.list-group-item.disabled,.list-group-item:disabled{color:#6b7280;background-color:rgba(var(--tblr-secondary-rgb),.08)}.list-bordered .list-item{border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);margin-top:-1px}.list-bordered .list-item:first-child{border-top:none}.list-group-hoverable .list-group-item:active,.list-group-hoverable .list-group-item:focus,.list-group-hoverable .list-group-item:hover{background-color:rgba(var(--tblr-secondary-rgb),.08)}.list-group-hoverable .list-group-item-actions{opacity:0;transition:opacity .3s}@media (prefers-reduced-motion:reduce){.list-group-hoverable .list-group-item-actions{transition:none}}.list-group-hoverable .list-group-item-actions.show,.list-group-hoverable .list-group-item:hover .list-group-item-actions{opacity:1}.list-group-transparent{--tblr-list-group-border-radius:0;margin:0 -1.25rem}.list-group-transparent .list-group-item{background:0 0;border:0}.list-group-transparent .list-group-item .icon{color:var(--tblr-secondary)}.list-group-transparent .list-group-item.active{font-weight:var(--tblr-font-weight-bold);color:inherit;background:var(--tblr-active-bg)}.list-group-transparent .list-group-item.active .icon{color:inherit}.list-separated{display:flex;flex-direction:column;gap:1rem}.list-inline{margin:0}.list-inline-item:not(:last-child){margin-right:auto;margin-inline-end:.5rem}.list-inline-dots .list-inline-item+.list-inline-item:before{content:" · ";margin-inline-end:.5rem}.loader{position:relative;display:block;width:2.5rem;height:2.5rem;color:#066fd1;vertical-align:middle}.loader:after{position:absolute;top:0;left:0;width:100%;height:100%;content:"";border:1px var(--tblr-border-style);border-color:transparent;border-top-color:currentColor;border-left-color:currentColor;border-radius:100rem;animation:rotate-360 .6s linear;animation-iteration-count:infinite}.dimmer{position:relative}.dimmer .loader{position:absolute;top:50%;right:0;left:0;display:none;margin:0 auto;transform:translateY(-50%)}.dimmer.active .loader{display:block}.dimmer.active .dimmer-content{pointer-events:none;opacity:.1}@keyframes animated-dots{0%{transform:translateX(-100%)}}.animated-dots{display:inline-block;overflow:hidden;vertical-align:bottom}.animated-dots:after{display:inline-block;content:"...";animation:animated-dots 1.2s steps(4,jump-none) infinite}.modal-content>.btn-close,.modal-header>.btn-close{position:absolute;top:0;right:0;width:3.5rem;height:3.5rem;margin:0;padding:0;z-index:10}.modal-body{scrollbar-color:color-mix(in srgb,var(--tblr-scrollbar-color,var(--tblr-body-color)) 20%,transparent) transparent}.modal-body::-webkit-scrollbar{width:1rem;height:1rem;-webkit-transition:background .3s;transition:background .3s}@media (prefers-reduced-motion:reduce){.modal-body::-webkit-scrollbar{-webkit-transition:none;transition:none}}.modal-body::-webkit-scrollbar-thumb{border-radius:1rem;border:5px solid transparent;box-shadow:inset 0 0 0 1rem color-mix(in srgb,var(--tblr-scrollbar-color,var(--tblr-body-color)) 20%,transparent)}.modal-body::-webkit-scrollbar-track{background:0 0}.modal-body:hover::-webkit-scrollbar-thumb{box-shadow:inset 0 0 0 1rem color-mix(in srgb,var(--tblr-scrollbar-color,var(--tblr-body-color)) 40%,transparent)}.modal-body::-webkit-scrollbar-corner{background:0 0}.modal-body .modal-title{margin-bottom:1rem}.modal-body+.modal-body{border-top:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}.modal-status{position:absolute;top:0;left:0;right:0;height:2px;background:var(--tblr-secondary);border-radius:var(--tblr-border-radius-lg) var(--tblr-border-radius-lg) 0 0}.modal-header{align-items:center;min-height:3.5rem;background:0 0;padding:0 3.5rem 0 1.5rem}.modal-title{font-size:1rem;font-weight:var(--tblr-font-weight-bold);color:inherit;line-height:1.4285714286}.modal-footer{padding-top:.75rem;padding-bottom:.75rem}.modal-blur{-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.modal-full-width{max-width:none;margin:0 .5rem}.nav{--tblr-nav-link-hover-bg:color-mix(in srgb, var(--tblr-nav-link-color) 4%, transparent)}.nav-vertical,.nav-vertical .nav{flex-direction:column;flex-wrap:nowrap}.nav-vertical .nav{margin-left:1.25rem;border-left:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);padding-left:.5rem}.nav-vertical .nav-item.show .nav-link,.nav-vertical .nav-link.active{font-weight:var(--tblr-font-weight-bold);color:var(--tblr-nav-link-active-color)}.nav-vertical.nav-pills{margin:0 -.75rem}.nav-bordered{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}.nav-bordered .nav-item+.nav-item{margin-left:1.25rem}.nav-bordered .nav-link{padding-left:0;padding-right:0;margin:0 0 calc(-1 * var(--tblr-border-width));border:0;border-bottom:2px var(--tblr-border-style) transparent}.nav-bordered .nav-link:hover{background-color:transparent}.nav-bordered .nav-item.show .nav-link,.nav-bordered .nav-link.active{color:var(--tblr-primary);border-color:var(--tblr-primary)}.nav-underline .nav-link{border-radius:0}.nav-link{display:flex;transition:color .3s,background-color .3s;align-items:center}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{background-color:var(--tblr-nav-link-hover-bg)}.nav-link-toggle{margin-left:auto;padding:0 .25rem;transition:transform .3s}@media (prefers-reduced-motion:reduce){.nav-link-toggle{transition:none}}.nav-link-toggle:after{content:"";display:inline-block;vertical-align:.306em;width:.36em;height:.36em;border-bottom:1px var(--tblr-border-style);border-left:1px var(--tblr-border-style);margin-right:.1em;margin-left:.4em;transform:rotate(-45deg)}.nav-link-toggle:after{margin:0}.nav-link[aria-expanded=true] .nav-link-toggle{transform:rotate(180deg)}.nav-link-icon{width:1.25rem;height:1.25rem;margin-right:.5rem;color:inherit}.nav-link-icon svg{display:block;height:100%}.nav-fill .nav-item .nav-link{justify-content:center}.stars{display:inline-flex;color:#9ca3af;font-size:.75rem}.stars .star:not(:first-child){margin-left:.25rem}.pagination{margin:0;--tblr-pagination-gap:.25rem;-webkit-user-select:none;-moz-user-select:none;user-select:none;gap:var(--tblr-pagination-gap);line-height:var(--tblr-body-line-height)}.page-link{min-width:2rem;border-radius:var(--tblr-pagination-border-radius)}.page-item:not(.active) .page-link:hover{background:var(--tblr-pagination-hover-bg)}.page-text{padding-left:.5rem;padding-right:.5rem}.page-item{text-align:center}.page-item.page-next,.page-item.page-prev{flex:0 0 50%;text-align:left}.page-item.page-next{margin-left:auto;text-align:right}.page-item-subtitle{margin-bottom:2px;font-size:12px;color:var(--tblr-secondary);text-transform:uppercase}.page-item.disabled .page-item-subtitle{color:var(--tblr-disabled-color)}.page-item-title{font-size:1rem;font-weight:var(--tblr-font-weight-normal);color:var(--tblr-body-color)}.page-link:hover .page-item-title{color:#066fd1}.page-item.disabled .page-item-title{color:var(--tblr-disabled-color)}.pagination-outline{--tblr-pagination-border-color:var(--tblr-border-color);--tblr-pagination-disabled-border-color:var(--tblr-border-color);--tblr-pagination-border-width:1px}.pagination-circle{--tblr-pagination-border-radius:var(--tblr-border-radius-pill)}@keyframes progress-indeterminate{0%{right:100%;left:-35%}100%,60%{right:-90%;left:100%}}.progress{position:relative;width:100%;line-height:.5rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.progress::-webkit-progress-bar{background:var(--tblr-progress-bg)}.progress::-webkit-progress-value{background-color:var(--tblr-primary)}.progress::-moz-progress-bar{background-color:var(--tblr-primary)}.progress::-ms-fill{background-color:var(--tblr-primary);border:none}.progress-sm{height:.25rem}.progress-bar{height:100%}.progress-bar-indeterminate:after,.progress-bar-indeterminate:before{position:absolute;top:0;bottom:0;left:0;content:"";background-color:inherit;will-change:left,right}.progress-bar-indeterminate:before{animation:progress-indeterminate 1.5s cubic-bezier(.65,.815,.735,.395) infinite}.progress-separated .progress-bar{box-shadow:0 0 0 2px var(--tblr-card-bg,var(--tblr-bg-surface))}.progressbg{position:relative;padding:.25rem .5rem;display:flex}.progressbg-text{position:relative;z-index:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.progressbg-progress{position:absolute;top:0;right:0;bottom:0;left:0;z-index:0;height:100%;background:0 0;pointer-events:none}.progressbg-value{font-weight:var(--tblr-font-weight-medium);margin-left:auto;padding-left:2rem}.ribbon{--tblr-ribbon-margin:0.25rem;--tblr-ribbon-border-radius:var(--tblr-border-radius);position:absolute;top:.75rem;right:calc(-1 * var(--tblr-ribbon-margin));z-index:1;padding:.25rem .75rem;font-size:.625rem;font-weight:var(--tblr-font-weight-bold);line-height:1;color:#fff;text-align:center;text-transform:uppercase;background:var(--tblr-primary);border-color:var(--tblr-primary);border-radius:var(--tblr-ribbon-border-radius) 0 var(--tblr-ribbon-border-radius) var(--tblr-ribbon-border-radius);display:inline-flex;align-items:center;justify-content:center;min-height:2rem;min-width:2rem}.ribbon:before{position:absolute;right:0;bottom:100%;width:0;height:0;content:"";filter:brightness(70%);border:calc(var(--tblr-ribbon-margin) * .5) var(--tblr-border-style);border-color:inherit;border-top-color:transparent;border-right-color:transparent}.ribbon.bg-blue{border-color:var(--tblr-blue)}.ribbon.bg-blue-lt{border-color:rgba(var(--tblr-blue-rgb),.1)!important}.ribbon.bg-azure{border-color:var(--tblr-azure)}.ribbon.bg-azure-lt{border-color:rgba(var(--tblr-azure-rgb),.1)!important}.ribbon.bg-indigo{border-color:var(--tblr-indigo)}.ribbon.bg-indigo-lt{border-color:rgba(var(--tblr-indigo-rgb),.1)!important}.ribbon.bg-purple{border-color:var(--tblr-purple)}.ribbon.bg-purple-lt{border-color:rgba(var(--tblr-purple-rgb),.1)!important}.ribbon.bg-pink{border-color:var(--tblr-pink)}.ribbon.bg-pink-lt{border-color:rgba(var(--tblr-pink-rgb),.1)!important}.ribbon.bg-red{border-color:var(--tblr-red)}.ribbon.bg-red-lt{border-color:rgba(var(--tblr-red-rgb),.1)!important}.ribbon.bg-orange{border-color:var(--tblr-orange)}.ribbon.bg-orange-lt{border-color:rgba(var(--tblr-orange-rgb),.1)!important}.ribbon.bg-yellow{border-color:var(--tblr-yellow)}.ribbon.bg-yellow-lt{border-color:rgba(var(--tblr-yellow-rgb),.1)!important}.ribbon.bg-lime{border-color:var(--tblr-lime)}.ribbon.bg-lime-lt{border-color:rgba(var(--tblr-lime-rgb),.1)!important}.ribbon.bg-green{border-color:var(--tblr-green)}.ribbon.bg-green-lt{border-color:rgba(var(--tblr-green-rgb),.1)!important}.ribbon.bg-teal{border-color:var(--tblr-teal)}.ribbon.bg-teal-lt{border-color:rgba(var(--tblr-teal-rgb),.1)!important}.ribbon.bg-cyan{border-color:var(--tblr-cyan)}.ribbon.bg-cyan-lt{border-color:rgba(var(--tblr-cyan-rgb),.1)!important}.ribbon .icon{width:1.25rem;height:1.25rem;font-size:1.25rem}.ribbon-top{top:calc(-1 * var(--tblr-ribbon-margin));right:.75rem;width:2rem;padding:.5rem 0;border-radius:0 var(--tblr-ribbon-border-radius) var(--tblr-ribbon-border-radius) var(--tblr-ribbon-border-radius)}.ribbon-top:before{top:0;right:100%;bottom:auto;border-color:inherit;border-top-color:transparent;border-left-color:transparent}.ribbon-top.ribbon-start{right:auto;left:.75rem}.ribbon-top.ribbon-start:before{top:0;right:100%;left:auto}.ribbon-start{right:auto;left:calc(-1 * var(--tblr-ribbon-margin));border-radius:0 var(--tblr-ribbon-border-radius) var(--tblr-ribbon-border-radius) var(--tblr-ribbon-border-radius)}.ribbon-start:before{top:auto;bottom:100%;left:0;border-color:inherit;border-top-color:transparent;border-left-color:transparent}.ribbon-bottom{top:auto;bottom:.75rem}.ribbon-bookmark{padding-left:.25rem;border-radius:0 0 var(--tblr-ribbon-border-radius) 0}.ribbon-bookmark:after{position:absolute;top:0;right:100%;display:block;width:0;height:0;content:"";border:1rem var(--tblr-border-style);border-color:inherit;border-right-width:0;border-left-color:transparent;border-left-width:.5rem}.ribbon-bookmark.ribbon-left{padding-right:.5rem}.ribbon-bookmark.ribbon-left:after{right:auto;left:100%;border-right-color:transparent;border-right-width:.5rem;border-left-width:0}.ribbon-bookmark.ribbon-top{padding-right:0;padding-bottom:.25rem;padding-left:0;border-radius:0 var(--tblr-ribbon-border-radius) 0 0}.ribbon-bookmark.ribbon-top:after{top:100%;right:0;left:0;border-color:inherit;border-width:1rem;border-top-width:0;border-bottom-color:transparent;border-bottom-width:.5rem}.markdown{line-height:2}.markdown>:first-child{margin-top:0}.markdown>:last-child,.markdown>:last-child .highlight{margin-bottom:0}@media (min-width:768px){.markdown>.hr,.markdown>hr{margin-top:3em;margin-bottom:3em}}.markdown>.h1,.markdown>.h2,.markdown>.h3,.markdown>.h4,.markdown>.h5,.markdown>.h6,.markdown>h1,.markdown>h2,.markdown>h3,.markdown>h4,.markdown>h5,.markdown>h6{font-weight:var(--tblr-font-weight-bold)}.markdown>.h2,.markdown>.h3,.markdown>.h4,.markdown>.h5,.markdown>.h6,.markdown>h2,.markdown>h3,.markdown>h4,.markdown>h5,.markdown>h6{margin-top:2.5rem}.markdown>table{font-size:var(--tblr-body-font-size)}.markdown>blockquote{font-size:1rem;margin:1.5rem 0;padding:.5rem 1.5rem}.markdown>img,.markdown>p>img{border-radius:var(--tblr-border-radius);border:1px solid var(--tblr-border-color)}.markdown pre{max-height:20rem}.placeholder:not(.btn):not([class*=bg-]){background-color:currentColor!important}.placeholder:not(.avatar):not([class*=card-img-]){border-radius:var(--tblr-border-radius)}.nav-segmented{--tblr-nav-bg:var(--tblr-bg-surface-tertiary);--tblr-nav-padding:2px;--tblr-nav-height:2.5rem;--tblr-nav-gap:.25rem;--tblr-nav-active-bg:var(--tblr-bg-surface);--tblr-nav-font-size:inherit;--tblr-nav-radius:6px;--tblr-nav-link-disabled-color:var(--tblr-disabled-color);--tblr-nav-link-gap:.25rem;--tblr-nav-link-padding-x:.75rem;--tblr-nav-link-icon-size:1.25rem;display:inline-flex;flex-wrap:wrap;gap:var(--tblr-nav-gap);padding:var(--tblr-nav-padding);list-style:none;background:var(--tblr-nav-bg);border-radius:calc(var(--tblr-nav-radius) + var(--tblr-nav-padding));box-shadow:inset 0 0 0 1px rgba(0,0,0,.04)}.nav-segmented .nav-link{display:inline-flex;gap:calc(.25rem + var(--tblr-nav-link-gap));align-items:center;margin:0;font-size:var(--tblr-nav-font-size);min-width:calc(var(--tblr-nav-height) - 2 * var(--tblr-nav-padding));height:calc(var(--tblr-nav-height) - 2 * var(--tblr-nav-padding));padding:0 calc(var(--tblr-nav-link-padding-x) - 2px);border:1px solid transparent;background:0 0;color:var(--tblr-secondary);text-align:center;text-decoration:none;white-space:nowrap;cursor:pointer;transition:background-color .3s,color .3s;border-radius:var(--tblr-nav-radius);flex-grow:1;justify-content:center}.nav-segmented .nav-link.hover,.nav-segmented .nav-link:hover{background:rgba(0,0,0,.04);color:var(--tblr-body-color)}.nav-segmented .nav-link.disabled,.nav-segmented .nav-link:disabled{color:var(--tblr-nav-link-disabled-color);cursor:not-allowed}.nav-segmented .nav-link-input:checked+.nav-link,.nav-segmented .nav-link.active{color:var(--tblr-body-color);background:var(--tblr-nav-active-bg);border-color:var(--tblr-border-color)}.nav-segmented .nav-link-input{display:none}.nav-segmented .nav-link-icon{width:var(--tblr-nav-link-icon-size);height:var(--tblr-nav-link-icon-size);margin:0 -.25rem;color:inherit}.nav-segmented-vertical{flex-direction:column}.nav-segmented-vertical .nav-link{justify-content:flex-start}.nav-sm{--tblr-nav-height:2rem;--tblr-nav-font-size:var(--tblr-font-size-h5);--tblr-nav-radius:4px;--tblr-nav-link-padding-x:.5rem;--tblr-nav-link-gap:.25rem;--tblr-nav-link-icon-size:1rem}.nav-lg{--tblr-nav-height:3rem;--tblr-nav-font-size:var(--tblr-font-size-h3);--tblr-nav-radius:8px;--tblr-nav-link-padding-x:1rem;--tblr-nav-link-gap:.5rem;--tblr-nav-link-icon-size:1.5rem}.steps{--tblr-steps-color:var(--tblr-primary);--tblr-steps-inactive-color:var(--tblr-border-color);--tblr-steps-dot-size:.5rem;--tblr-steps-border-width:2px;display:flex;flex-wrap:nowrap;width:100%;padding:0;margin:0;list-style:none}.steps-blue{--tblr-steps-color:var(--tblr-blue)}.steps-blue-lt{--tblr-steps-color:var(--tblr-blue-lt)}.steps-azure{--tblr-steps-color:var(--tblr-azure)}.steps-azure-lt{--tblr-steps-color:var(--tblr-azure-lt)}.steps-indigo{--tblr-steps-color:var(--tblr-indigo)}.steps-indigo-lt{--tblr-steps-color:var(--tblr-indigo-lt)}.steps-purple{--tblr-steps-color:var(--tblr-purple)}.steps-purple-lt{--tblr-steps-color:var(--tblr-purple-lt)}.steps-pink{--tblr-steps-color:var(--tblr-pink)}.steps-pink-lt{--tblr-steps-color:var(--tblr-pink-lt)}.steps-red{--tblr-steps-color:var(--tblr-red)}.steps-red-lt{--tblr-steps-color:var(--tblr-red-lt)}.steps-orange{--tblr-steps-color:var(--tblr-orange)}.steps-orange-lt{--tblr-steps-color:var(--tblr-orange-lt)}.steps-yellow{--tblr-steps-color:var(--tblr-yellow)}.steps-yellow-lt{--tblr-steps-color:var(--tblr-yellow-lt)}.steps-lime{--tblr-steps-color:var(--tblr-lime)}.steps-lime-lt{--tblr-steps-color:var(--tblr-lime-lt)}.steps-green{--tblr-steps-color:var(--tblr-green)}.steps-green-lt{--tblr-steps-color:var(--tblr-green-lt)}.steps-teal{--tblr-steps-color:var(--tblr-teal)}.steps-teal-lt{--tblr-steps-color:var(--tblr-teal-lt)}.steps-cyan{--tblr-steps-color:var(--tblr-cyan)}.steps-cyan-lt{--tblr-steps-color:var(--tblr-cyan-lt)}.step-item{position:relative;flex:1 1 0;min-height:1rem;margin-top:0;color:inherit;text-align:center;cursor:default;padding-top:calc(var(--tblr-steps-dot-size))}a.step-item{cursor:pointer}a.step-item:hover{color:inherit}.step-item:after,.step-item:before{background:var(--tblr-steps-color)}.step-item:not(:last-child):after{position:absolute;left:50%;width:100%;content:"";transform:translateY(-50%)}.step-item:after{top:calc(var(--tblr-steps-dot-size) * .5);height:var(--tblr-steps-border-width)}.step-item:before{content:"";position:absolute;top:0;left:50%;z-index:1;box-sizing:content-box;display:flex;align-items:center;justify-content:center;border-radius:100rem;transform:translateX(-50%);color:var(--tblr-white);width:var(--tblr-steps-dot-size);height:var(--tblr-steps-dot-size)}.step-item.active{font-weight:var(--tblr-font-weight-bold)}.step-item.active:after{background:var(--tblr-steps-inactive-color)}.step-item.active~.step-item{color:var(--tblr-disabled-color)}.step-item.active~.step-item:after,.step-item.active~.step-item:before{background:var(--tblr-steps-inactive-color)}.steps-counter{--tblr-steps-dot-size:1.5rem;counter-reset:steps}.steps-counter .step-item{counter-increment:steps}.steps-counter .step-item:before{content:counter(steps)}.steps-vertical{--tblr-steps-dot-offset:6px;flex-direction:column}.steps-vertical.steps-counter{--tblr-steps-dot-offset:-2px}.steps-vertical .step-item{text-align:left;padding-top:0;padding-left:calc(var(--tblr-steps-dot-size) + 1rem);min-height:auto}.steps-vertical .step-item:not(:first-child){margin-top:1rem}.steps-vertical .step-item:before{top:var(--tblr-steps-dot-offset);left:0;transform:translate(0,0)}.steps-vertical .step-item:not(:last-child):after{position:absolute;content:"";transform:translateX(-50%);top:var(--tblr-steps-dot-offset);left:calc(var(--tblr-steps-dot-size) * .5);width:var(--tblr-steps-border-width);height:calc(100% + 1rem)}@keyframes status-pulsate-main{40%{transform:scale(1.25,1.25)}60%{transform:scale(1.25,1.25)}}@keyframes status-pulsate-secondary{10%{transform:scale(1,1)}30%{transform:scale(3,3)}80%{transform:scale(3,3)}100%{transform:scale(1,1)}}@keyframes status-pulsate-tertiary{25%{transform:scale(1,1)}80%{transform:scale(3,3);opacity:0}100%{transform:scale(3,3);opacity:0}}.status{--tblr-status-height:1.5rem;--tblr-status-color:#6b7280;--tblr-status-color-rgb:107,114,128;display:inline-flex;align-items:center;height:var(--tblr-status-height);padding:.25rem .75rem;gap:.5rem;color:var(--tblr-status-color);background:rgba(var(--tblr-status-color-rgb),.1);font-size:.875rem;text-transform:none;letter-spacing:normal;border-radius:100rem;font-weight:var(--tblr-font-weight-medium);line-height:1;margin:0}.status .status-dot{background:var(--tblr-status-color)}.status .icon{font-size:1.25rem}.status-lite{border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)!important;background:0 0!important;color:var(--tblr-body-color)!important}.status-primary{--tblr-status-color:#066fd1;--tblr-status-color-rgb:6,111,209}.status-secondary{--tblr-status-color:#6b7280;--tblr-status-color-rgb:107,114,128}.status-success{--tblr-status-color:#2fb344;--tblr-status-color-rgb:47,179,68}.status-info{--tblr-status-color:#4299e1;--tblr-status-color-rgb:66,153,225}.status-warning{--tblr-status-color:#f59f00;--tblr-status-color-rgb:245,159,0}.status-danger{--tblr-status-color:#d63939;--tblr-status-color-rgb:214,57,57}.status-light{--tblr-status-color:#f9fafb;--tblr-status-color-rgb:249,250,251}.status-dark{--tblr-status-color:#1f2937;--tblr-status-color-rgb:31,41,55}.status-muted{--tblr-status-color:#6b7280;--tblr-status-color-rgb:107,114,128}.status-blue{--tblr-status-color:#066fd1;--tblr-status-color-rgb:6,111,209}.status-azure{--tblr-status-color:#4299e1;--tblr-status-color-rgb:66,153,225}.status-indigo{--tblr-status-color:#4263eb;--tblr-status-color-rgb:66,99,235}.status-purple{--tblr-status-color:#ae3ec9;--tblr-status-color-rgb:174,62,201}.status-pink{--tblr-status-color:#d6336c;--tblr-status-color-rgb:214,51,108}.status-red{--tblr-status-color:#d63939;--tblr-status-color-rgb:214,57,57}.status-orange{--tblr-status-color:#f76707;--tblr-status-color-rgb:247,103,7}.status-yellow{--tblr-status-color:#f59f00;--tblr-status-color-rgb:245,159,0}.status-lime{--tblr-status-color:#74b816;--tblr-status-color-rgb:116,184,22}.status-green{--tblr-status-color:#2fb344;--tblr-status-color-rgb:47,179,68}.status-teal{--tblr-status-color:#0ca678;--tblr-status-color-rgb:12,166,120}.status-cyan{--tblr-status-color:#17a2b8;--tblr-status-color-rgb:23,162,184}.status-dot{--tblr-status-dot-color:var(--tblr-status-color, #6b7280);--tblr-status-size:0.5rem;position:relative;display:inline-block;width:var(--tblr-status-size);height:var(--tblr-status-size);background:var(--tblr-status-dot-color);border-radius:100rem}.status-dot-animated:before{content:"";position:absolute;inset:0;z-index:0;background:inherit;border-radius:inherit;opacity:.6;animation:1s linear 2s backwards infinite status-pulsate-tertiary}.status-indicator{--tblr-status-indicator-size:2.5rem;--tblr-status-indicator-color:var(--tblr-status-color, #6b7280);display:block;position:relative;width:var(--tblr-status-indicator-size);height:var(--tblr-status-indicator-size)}.status-indicator-circle{--tblr-status-circle-size:.75rem;position:absolute;left:50%;top:50%;margin:calc(var(--tblr-status-circle-size)/ -2) 0 0 calc(var(--tblr-status-circle-size)/ -2);width:var(--tblr-status-circle-size);height:var(--tblr-status-circle-size);border-radius:100rem;background:var(--tblr-status-color)}.status-indicator-circle:first-child{z-index:3}.status-indicator-circle:nth-child(2){z-index:2;opacity:.1}.status-indicator-circle:nth-child(3){z-index:1;opacity:.3}.status-indicator-animated .status-indicator-circle:first-child{animation:2s linear 1s infinite backwards status-pulsate-main}.status-indicator-animated .status-indicator-circle:nth-child(2){animation:2s linear 1s infinite backwards status-pulsate-secondary}.status-indicator-animated .status-indicator-circle:nth-child(3){animation:2s linear 1s infinite backwards status-pulsate-tertiary}.switch-icon{display:inline-block;line-height:1;border:0;padding:0;background:0 0;width:1.25rem;height:1.25rem;vertical-align:bottom;position:relative;cursor:pointer}.switch-icon.disabled{pointer-events:none;opacity:.4}.switch-icon:focus{outline:0}.switch-icon svg{display:block;width:100%;height:100%}.switch-icon .switch-icon-a,.switch-icon .switch-icon-b{display:block;width:100%;height:100%}.switch-icon .switch-icon-a{opacity:1}.switch-icon .switch-icon-b{position:absolute;top:0;left:0;opacity:0}.switch-icon.active .switch-icon-a{opacity:0}.switch-icon.active .switch-icon-b{opacity:1}.switch-icon-fade .switch-icon-a,.switch-icon-fade .switch-icon-b{transition:opacity .5s}@media (prefers-reduced-motion:reduce){.switch-icon-fade .switch-icon-a,.switch-icon-fade .switch-icon-b{transition:none}}.switch-icon-scale .switch-icon-a,.switch-icon-scale .switch-icon-b{transition:opacity .5s,transform 0s .5s}@media (prefers-reduced-motion:reduce){.switch-icon-scale .switch-icon-a,.switch-icon-scale .switch-icon-b{transition:none}}.switch-icon-scale .switch-icon-b{transform:scale(1.5)}.switch-icon-scale.active .switch-icon-a,.switch-icon-scale.active .switch-icon-b{transition:opacity 0s,transform .5s}@media (prefers-reduced-motion:reduce){.switch-icon-scale.active .switch-icon-a,.switch-icon-scale.active .switch-icon-b{transition:none}}.switch-icon-scale.active .switch-icon-b{transform:scale(1)}.switch-icon-flip{perspective:10em}.switch-icon-flip .switch-icon-a,.switch-icon-flip .switch-icon-b{backface-visibility:hidden;transform-style:preserve-3d;transition:opacity 0s .2s,transform .4s ease-in-out}@media (prefers-reduced-motion:reduce){.switch-icon-flip .switch-icon-a,.switch-icon-flip .switch-icon-b{transition:none}}.switch-icon-flip .switch-icon-a{opacity:1;transform:rotateY(0)}.switch-icon-flip .switch-icon-b{opacity:1;transform:rotateY(-180deg)}.switch-icon-flip.active .switch-icon-a{opacity:1;transform:rotateY(180deg)}.switch-icon-flip.active .switch-icon-b{opacity:1;transform:rotateY(0)}.switch-icon-slide-down,.switch-icon-slide-end,.switch-icon-slide-left,.switch-icon-slide-right,.switch-icon-slide-start,.switch-icon-slide-up{overflow:hidden}.switch-icon-slide-down .switch-icon-a,.switch-icon-slide-down .switch-icon-b,.switch-icon-slide-end .switch-icon-a,.switch-icon-slide-end .switch-icon-b,.switch-icon-slide-left .switch-icon-a,.switch-icon-slide-left .switch-icon-b,.switch-icon-slide-right .switch-icon-a,.switch-icon-slide-right .switch-icon-b,.switch-icon-slide-start .switch-icon-a,.switch-icon-slide-start .switch-icon-b,.switch-icon-slide-up .switch-icon-a,.switch-icon-slide-up .switch-icon-b{transition:opacity .3s,transform .3s}@media (prefers-reduced-motion:reduce){.switch-icon-slide-down .switch-icon-a,.switch-icon-slide-down .switch-icon-b,.switch-icon-slide-end .switch-icon-a,.switch-icon-slide-end .switch-icon-b,.switch-icon-slide-left .switch-icon-a,.switch-icon-slide-left .switch-icon-b,.switch-icon-slide-right .switch-icon-a,.switch-icon-slide-right .switch-icon-b,.switch-icon-slide-start .switch-icon-a,.switch-icon-slide-start .switch-icon-b,.switch-icon-slide-up .switch-icon-a,.switch-icon-slide-up .switch-icon-b{transition:none}}.switch-icon-slide-down .switch-icon-a,.switch-icon-slide-end .switch-icon-a,.switch-icon-slide-left .switch-icon-a,.switch-icon-slide-right .switch-icon-a,.switch-icon-slide-start .switch-icon-a,.switch-icon-slide-up .switch-icon-a{transform:translateY(0)}.switch-icon-slide-down .switch-icon-b,.switch-icon-slide-end .switch-icon-b,.switch-icon-slide-left .switch-icon-b,.switch-icon-slide-right .switch-icon-b,.switch-icon-slide-start .switch-icon-b,.switch-icon-slide-up .switch-icon-b{transform:translateY(100%)}.switch-icon-slide-down.active .switch-icon-a,.switch-icon-slide-end.active .switch-icon-a,.switch-icon-slide-left.active .switch-icon-a,.switch-icon-slide-right.active .switch-icon-a,.switch-icon-slide-start.active .switch-icon-a,.switch-icon-slide-up.active .switch-icon-a{transform:translateY(-100%)}.switch-icon-slide-down.active .switch-icon-b,.switch-icon-slide-end.active .switch-icon-b,.switch-icon-slide-left.active .switch-icon-b,.switch-icon-slide-right.active .switch-icon-b,.switch-icon-slide-start.active .switch-icon-b,.switch-icon-slide-up.active .switch-icon-b{transform:translateY(0)}.switch-icon-slide-left .switch-icon-a,.switch-icon-slide-start .switch-icon-a{transform:translateX(0)}.switch-icon-slide-left .switch-icon-b,.switch-icon-slide-start .switch-icon-b{transform:translateX(100%)}.switch-icon-slide-left.active .switch-icon-a,.switch-icon-slide-start.active .switch-icon-a{transform:translateX(-100%)}.switch-icon-slide-left.active .switch-icon-b,.switch-icon-slide-start.active .switch-icon-b{transform:translateX(0)}.switch-icon-slide-end .switch-icon-a,.switch-icon-slide-right .switch-icon-a{transform:translateX(0)}.switch-icon-slide-end .switch-icon-b,.switch-icon-slide-right .switch-icon-b{transform:translateX(-100%)}.switch-icon-slide-end.active .switch-icon-a,.switch-icon-slide-right.active .switch-icon-a{transform:translateX(100%)}.switch-icon-slide-end.active .switch-icon-b,.switch-icon-slide-right.active .switch-icon-b{transform:translateX(0)}.switch-icon-slide-down .switch-icon-a{transform:translateY(0)}.switch-icon-slide-down .switch-icon-b{transform:translateY(-100%)}.switch-icon-slide-down.active .switch-icon-a{transform:translateY(100%)}.switch-icon-slide-down.active .switch-icon-b{transform:translateY(0)}.markdown>table thead th,.table thead th{background:var(--tblr-bg-surface-tertiary);font-size:.75rem;font-weight:var(--tblr-font-weight-medium);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-secondary);padding-top:.5rem;padding-bottom:.5rem;white-space:nowrap}@media print{.markdown>table thead th,.table thead th{background:0 0}}.table-responsive .markdown>table,.table-responsive .table{margin-bottom:0}.table-responsive+.card-footer{border-top:0}.table-transparent thead th{background:0 0}.table-nowrap>:not(caption)>*>*{white-space:nowrap}.table-vcenter>:not(caption)>*>*{vertical-align:middle}.table-center>:not(caption)>*>*{text-align:center}.td-truncate{max-width:1px;width:100%}.table-mobile{display:block}.table-mobile thead{display:none}.table-mobile tbody,.table-mobile tr{display:flex;flex-direction:column}.table-mobile td{display:block;padding:.75rem .75rem!important;border:none;color:var(--tblr-body-color)!important}.table-mobile td[data-label]:before{font-size:.75rem;font-weight:var(--tblr-font-weight-medium);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-secondary);content:attr(data-label);display:block}.table-mobile tr{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)}.table-mobile .btn{display:block}@media (max-width:575.98px){.table-mobile-sm{display:block}.table-mobile-sm thead{display:none}.table-mobile-sm tbody,.table-mobile-sm tr{display:flex;flex-direction:column}.table-mobile-sm td{display:block;padding:.75rem .75rem!important;border:none;color:var(--tblr-body-color)!important}.table-mobile-sm td[data-label]:before{font-size:.75rem;font-weight:var(--tblr-font-weight-medium);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-secondary);content:attr(data-label);display:block}.table-mobile-sm tr{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)}.table-mobile-sm .btn{display:block}}@media (max-width:767.98px){.table-mobile-md{display:block}.table-mobile-md thead{display:none}.table-mobile-md tbody,.table-mobile-md tr{display:flex;flex-direction:column}.table-mobile-md td{display:block;padding:.75rem .75rem!important;border:none;color:var(--tblr-body-color)!important}.table-mobile-md td[data-label]:before{font-size:.75rem;font-weight:var(--tblr-font-weight-medium);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-secondary);content:attr(data-label);display:block}.table-mobile-md tr{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)}.table-mobile-md .btn{display:block}}@media (max-width:991.98px){.table-mobile-lg{display:block}.table-mobile-lg thead{display:none}.table-mobile-lg tbody,.table-mobile-lg tr{display:flex;flex-direction:column}.table-mobile-lg td{display:block;padding:.75rem .75rem!important;border:none;color:var(--tblr-body-color)!important}.table-mobile-lg td[data-label]:before{font-size:.75rem;font-weight:var(--tblr-font-weight-medium);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-secondary);content:attr(data-label);display:block}.table-mobile-lg tr{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)}.table-mobile-lg .btn{display:block}}@media (max-width:1199.98px){.table-mobile-xl{display:block}.table-mobile-xl thead{display:none}.table-mobile-xl tbody,.table-mobile-xl tr{display:flex;flex-direction:column}.table-mobile-xl td{display:block;padding:.75rem .75rem!important;border:none;color:var(--tblr-body-color)!important}.table-mobile-xl td[data-label]:before{font-size:.75rem;font-weight:var(--tblr-font-weight-medium);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-secondary);content:attr(data-label);display:block}.table-mobile-xl tr{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)}.table-mobile-xl .btn{display:block}}@media (max-width:1399.98px){.table-mobile-xxl{display:block}.table-mobile-xxl thead{display:none}.table-mobile-xxl tbody,.table-mobile-xxl tr{display:flex;flex-direction:column}.table-mobile-xxl td{display:block;padding:.75rem .75rem!important;border:none;color:var(--tblr-body-color)!important}.table-mobile-xxl td[data-label]:before{font-size:.75rem;font-weight:var(--tblr-font-weight-medium);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-secondary);content:attr(data-label);display:block}.table-mobile-xxl tr{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent)}.table-mobile-xxl .btn{display:block}}.table-sort{font:inherit;color:inherit;text-transform:inherit;letter-spacing:inherit;border:0;background:inherit;display:block;width:100%;text-align:inherit;transition:color .3s;margin:-.5rem -.75rem;padding:.5rem .75rem}@media (prefers-reduced-motion:reduce){.table-sort{transition:none}}.table-sort.asc,.table-sort.desc,.table-sort:hover{color:var(--tblr-body-color)}.table-sort:after{content:"";display:inline-flex;width:1rem;height:1rem;vertical-align:bottom;-webkit-mask-image:url("data:image/svg+xml,");mask-image:url("data:image/svg+xml,");background:currentColor;margin-left:.25rem}.table-sort.asc:after{-webkit-mask-image:url("data:image/svg+xml,");mask-image:url("data:image/svg+xml,")}.table-sort.desc:after{-webkit-mask-image:url("data:image/svg+xml,");mask-image:url("data:image/svg+xml,")}.table-borderless thead th{background:0 0}.table-selectable tbody tr .on-checked{display:none}.table-selectable tbody tr .on-unchecked{display:initial}.table-selectable tbody tr:has(.table-selectable-check:checked){background-color:var(--tblr-active-bg)}.table-selectable tbody tr:has(.table-selectable-check:checked) .on-checked{display:initial}.table-selectable tbody tr:has(.table-selectable-check:checked) .on-unchecked{display:none}.tag{--tblr-tag-height:1.5rem;border:1px solid var(--tblr-border-color);display:inline-flex;align-items:center;height:var(--tblr-tag-height);border-radius:var(--tblr-border-radius);padding:0 .5rem;background:var(--tblr-bg-surface);box-shadow:var(--tblr-shadow-input);gap:.5rem}.tag .btn-close{margin-right:-.25rem;margin-left:-.125rem;padding:0;width:1rem;height:1rem;font-size:.5rem}.tag-badge{--tblr-badge-font-size:0.625rem;--tblr-badge-padding-x:.25rem;--tblr-badge-padding-y:.125rem;margin-right:-.25rem}.tag-avatar,.tag-check,.tag-flag,.tag-icon,.tag-payment{margin-left:-.25rem}.tag-icon{color:var(--tblr-secondary);margin-right:-.125rem;width:1rem;height:1rem}.tag-check{width:1rem;height:1rem;background-size:1rem}.tags-list{--tblr-list-gap:0.5rem;display:flex;flex-wrap:wrap;gap:var(--tblr-list-gap)}.toast{border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent);box-shadow:rgba(31,41,55,.04) 0 2px 4px 0}.toast .toast-header{-webkit-user-select:none;-moz-user-select:none;user-select:none}.toast button[data-bs-dismiss=toast]{outline:0}.toast-primary{--tblr-toast-color:#066fd1}.toast-secondary{--tblr-toast-color:#6b7280}.toast-success{--tblr-toast-color:#2fb344}.toast-info{--tblr-toast-color:#4299e1}.toast-warning{--tblr-toast-color:#f59f00}.toast-danger{--tblr-toast-color:#d63939}.toast-light{--tblr-toast-color:#f9fafb}.toast-dark{--tblr-toast-color:#1f2937}.toast-muted{--tblr-toast-color:#6b7280}.toast-blue{--tblr-toast-color:#066fd1}.toast-azure{--tblr-toast-color:#4299e1}.toast-indigo{--tblr-toast-color:#4263eb}.toast-purple{--tblr-toast-color:#ae3ec9}.toast-pink{--tblr-toast-color:#d6336c}.toast-red{--tblr-toast-color:#d63939}.toast-orange{--tblr-toast-color:#f76707}.toast-yellow{--tblr-toast-color:#f59f00}.toast-lime{--tblr-toast-color:#74b816}.toast-green{--tblr-toast-color:#2fb344}.toast-teal{--tblr-toast-color:#0ca678}.toast-cyan{--tblr-toast-color:#17a2b8}.toolbar{display:flex;flex-wrap:nowrap;flex-shrink:0;margin:0 -.5rem}.toolbar>*{margin:0 .5rem}.tracking{--tblr-tracking-height:1.5rem;--tblr-tracking-gap-width:0.125rem;--tblr-tracking-block-border-radius:var(--tblr-border-radius);display:flex;gap:var(--tblr-tracking-gap-width)}.tracking-squares{--tblr-tracking-block-border-radius:var(--tblr-border-radius-sm)}.tracking-squares .tracking-block{height:auto}.tracking-squares .tracking-block:before{content:"";display:block;padding-top:100%}.tracking-block{flex:1;border-radius:var(--tblr-tracking-block-border-radius);height:var(--tblr-tracking-height);min-width:.25rem;background:var(--tblr-border-color)}.timeline{--tblr-timeline-icon-size:2.5rem;position:relative;list-style:none;padding:0}.timeline-event{position:relative}.timeline-event:not(:last-child){margin-bottom:var(--tblr-page-padding)}.timeline-event:not(:last-child):before{content:"";position:absolute;top:var(--tblr-timeline-icon-size);left:calc(var(--tblr-timeline-icon-size)/ 2);bottom:calc(-1 * var(--tblr-page-padding));width:var(--tblr-border-width);background-color:var(--tblr-border-color);border-radius:var(--tblr-border-radius)}.timeline-event-icon{position:absolute;display:flex;align-items:center;justify-content:center;width:var(--tblr-timeline-icon-size,2.5rem);height:var(--tblr-timeline-icon-size,2.5rem);background:var(--tblr-bg-surface-secondary);color:var(--tblr-secondary);border-radius:var(--tblr-border-radius);z-index:5}.timeline-event-card{margin-left:calc(var(--tblr-timeline-icon-size,2.5rem) + var(--tblr-page-padding))}.timeline-simple .timeline-event-icon{display:none}.timeline-simple .timeline-event-card{margin-left:0}.hr-text{display:flex;align-items:center;margin:2rem 0;font-size:.75rem;font-weight:var(--tblr-font-weight-medium);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-secondary);height:1px}.hr-text:after,.hr-text:before{flex:1 1 auto;height:1px;background-color:var(--tblr-border-color)}.hr-text:before{content:"";margin-right:.5rem}.hr-text:after{content:"";margin-left:.5rem}.hr-text>:first-child{padding-right:.5rem;padding-left:0;color:var(--tblr-secondary)}.hr-text.hr-text-left:before,.hr-text.hr-text-start:before{content:none}.hr-text.hr-text-left>:first-child,.hr-text.hr-text-start>:first-child{padding-right:.5rem;padding-left:.5rem}.hr-text.hr-text-end:before,.hr-text.hr-text-right:before{content:""}.hr-text.hr-text-end:after,.hr-text.hr-text-right:after{content:none}.hr-text.hr-text-end>:first-child,.hr-text.hr-text-right>:first-child{padding-right:0;padding-left:.5rem}.card>.hr-text{margin:0}.hr-text-spaceless{margin:-.5rem 0}.lead{color:var(--tblr-secondary);font-size:inherit}a{-webkit-text-decoration-skip:ink;text-decoration-skip-ink:auto;color:color-mix(in srgb,transparent,var(--tblr-link-color) var(--tblr-link-opacity,100%))}a:hover{color:color-mix(in srgb,transparent,var(--tblr-link-hover-color) var(--tblr-link-opacity,100%))}.h1 a,.h2 a,.h3 a,.h4 a,.h5 a,.h6 a,h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{color:inherit}.h1 a:hover,.h2 a:hover,.h3 a:hover,.h4 a:hover,.h5 a:hover,.h6 a:hover,h1 a:hover,h2 a:hover,h3 a:hover,h4 a:hover,h5 a:hover,h6 a:hover{color:inherit}.h1,h1{font-size:var(--tblr-font-size-h1);line-height:var(--tblr-line-height-h1)}.h2,h2{font-size:var(--tblr-font-size-h2);line-height:var(--tblr-line-height-h2)}.h3,h3{font-size:var(--tblr-font-size-h3);line-height:var(--tblr-line-height-h3)}.h4,h4{font-size:var(--tblr-font-size-h4);line-height:var(--tblr-line-height-h4)}.h5,h5{font-size:var(--tblr-font-size-h5);line-height:var(--tblr-line-height-h5)}.h6,h6{font-size:var(--tblr-font-size-h6);line-height:var(--tblr-line-height-h6)}.fs-base{font-size:var(--tblr-body-font-size)}.strong,b,strong{font-weight:var(--tblr-font-weight-bold)}blockquote{padding:1rem 1rem 1rem;border-left:2px var(--tblr-border-style) var(--tblr-border-color)}blockquote p{margin-bottom:1rem}blockquote cite{display:block;text-align:right}blockquote cite:before{content:"— "}ol,ul{padding-left:1.5rem}.hr,hr{margin:2rem 0}dl dd:last-child{margin-bottom:0}pre{--tblr-scrollbar-color:var(--tblr-light);padding:1rem;background:var(--tblr-bg-surface-dark);color:var(--tblr-light);border-radius:var(--tblr-border-radius);line-height:1.4285714286}pre{scrollbar-color:color-mix(in srgb,var(--tblr-scrollbar-color,var(--tblr-body-color)) 20%,transparent) transparent}pre::-webkit-scrollbar{width:1rem;height:1rem;-webkit-transition:background .3s;transition:background .3s}@media (prefers-reduced-motion:reduce){pre::-webkit-scrollbar{-webkit-transition:none;transition:none}}pre::-webkit-scrollbar-thumb{border-radius:1rem;border:5px solid transparent;box-shadow:inset 0 0 0 1rem color-mix(in srgb,var(--tblr-scrollbar-color,var(--tblr-body-color)) 20%,transparent)}pre::-webkit-scrollbar-track{background:0 0}pre:hover::-webkit-scrollbar-thumb{box-shadow:inset 0 0 0 1rem color-mix(in srgb,var(--tblr-scrollbar-color,var(--tblr-body-color)) 40%,transparent)}pre::-webkit-scrollbar-corner{background:0 0}pre code{background:0 0;padding:0}code{background:var(--tblr-code-bg);padding:2px 4px;border-radius:var(--tblr-border-radius)}abbr{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}.kbd,kbd{border:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color);display:inline-block;box-sizing:border-box;max-width:100%;font-size:var(--tblr-font-size-h5);font-weight:var(--tblr-font-weight-medium);line-height:1;vertical-align:baseline;border-radius:var(--tblr-border-radius)}img{max-width:100%;height:auto}.list-unstyled{margin-left:0}::-moz-selection{background-color:color-mix(in srgb,var(--tblr-primary) 10%,transparent)}.text-selected,::selection{background-color:color-mix(in srgb,var(--tblr-primary) 10%,transparent)}.text-selected{display:inline-block}[class*=" link-"].disabled,[class^=link-].disabled{color:var(--tblr-disabled-color)!important;pointer-events:none}a:hover:has(.icon){text-decoration:none}.link-hoverable{border-radius:var(--tblr-border-radius);transition:background-color .15s ease-in-out}.link-hoverable:hover{text-decoration:none;color:var(--tblr-primary);background:color-mix(in srgb,var(--tblr-secondary) 4%,transparent)}.subheader{font-size:.75rem;font-weight:var(--tblr-font-weight-medium);text-transform:uppercase;letter-spacing:.04em;line-height:1rem;color:var(--tblr-secondary)}.mention{display:inline-block;box-shadow:var(--tblr-shadow-border);border-radius:var(--tblr-border-radius-pill);line-height:1.3333333333em;font-size:.8571428571em;color:var(--tblr-body-color);background:var(--tblr-bg-surface-tertiary);padding:.1666666667em .6666666667em;font-weight:var(--tblr-font-weight-medium)}a.mention{cursor:pointer}a.mention.hover,a.mention:hover{background:var(--tblr-bg-surface-secondary);text-decoration:underline}.mention-app,.mention-avatar,.mention-color{width:1.1666666667em;height:1.1666666667em;border-radius:var(--tblr-border-radius-pill);margin:-.1666666667em .3333333333em 0 -.3333333333em;display:inline-flex;background:no-repeat center center/cover;box-shadow:var(--tblr-shadow-border);vertical-align:middle;text-align:center}.mention-app{box-shadow:none;background:0 0;border-radius:0}.mention-count{color:var(--tblr-secondary);margin-left:.6666666667em}.text-incorrect{background:color-mix(in srgb,var(--tblr-red) 4%,transparent);background:color-mix(in srgb,var(--tblr-red) 4%,transparent);text-decoration:underline;text-decoration-thickness:1px;text-decoration-color:var(--tblr-red)}.text-correct{background:color-mix(in srgb,var(--tblr-green) 4%,transparent);background:color-mix(in srgb,var(--tblr-green) 4%,transparent);text-decoration:underline;text-decoration-thickness:1px;text-decoration-color:var(--tblr-green)}.steps{--tblr-steps-padding:2rem;--tblr-steps-item-size:1.5rem;margin-left:1rem;padding-left:var(--tblr-steps-padding);counter-reset:step;border-left:1px solid var(--tblr-border-color);margin-bottom:2rem}.steps .h3,.steps h3{counter-increment:step}.steps .h3:not(:first-child),.steps h3:not(:first-child){margin-top:2.5rem!important}.steps .h3:before,.steps h3:before{content:counter(step);display:inline-block;position:absolute;margin-top:1px;margin-left:calc(-1 * var(--tblr-steps-padding) - var(--tblr-steps-item-size)/ 2);width:var(--tblr-steps-item-size);height:var(--tblr-steps-item-size);text-align:center;color:var(--tblr-body-color);border:1px solid var(--tblr-border-color);background:var(--tblr-bg-surface);border-radius:var(--tblr-border-radius);line-height:calc(var(--tblr-steps-item-size) - 2px);font-size:var(--tblr-font-size-h4);font-weight:var(--tblr-font-weight-bold)}.steps>:last-child{margin-bottom:0}.callout{margin-bottom:1.5rem;border:1px solid var(--tblr-primary-200);border-radius:var(--tblr-border-radius);padding:.5rem 1rem;background:var(--tblr-primary-lt)}.callout>:last-child{margin-bottom:0}.chart{display:block;min-height:10rem}.chart text{font-family:inherit}.chart-sm{height:2.5rem}.chart-lg{height:15rem}.chart-square{height:5.75rem}.chart-sparkline{position:relative;width:4rem;height:2.5rem;line-height:1;min-height:0!important}.chart-sparkline-sm{height:1.5rem}.chart-sparkline-square{width:2.5rem}.chart-sparkline-wide{width:6rem}.chart-sparkline-label{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;font-size:.625rem}.chart-sparkline-label .icon{width:1rem;height:1rem;font-size:1rem}.offcanvas-header{border-bottom:var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color)}.offcanvas-footer{padding:1.5rem 1.5rem}.offcanvas-title{font-size:1rem;font-weight:var(--tblr-font-weight-medium);line-height:1.5rem}.offcanvas-narrow{width:20rem}.chat-bubbles{display:flex;flex-direction:column;gap:1rem}.chat-bubble{background:var(--tblr-bg-surface-secondary);border-radius:var(--tblr-border-radius-lg);padding:1rem;position:relative}.chat-bubble-me{background-color:var(--tblr-primary-lt);box-shadow:none}.chat-bubble-title{margin-bottom:.25rem}.chat-bubble-author{font-weight:600}.chat-bubble-date{color:var(--tblr-secondary)}.chat-bubble-body>:last-child{margin-bottom:0}.signature{--tblr-signature-padding:var(--tblr-spacer-1);--tblr-signature-border-radius:var(--tblr-border-radius);border:var(--tblr-border-width) solid var(--tblr-border-color);padding:var(--tblr-signature-padding);border-radius:var(--tblr-border-radius)}.signature-canvas{border:var(--tblr-border-width) dashed var(--tblr-border-color);border-radius:calc(var(--tblr-signature-border-radius) - var(--tblr-signature-padding));display:block;cursor:crosshair;width:100%}.clearfix::after{display:block;clear:both;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--tblr-border-width);min-height:1em;background-color:currentcolor;opacity:.16}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.visually-hidden *,.visually-hidden-focusable:not(:focus):not(:focus-within) *{overflow:hidden!important}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:sticky;top:0;z-index:1020}.sticky-bottom{position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:sticky;bottom:0;z-index:1020}}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--tblr-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--tblr-aspect-ratio:100%}.ratio-2x1{--tblr-aspect-ratio:50%}.ratio-1x2{--tblr-aspect-ratio:200%}.ratio-3x1{--tblr-aspect-ratio:33.3333333333%}.ratio-1x3{--tblr-aspect-ratio:300%}.ratio-4x1{--tblr-aspect-ratio:25%}.ratio-1x4{--tblr-aspect-ratio:400%}.ratio-4x3{--tblr-aspect-ratio:75%}.ratio-3x4{--tblr-aspect-ratio:133.3333333333%}.ratio-16x9{--tblr-aspect-ratio:56.25%}.ratio-9x16{--tblr-aspect-ratio:177.7777777778%}.ratio-21x9{--tblr-aspect-ratio:42.8571428571%}.ratio-9x21{--tblr-aspect-ratio:233.3333333333%}.focus-ring:focus{outline:0;box-shadow:var(--tblr-focus-ring-x,0) var(--tblr-focus-ring-y,0) var(--tblr-focus-ring-blur,0) var(--tblr-focus-ring-width) var(--tblr-focus-ring-color)}.bg-white-overlay{color:#fff;background-color:rgba(249,250,251,.24)}.bg-dark-overlay{color:#fff;background-color:rgba(31,41,55,.24)}.bg-cover{background-repeat:no-repeat;background-size:cover;background-position:center}.bg-primary{background-color:color-mix(in srgb,var(--tblr-primary) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-primary-lt{color:color-mix(in srgb,var(--tblr-primary) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-primary-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-primary{border-color:color-mix(in srgb,var(--tblr-primary) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-primary{--tblr-gradient-from:var(--tblr-primary)}.bg-gradient-to-primary{--tblr-gradient-to:var(--tblr-primary)}.bg-gradient-via-primary{--tblr-gradient-via:var(--tblr-primary);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-primary{color:#f9fafb!important;background-color:RGBA(var(--tblr-primary-rgb),var(--tblr-bg-opacity,1))!important}.link-primary{color:color-mix(in srgb,var(--tblr-primary) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-primary) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-primary:focus,.link-primary:hover{color:RGBA(5,89,167,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(5,89,167,var(--tblr-link-underline-opacity,1))!important}.bg-secondary{background-color:color-mix(in srgb,var(--tblr-secondary) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-secondary-lt{color:color-mix(in srgb,var(--tblr-secondary) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-secondary-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-secondary{border-color:color-mix(in srgb,var(--tblr-secondary) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-secondary{--tblr-gradient-from:var(--tblr-secondary)}.bg-gradient-to-secondary{--tblr-gradient-to:var(--tblr-secondary)}.bg-gradient-via-secondary{--tblr-gradient-via:var(--tblr-secondary);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-secondary{color:#f9fafb!important;background-color:RGBA(var(--tblr-secondary-rgb),var(--tblr-bg-opacity,1))!important}.link-secondary{color:color-mix(in srgb,var(--tblr-secondary) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-secondary) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(86,91,102,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(86,91,102,var(--tblr-link-underline-opacity,1))!important}.bg-success{background-color:color-mix(in srgb,var(--tblr-success) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-success-lt{color:color-mix(in srgb,var(--tblr-success) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-success-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-success{border-color:color-mix(in srgb,var(--tblr-success) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-success{--tblr-gradient-from:var(--tblr-success)}.bg-gradient-to-success{--tblr-gradient-to:var(--tblr-success)}.bg-gradient-via-success{--tblr-gradient-via:var(--tblr-success);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-success{color:#f9fafb!important;background-color:RGBA(var(--tblr-success-rgb),var(--tblr-bg-opacity,1))!important}.link-success{color:color-mix(in srgb,var(--tblr-success) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-success) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-success:focus,.link-success:hover{color:RGBA(38,143,54,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(38,143,54,var(--tblr-link-underline-opacity,1))!important}.bg-info{background-color:color-mix(in srgb,var(--tblr-info) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-info-lt{color:color-mix(in srgb,var(--tblr-info) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-info-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-info{border-color:color-mix(in srgb,var(--tblr-info) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-info{--tblr-gradient-from:var(--tblr-info)}.bg-gradient-to-info{--tblr-gradient-to:var(--tblr-info)}.bg-gradient-via-info{--tblr-gradient-via:var(--tblr-info);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-info{color:#f9fafb!important;background-color:RGBA(var(--tblr-info-rgb),var(--tblr-bg-opacity,1))!important}.link-info{color:color-mix(in srgb,var(--tblr-info) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-info) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-info:focus,.link-info:hover{color:RGBA(53,122,180,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(53,122,180,var(--tblr-link-underline-opacity,1))!important}.bg-warning{background-color:color-mix(in srgb,var(--tblr-warning) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-warning-lt{color:color-mix(in srgb,var(--tblr-warning) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-warning-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-warning{border-color:color-mix(in srgb,var(--tblr-warning) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-warning{--tblr-gradient-from:var(--tblr-warning)}.bg-gradient-to-warning{--tblr-gradient-to:var(--tblr-warning)}.bg-gradient-via-warning{--tblr-gradient-via:var(--tblr-warning);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-warning{color:#f9fafb!important;background-color:RGBA(var(--tblr-warning-rgb),var(--tblr-bg-opacity,1))!important}.link-warning{color:color-mix(in srgb,var(--tblr-warning) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-warning) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-warning:focus,.link-warning:hover{color:RGBA(196,127,0,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(196,127,0,var(--tblr-link-underline-opacity,1))!important}.bg-danger{background-color:color-mix(in srgb,var(--tblr-danger) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-danger-lt{color:color-mix(in srgb,var(--tblr-danger) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-danger-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-danger{border-color:color-mix(in srgb,var(--tblr-danger) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-danger{--tblr-gradient-from:var(--tblr-danger)}.bg-gradient-to-danger{--tblr-gradient-to:var(--tblr-danger)}.bg-gradient-via-danger{--tblr-gradient-via:var(--tblr-danger);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-danger{color:#f9fafb!important;background-color:RGBA(var(--tblr-danger-rgb),var(--tblr-bg-opacity,1))!important}.link-danger{color:color-mix(in srgb,var(--tblr-danger) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-danger) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-danger:focus,.link-danger:hover{color:RGBA(171,46,46,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(171,46,46,var(--tblr-link-underline-opacity,1))!important}.bg-light{background-color:color-mix(in srgb,var(--tblr-light) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-light-lt{color:color-mix(in srgb,var(--tblr-light) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-light-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-light{border-color:color-mix(in srgb,var(--tblr-light) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-light{--tblr-gradient-from:var(--tblr-light)}.bg-gradient-to-light{--tblr-gradient-to:var(--tblr-light)}.bg-gradient-via-light{--tblr-gradient-via:var(--tblr-light);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-light{color:#1f2937!important;background-color:RGBA(var(--tblr-light-rgb),var(--tblr-bg-opacity,1))!important}.link-light{color:color-mix(in srgb,var(--tblr-light) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-light) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-light:focus,.link-light:hover{color:RGBA(250,251,252,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(250,251,252,var(--tblr-link-underline-opacity,1))!important}.bg-dark{background-color:color-mix(in srgb,var(--tblr-dark) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-dark-lt{color:color-mix(in srgb,var(--tblr-dark) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-dark-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-dark{border-color:color-mix(in srgb,var(--tblr-dark) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-dark{--tblr-gradient-from:var(--tblr-dark)}.bg-gradient-to-dark{--tblr-gradient-to:var(--tblr-dark)}.bg-gradient-via-dark{--tblr-gradient-via:var(--tblr-dark);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-dark{color:#f9fafb!important;background-color:RGBA(var(--tblr-dark-rgb),var(--tblr-bg-opacity,1))!important}.link-dark{color:color-mix(in srgb,var(--tblr-dark) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-dark) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-dark:focus,.link-dark:hover{color:RGBA(25,33,44,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(25,33,44,var(--tblr-link-underline-opacity,1))!important}.bg-muted{background-color:color-mix(in srgb,var(--tblr-muted) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-muted-lt{color:color-mix(in srgb,var(--tblr-muted) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-muted-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-muted{border-color:color-mix(in srgb,var(--tblr-muted) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-muted{--tblr-gradient-from:var(--tblr-muted)}.bg-gradient-to-muted{--tblr-gradient-to:var(--tblr-muted)}.bg-gradient-via-muted{--tblr-gradient-via:var(--tblr-muted);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-muted{color:#f9fafb!important;background-color:RGBA(var(--tblr-muted-rgb),var(--tblr-bg-opacity,1))!important}.link-muted{color:color-mix(in srgb,var(--tblr-muted) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-muted) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-muted:focus,.link-muted:hover{color:RGBA(86,91,102,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(86,91,102,var(--tblr-link-underline-opacity,1))!important}.bg-blue{background-color:color-mix(in srgb,var(--tblr-blue) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-blue-lt{color:color-mix(in srgb,var(--tblr-blue) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-blue-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-blue{border-color:color-mix(in srgb,var(--tblr-blue) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-blue{--tblr-gradient-from:var(--tblr-blue)}.bg-gradient-to-blue{--tblr-gradient-to:var(--tblr-blue)}.bg-gradient-via-blue{--tblr-gradient-via:var(--tblr-blue);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-blue{color:#f9fafb!important;background-color:RGBA(var(--tblr-blue-rgb),var(--tblr-bg-opacity,1))!important}.link-blue{color:color-mix(in srgb,var(--tblr-blue) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-blue) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-blue:focus,.link-blue:hover{color:RGBA(5,89,167,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(5,89,167,var(--tblr-link-underline-opacity,1))!important}.bg-azure{background-color:color-mix(in srgb,var(--tblr-azure) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-azure-lt{color:color-mix(in srgb,var(--tblr-azure) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-azure-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-azure{border-color:color-mix(in srgb,var(--tblr-azure) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-azure{--tblr-gradient-from:var(--tblr-azure)}.bg-gradient-to-azure{--tblr-gradient-to:var(--tblr-azure)}.bg-gradient-via-azure{--tblr-gradient-via:var(--tblr-azure);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-azure{color:#f9fafb!important;background-color:RGBA(var(--tblr-azure-rgb),var(--tblr-bg-opacity,1))!important}.link-azure{color:color-mix(in srgb,var(--tblr-azure) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-azure) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-azure:focus,.link-azure:hover{color:RGBA(53,122,180,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(53,122,180,var(--tblr-link-underline-opacity,1))!important}.bg-indigo{background-color:color-mix(in srgb,var(--tblr-indigo) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-indigo-lt{color:color-mix(in srgb,var(--tblr-indigo) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-indigo-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-indigo{border-color:color-mix(in srgb,var(--tblr-indigo) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-indigo{--tblr-gradient-from:var(--tblr-indigo)}.bg-gradient-to-indigo{--tblr-gradient-to:var(--tblr-indigo)}.bg-gradient-via-indigo{--tblr-gradient-via:var(--tblr-indigo);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-indigo{color:#f9fafb!important;background-color:RGBA(var(--tblr-indigo-rgb),var(--tblr-bg-opacity,1))!important}.link-indigo{color:color-mix(in srgb,var(--tblr-indigo) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-indigo) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-indigo:focus,.link-indigo:hover{color:RGBA(53,79,188,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(53,79,188,var(--tblr-link-underline-opacity,1))!important}.bg-purple{background-color:color-mix(in srgb,var(--tblr-purple) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-purple-lt{color:color-mix(in srgb,var(--tblr-purple) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-purple-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-purple{border-color:color-mix(in srgb,var(--tblr-purple) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-purple{--tblr-gradient-from:var(--tblr-purple)}.bg-gradient-to-purple{--tblr-gradient-to:var(--tblr-purple)}.bg-gradient-via-purple{--tblr-gradient-via:var(--tblr-purple);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-purple{color:#f9fafb!important;background-color:RGBA(var(--tblr-purple-rgb),var(--tblr-bg-opacity,1))!important}.link-purple{color:color-mix(in srgb,var(--tblr-purple) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-purple) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-purple:focus,.link-purple:hover{color:RGBA(139,50,161,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(139,50,161,var(--tblr-link-underline-opacity,1))!important}.bg-pink{background-color:color-mix(in srgb,var(--tblr-pink) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-pink-lt{color:color-mix(in srgb,var(--tblr-pink) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-pink-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-pink{border-color:color-mix(in srgb,var(--tblr-pink) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-pink{--tblr-gradient-from:var(--tblr-pink)}.bg-gradient-to-pink{--tblr-gradient-to:var(--tblr-pink)}.bg-gradient-via-pink{--tblr-gradient-via:var(--tblr-pink);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-pink{color:#f9fafb!important;background-color:RGBA(var(--tblr-pink-rgb),var(--tblr-bg-opacity,1))!important}.link-pink{color:color-mix(in srgb,var(--tblr-pink) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-pink) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-pink:focus,.link-pink:hover{color:RGBA(171,41,86,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(171,41,86,var(--tblr-link-underline-opacity,1))!important}.bg-red{background-color:color-mix(in srgb,var(--tblr-red) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-red-lt{color:color-mix(in srgb,var(--tblr-red) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-red-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-red{border-color:color-mix(in srgb,var(--tblr-red) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-red{--tblr-gradient-from:var(--tblr-red)}.bg-gradient-to-red{--tblr-gradient-to:var(--tblr-red)}.bg-gradient-via-red{--tblr-gradient-via:var(--tblr-red);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-red{color:#f9fafb!important;background-color:RGBA(var(--tblr-red-rgb),var(--tblr-bg-opacity,1))!important}.link-red{color:color-mix(in srgb,var(--tblr-red) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-red) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-red:focus,.link-red:hover{color:RGBA(171,46,46,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(171,46,46,var(--tblr-link-underline-opacity,1))!important}.bg-orange{background-color:color-mix(in srgb,var(--tblr-orange) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-orange-lt{color:color-mix(in srgb,var(--tblr-orange) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-orange-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-orange{border-color:color-mix(in srgb,var(--tblr-orange) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-orange{--tblr-gradient-from:var(--tblr-orange)}.bg-gradient-to-orange{--tblr-gradient-to:var(--tblr-orange)}.bg-gradient-via-orange{--tblr-gradient-via:var(--tblr-orange);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-orange{color:#f9fafb!important;background-color:RGBA(var(--tblr-orange-rgb),var(--tblr-bg-opacity,1))!important}.link-orange{color:color-mix(in srgb,var(--tblr-orange) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-orange) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-orange:focus,.link-orange:hover{color:RGBA(198,82,6,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(198,82,6,var(--tblr-link-underline-opacity,1))!important}.bg-yellow{background-color:color-mix(in srgb,var(--tblr-yellow) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-yellow-lt{color:color-mix(in srgb,var(--tblr-yellow) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-yellow-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-yellow{border-color:color-mix(in srgb,var(--tblr-yellow) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-yellow{--tblr-gradient-from:var(--tblr-yellow)}.bg-gradient-to-yellow{--tblr-gradient-to:var(--tblr-yellow)}.bg-gradient-via-yellow{--tblr-gradient-via:var(--tblr-yellow);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-yellow{color:#f9fafb!important;background-color:RGBA(var(--tblr-yellow-rgb),var(--tblr-bg-opacity,1))!important}.link-yellow{color:color-mix(in srgb,var(--tblr-yellow) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-yellow) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-yellow:focus,.link-yellow:hover{color:RGBA(196,127,0,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(196,127,0,var(--tblr-link-underline-opacity,1))!important}.bg-lime{background-color:color-mix(in srgb,var(--tblr-lime) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-lime-lt{color:color-mix(in srgb,var(--tblr-lime) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-lime-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-lime{border-color:color-mix(in srgb,var(--tblr-lime) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-lime{--tblr-gradient-from:var(--tblr-lime)}.bg-gradient-to-lime{--tblr-gradient-to:var(--tblr-lime)}.bg-gradient-via-lime{--tblr-gradient-via:var(--tblr-lime);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-lime{color:#f9fafb!important;background-color:RGBA(var(--tblr-lime-rgb),var(--tblr-bg-opacity,1))!important}.link-lime{color:color-mix(in srgb,var(--tblr-lime) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-lime) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-lime:focus,.link-lime:hover{color:RGBA(93,147,18,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(93,147,18,var(--tblr-link-underline-opacity,1))!important}.bg-green{background-color:color-mix(in srgb,var(--tblr-green) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-green-lt{color:color-mix(in srgb,var(--tblr-green) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-green-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-green{border-color:color-mix(in srgb,var(--tblr-green) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-green{--tblr-gradient-from:var(--tblr-green)}.bg-gradient-to-green{--tblr-gradient-to:var(--tblr-green)}.bg-gradient-via-green{--tblr-gradient-via:var(--tblr-green);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-green{color:#f9fafb!important;background-color:RGBA(var(--tblr-green-rgb),var(--tblr-bg-opacity,1))!important}.link-green{color:color-mix(in srgb,var(--tblr-green) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-green) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-green:focus,.link-green:hover{color:RGBA(38,143,54,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(38,143,54,var(--tblr-link-underline-opacity,1))!important}.bg-teal{background-color:color-mix(in srgb,var(--tblr-teal) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-teal-lt{color:color-mix(in srgb,var(--tblr-teal) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-teal-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-teal{border-color:color-mix(in srgb,var(--tblr-teal) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-teal{--tblr-gradient-from:var(--tblr-teal)}.bg-gradient-to-teal{--tblr-gradient-to:var(--tblr-teal)}.bg-gradient-via-teal{--tblr-gradient-via:var(--tblr-teal);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-teal{color:#f9fafb!important;background-color:RGBA(var(--tblr-teal-rgb),var(--tblr-bg-opacity,1))!important}.link-teal{color:color-mix(in srgb,var(--tblr-teal) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-teal) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-teal:focus,.link-teal:hover{color:RGBA(10,133,96,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(10,133,96,var(--tblr-link-underline-opacity,1))!important}.bg-cyan{background-color:color-mix(in srgb,var(--tblr-cyan) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-cyan-lt{color:color-mix(in srgb,var(--tblr-cyan) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-cyan-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-cyan{border-color:color-mix(in srgb,var(--tblr-cyan) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-cyan{--tblr-gradient-from:var(--tblr-cyan)}.bg-gradient-to-cyan{--tblr-gradient-to:var(--tblr-cyan)}.bg-gradient-via-cyan{--tblr-gradient-via:var(--tblr-cyan);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-cyan{color:#f9fafb!important;background-color:RGBA(var(--tblr-cyan-rgb),var(--tblr-bg-opacity,1))!important}.link-cyan{color:color-mix(in srgb,var(--tblr-cyan) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-cyan) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-cyan:focus,.link-cyan:hover{color:RGBA(18,130,147,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(18,130,147,var(--tblr-link-underline-opacity,1))!important}.bg-white{background-color:color-mix(in srgb,var(--tblr-white) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.bg-white-lt{color:color-mix(in srgb,var(--tblr-white) calc(var(--tblr-text-opacity,1) * 100%),transparent)!important;background-color:color-mix(in srgb,var(--tblr-white-lt) calc(var(--tblr-bg-opacity,1) * 100%),transparent)!important}.border-white{border-color:color-mix(in srgb,var(--tblr-white) calc(var(--tblr-border-opacity,1) * 100%),transparent)!important}.bg-gradient-from-white{--tblr-gradient-from:var(--tblr-white)}.bg-gradient-to-white{--tblr-gradient-to:var(--tblr-white)}.bg-gradient-via-white{--tblr-gradient-via:var(--tblr-white);--tblr-gradient-stops:var(--tblr-gradient-from, transparent),var(--tblr-gradient-via, transparent),var(--tblr-gradient-to, transparent)}.text-bg-white{color:#1f2937!important;background-color:RGBA(var(--tblr-white-rgb),var(--tblr-bg-opacity,1))!important}.link-white{color:color-mix(in srgb,var(--tblr-white) calc(var(--tblr-link-opacity,1) * 100%),transparent)!important;text-decoration-color:color-mix(in srgb,var(--tblr-white) calc(var(--tblr-link-underline-opacity,1) * 100%),transparent)!important}.link-white:focus,.link-white:hover{color:RGBA(255,255,255,var(--tblr-link-opacity,1))!important;text-decoration-color:RGBA(255,255,255,var(--tblr-link-underline-opacity,1))!important}.text-primary{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-primary) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-primary-fg{color:var(--tblr-primary-fg)!important}.text-secondary{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-secondary) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-secondary-fg{color:var(--tblr-secondary-fg)!important}.text-success{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-success) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-success-fg{color:var(--tblr-success-fg)!important}.text-info{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-info) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-info-fg{color:var(--tblr-info-fg)!important}.text-warning{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-warning) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-warning-fg{color:var(--tblr-warning-fg)!important}.text-danger{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-danger) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-danger-fg{color:var(--tblr-danger-fg)!important}.text-light{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-light) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-light-fg{color:var(--tblr-light-fg)!important}.text-dark{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-dark) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-dark-fg{color:var(--tblr-dark-fg)!important}.text-muted{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-muted) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-muted-fg{color:var(--tblr-muted-fg)!important}.text-blue{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-blue) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-blue-fg{color:var(--tblr-blue-fg)!important}.text-azure{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-azure) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-azure-fg{color:var(--tblr-azure-fg)!important}.text-indigo{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-indigo) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-indigo-fg{color:var(--tblr-indigo-fg)!important}.text-purple{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-purple) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-purple-fg{color:var(--tblr-purple-fg)!important}.text-pink{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-pink) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-pink-fg{color:var(--tblr-pink-fg)!important}.text-red{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-red) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-red-fg{color:var(--tblr-red-fg)!important}.text-orange{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-orange) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-orange-fg{color:var(--tblr-orange-fg)!important}.text-yellow{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-yellow) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-yellow-fg{color:var(--tblr-yellow-fg)!important}.text-lime{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-lime) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-lime-fg{color:var(--tblr-lime-fg)!important}.text-green{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-green) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-green-fg{color:var(--tblr-green-fg)!important}.text-teal{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-teal) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-teal-fg{color:var(--tblr-teal-fg)!important}.text-cyan{--tblr-text-opacity:1;color:color-mix(in srgb,var(--tblr-cyan) calc(var(--tblr-text-opacity) * 100%),transparent)!important}.text-cyan-fg{color:var(--tblr-cyan-fg)!important}.bg-gray-50{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-gray-50) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-gray-50-fg{color:var(--tblr-gray-50-fg)!important}.bg-gray-100{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-gray-100) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-gray-100-fg{color:var(--tblr-gray-100-fg)!important}.bg-gray-200{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-gray-200) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-gray-200-fg{color:var(--tblr-gray-200-fg)!important}.bg-gray-300{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-gray-300) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-gray-300-fg{color:var(--tblr-gray-300-fg)!important}.bg-gray-400{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-gray-400) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-gray-400-fg{color:var(--tblr-gray-400-fg)!important}.bg-gray-500{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-gray-500) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-gray-500-fg{color:var(--tblr-gray-500-fg)!important}.bg-gray-600{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-gray-600) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-gray-600-fg{color:var(--tblr-gray-600-fg)!important}.bg-gray-700{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-gray-700) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-gray-700-fg{color:var(--tblr-gray-700-fg)!important}.bg-gray-800{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-gray-800) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-gray-800-fg{color:var(--tblr-gray-800-fg)!important}.bg-gray-900{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-gray-900) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-gray-900-fg{color:var(--tblr-gray-900-fg)!important}.bg-gray-950{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-gray-950) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-gray-950-fg{color:var(--tblr-gray-950-fg)!important}.bg-x{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-x) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-x-fg{color:var(--tblr-x-fg)!important}.bg-facebook{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-facebook) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-facebook-fg{color:var(--tblr-facebook-fg)!important}.bg-twitter{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-twitter) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-twitter-fg{color:var(--tblr-twitter-fg)!important}.bg-linkedin{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-linkedin) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-linkedin-fg{color:var(--tblr-linkedin-fg)!important}.bg-google{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-google) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-google-fg{color:var(--tblr-google-fg)!important}.bg-youtube{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-youtube) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-youtube-fg{color:var(--tblr-youtube-fg)!important}.bg-vimeo{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-vimeo) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-vimeo-fg{color:var(--tblr-vimeo-fg)!important}.bg-dribbble{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-dribbble) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-dribbble-fg{color:var(--tblr-dribbble-fg)!important}.bg-github{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-github) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-github-fg{color:var(--tblr-github-fg)!important}.bg-instagram{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-instagram) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-instagram-fg{color:var(--tblr-instagram-fg)!important}.bg-pinterest{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-pinterest) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-pinterest-fg{color:var(--tblr-pinterest-fg)!important}.bg-vk{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-vk) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-vk-fg{color:var(--tblr-vk-fg)!important}.bg-rss{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-rss) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-rss-fg{color:var(--tblr-rss-fg)!important}.bg-flickr{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-flickr) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-flickr-fg{color:var(--tblr-flickr-fg)!important}.bg-bitbucket{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-bitbucket) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-bitbucket-fg{color:var(--tblr-bitbucket-fg)!important}.bg-tabler{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-tabler) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.text-tabler-fg{color:var(--tblr-tabler-fg)!important}.bg-inverted{--tblr-bg-opacity:1;background-color:color-mix(in srgb,var(--tblr-bg-surface-inverted) calc(var(--tblr-bg-opacity) * 100%),transparent)!important}.bg-surface{background-color:var(--tblr-bg-surface)!important}.bg-surface-secondary{background-color:var(--tblr-bg-surface-secondary)!important}.bg-surface-tertiary{background-color:var(--tblr-bg-surface-tertiary)!important}.bg-surface-backdrop{background-color:color-mix(in srgb,var(--tblr-gray-800) 24%,transparent)!important}.scrollable{overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch}.scrollable.hover{overflow-y:hidden}.scrollable.hover>*{margin-top:-1px}.scrollable.hover:active,.scrollable.hover:focus,.scrollable.hover:hover{overflow:visible;overflow-y:auto}.touch .scrollable{overflow-y:auto!important}.scroll-x,.scroll-y{overflow:hidden;-webkit-overflow-scrolling:touch}.scroll-y{overflow-y:auto}.scroll-x{overflow-x:auto}.no-scroll{overflow:hidden}.w-0{width:0!important}.h-0{height:0!important}.w-1{width:.25rem!important}.h-1{height:.25rem!important}.w-2{width:.5rem!important}.h-2{height:.5rem!important}.w-3{width:1rem!important}.h-3{height:1rem!important}.w-4{width:1.5rem!important}.h-4{height:1.5rem!important}.w-5{width:2rem!important}.h-5{height:2rem!important}.w-6{width:2.5rem!important}.h-6{height:2.5rem!important}.w-auto{width:auto!important}.h-auto{height:auto!important}.w-px{width:1px!important}.h-px{height:1px!important}.w-full{width:100%!important}.h-full{height:100%!important}.opacity-0{opacity:calc(0 / 100)!important}.opacity-5{opacity:calc(5 / 100)!important}.opacity-10{opacity:calc(10 / 100)!important}.opacity-15{opacity:calc(15 / 100)!important}.opacity-20{opacity:calc(20 / 100)!important}.opacity-25{opacity:calc(25 / 100)!important}.opacity-30{opacity:calc(30 / 100)!important}.opacity-35{opacity:calc(35 / 100)!important}.opacity-40{opacity:calc(40 / 100)!important}.opacity-45{opacity:calc(45 / 100)!important}.opacity-50{opacity:calc(50 / 100)!important}.opacity-55{opacity:calc(55 / 100)!important}.opacity-60{opacity:calc(60 / 100)!important}.opacity-65{opacity:calc(65 / 100)!important}.opacity-70{opacity:calc(70 / 100)!important}.opacity-75{opacity:calc(75 / 100)!important}.opacity-80{opacity:calc(80 / 100)!important}.opacity-85{opacity:calc(85 / 100)!important}.opacity-90{opacity:calc(90 / 100)!important}.opacity-95{opacity:calc(95 / 100)!important}.opacity-100{opacity:calc(100 / 100)!important}.hover-shadow-sm:hover{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.hover-shadow:hover{box-shadow:rgba(var(--tblr-body-color-rgb),.04) 0 2px 4px 0!important}.hover-shadow-lg:hover{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.hover-shadow-none:hover{box-shadow:none!important}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.subpixel-antialiased{-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}.hover-elevate-down,.hover-elevate-up,.hover-rotate-end,.hover-rotate-start,.hover-scale{transition:transform .3s ease}.hover-elevate-down:hover,.hover-elevate-up:hover,.hover-rotate-end:hover,.hover-rotate-start:hover,.hover-scale:hover{will-change:transform}.hover-elevate-up:hover{transform:translateY(-4px)}.hover-elevate-down:hover{transform:translateY(4px)}.hover-scale:hover{transform:scale(1.1)}.hover-rotate-end:hover{transform:rotate(4deg)}.hover-rotate-start:hover{transform:rotate(-4deg)} +/*# sourceMappingURL=tabler.min.css.map */ \ No newline at end of file diff --git a/frontend/public/vendor/tabler/tabler.min.js b/frontend/public/vendor/tabler/tabler.min.js new file mode 100644 index 0000000..5be114e --- /dev/null +++ b/frontend/public/vendor/tabler/tabler.min.js @@ -0,0 +1,13 @@ +/*! + * Tabler v1.4.0 (https://tabler.io) + * Copyright 2018-2025 The Tabler Authors + * Copyright 2018-2025 codecalm.net Paweł Kuna + * Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).tabler={})}(this,function(t){"use strict";const e=document.querySelectorAll('[data-bs-toggle="autosize"]');e.length&&e.forEach(function(t){window.autosize&&window.autosize(t)});const i=document.querySelectorAll("[data-countup]");i.length&&i.forEach(function(t){let e={};try{const i=t.getAttribute("data-countup")?JSON.parse(t.getAttribute("data-countup")):{};e=Object.assign({enableScrollSpy:!0},i)}catch(t){}const i=parseInt(t.innerHTML,10);if(window.countUp&&window.countUp.CountUp){const n=new window.countUp.CountUp(t,i,e);n.error||n.start()}}),[].slice.call(document.querySelectorAll("[data-mask]")).map(function(t){window.IMask&&new window.IMask(t,{mask:t.dataset.mask,lazy:"true"===t.dataset["mask-visible"]})});var n="top",s="bottom",o="right",r="left",a="auto",l=[n,s,o,r],c="start",h="end",u="clippingParents",d="viewport",f="popper",p="reference",m=l.reduce(function(t,e){return t.concat([e+"-"+c,e+"-"+h])},[]),g=[].concat(l,[a]).reduce(function(t,e){return t.concat([e,e+"-"+c,e+"-"+h])},[]),_="beforeRead",b="read",v="afterRead",y="beforeMain",w="main",A="afterMain",E="beforeWrite",T="write",C="afterWrite",O=[_,b,v,y,w,A,E,T,C];function x(t){return t?(t.nodeName||"").toLowerCase():null}function k(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function S(t){return t instanceof k(t).Element||t instanceof Element}function L(t){return t instanceof k(t).HTMLElement||t instanceof HTMLElement}function $(t){return"undefined"!=typeof ShadowRoot&&(t instanceof k(t).ShadowRoot||t instanceof ShadowRoot)}const D={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach(function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];L(s)&&x(s)&&(Object.assign(s.style,i),Object.keys(n).forEach(function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)}))})},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach(function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce(function(t,e){return t[e]="",t},{});L(n)&&x(n)&&(Object.assign(n.style,o),Object.keys(s).forEach(function(t){n.removeAttribute(t)}))})}},requires:["computeStyles"]};function I(t){return t.split("-")[0]}var P=Math.max,N=Math.min,M=Math.round;function j(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map(function(t){return t.brand+"/"+t.version}).join(" "):navigator.userAgent}function F(){return!/^((?!chrome|android).)*safari/i.test(j())}function H(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&L(t)&&(s=t.offsetWidth>0&&M(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&M(n.height)/t.offsetHeight||1);var r=(S(t)?k(t):window).visualViewport,a=!F()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,u=n.height/o;return{width:h,height:u,top:c,right:l+h,bottom:c+u,left:l,x:l,y:c}}function z(t){var e=H(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function B(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&$(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function W(t){return k(t).getComputedStyle(t)}function q(t){return["table","td","th"].indexOf(x(t))>=0}function R(t){return((S(t)?t.ownerDocument:t.document)||window.document).documentElement}function V(t){return"html"===x(t)?t:t.assignedSlot||t.parentNode||($(t)?t.host:null)||R(t)}function U(t){return L(t)&&"fixed"!==W(t).position?t.offsetParent:null}function K(t){for(var e=k(t),i=U(t);i&&q(i)&&"static"===W(i).position;)i=U(i);return i&&("html"===x(i)||"body"===x(i)&&"static"===W(i).position)?e:i||function(t){var e=/firefox/i.test(j());if(/Trident/i.test(j())&&L(t)&&"fixed"===W(t).position)return null;var i=V(t);for($(i)&&(i=i.host);L(i)&&["html","body"].indexOf(x(i))<0;){var n=W(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Q(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function X(t,e,i){return P(t,N(e,i))}function Y(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function G(t,e){return e.reduce(function(e,i){return e[i]=t,e},{})}const J={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,a=t.name,c=t.options,h=i.elements.arrow,u=i.modifiersData.popperOffsets,d=I(i.placement),f=Q(d),p=[r,o].indexOf(d)>=0?"height":"width";if(h&&u){var m=function(t,e){return Y("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:G(t,l))}(c.padding,i),g=z(h),_="y"===f?n:r,b="y"===f?s:o,v=i.rects.reference[p]+i.rects.reference[f]-u[f]-i.rects.popper[p],y=u[f]-i.rects.reference[f],w=K(h),A=w?"y"===f?w.clientHeight||0:w.clientWidth||0:0,E=v/2-y/2,T=m[_],C=A-g[p]-m[b],O=A/2-g[p]/2+E,x=X(T,O,C),k=f;i.modifiersData[a]=((e={})[k]=x,e.centerOffset=x-O,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&B(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Z(t){return t.split("-")[1]}var tt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function et(t){var e,i=t.popper,a=t.popperRect,l=t.placement,c=t.variation,u=t.offsets,d=t.position,f=t.gpuAcceleration,p=t.adaptive,m=t.roundOffsets,g=t.isFixed,_=u.x,b=void 0===_?0:_,v=u.y,y=void 0===v?0:v,w="function"==typeof m?m({x:b,y:y}):{x:b,y:y};b=w.x,y=w.y;var A=u.hasOwnProperty("x"),E=u.hasOwnProperty("y"),T=r,C=n,O=window;if(p){var x=K(i),S="clientHeight",L="clientWidth";x===k(i)&&"static"!==W(x=R(i)).position&&"absolute"===d&&(S="scrollHeight",L="scrollWidth"),(l===n||(l===r||l===o)&&c===h)&&(C=s,y-=(g&&x===O&&O.visualViewport?O.visualViewport.height:x[S])-a.height,y*=f?1:-1),l!==r&&(l!==n&&l!==s||c!==h)||(T=o,b-=(g&&x===O&&O.visualViewport?O.visualViewport.width:x[L])-a.width,b*=f?1:-1)}var $,D=Object.assign({position:d},p&&tt),I=!0===m?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:M(i*s)/s||0,y:M(n*s)/s||0}}({x:b,y:y},k(i)):{x:b,y:y};return b=I.x,y=I.y,f?Object.assign({},D,(($={})[C]=E?"0":"",$[T]=A?"0":"",$.transform=(O.devicePixelRatio||1)<=1?"translate("+b+"px, "+y+"px)":"translate3d("+b+"px, "+y+"px, 0)",$)):Object.assign({},D,((e={})[C]=E?y+"px":"",e[T]=A?b+"px":"",e.transform="",e))}const it={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:I(e.placement),variation:Z(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,et(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,et(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var nt={passive:!0};const st={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=k(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach(function(t){t.addEventListener("scroll",i.update,nt)}),a&&l.addEventListener("resize",i.update,nt),function(){o&&c.forEach(function(t){t.removeEventListener("scroll",i.update,nt)}),a&&l.removeEventListener("resize",i.update,nt)}},data:{}};var ot={left:"right",right:"left",bottom:"top",top:"bottom"};function rt(t){return t.replace(/left|right|bottom|top/g,function(t){return ot[t]})}var at={start:"end",end:"start"};function lt(t){return t.replace(/start|end/g,function(t){return at[t]})}function ct(t){var e=k(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ht(t){return H(R(t)).left+ct(t).scrollLeft}function ut(t){var e=W(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function dt(t){return["html","body","#document"].indexOf(x(t))>=0?t.ownerDocument.body:L(t)&&ut(t)?t:dt(V(t))}function ft(t,e){var i;void 0===e&&(e=[]);var n=dt(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=k(n),r=s?[o].concat(o.visualViewport||[],ut(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(ft(V(r)))}function pt(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function mt(t,e,i){return e===d?pt(function(t,e){var i=k(t),n=R(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=F();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+ht(t),y:l}}(t,i)):S(e)?function(t,e){var i=H(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):pt(function(t){var e,i=R(t),n=ct(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=P(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=P(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ht(t),l=-n.scrollTop;return"rtl"===W(s||i).direction&&(a+=P(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(R(t)))}function gt(t){var e,i=t.reference,a=t.element,l=t.placement,u=l?I(l):null,d=l?Z(l):null,f=i.x+i.width/2-a.width/2,p=i.y+i.height/2-a.height/2;switch(u){case n:e={x:f,y:i.y-a.height};break;case s:e={x:f,y:i.y+i.height};break;case o:e={x:i.x+i.width,y:p};break;case r:e={x:i.x-a.width,y:p};break;default:e={x:i.x,y:i.y}}var m=u?Q(u):null;if(null!=m){var g="y"===m?"height":"width";switch(d){case c:e[m]=e[m]-(i[g]/2-a[g]/2);break;case h:e[m]=e[m]+(i[g]/2-a[g]/2)}}return e}function _t(t,e){void 0===e&&(e={});var i=e,r=i.placement,a=void 0===r?t.placement:r,c=i.strategy,h=void 0===c?t.strategy:c,m=i.boundary,g=void 0===m?u:m,_=i.rootBoundary,b=void 0===_?d:_,v=i.elementContext,y=void 0===v?f:v,w=i.altBoundary,A=void 0!==w&&w,E=i.padding,T=void 0===E?0:E,C=Y("number"!=typeof T?T:G(T,l)),O=y===f?p:f,k=t.rects.popper,$=t.elements[A?O:y],D=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=ft(V(t)),i=["absolute","fixed"].indexOf(W(t).position)>=0&&L(t)?K(t):t;return S(i)?e.filter(function(t){return S(t)&&B(t,i)&&"body"!==x(t)}):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce(function(e,i){var s=mt(t,i,n);return e.top=P(s.top,e.top),e.right=N(s.right,e.right),e.bottom=N(s.bottom,e.bottom),e.left=P(s.left,e.left),e},mt(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(S($)?$:$.contextElement||R(t.elements.popper),g,b,h),I=H(t.elements.reference),M=gt({reference:I,element:k,placement:a}),j=pt(Object.assign({},k,M)),F=y===f?j:I,z={top:D.top-F.top+C.top,bottom:F.bottom-D.bottom+C.bottom,left:D.left-F.left+C.left,right:F.right-D.right+C.right},q=t.modifiersData.offset;if(y===f&&q){var U=q[a];Object.keys(z).forEach(function(t){var e=[o,s].indexOf(t)>=0?1:-1,i=[n,s].indexOf(t)>=0?"y":"x";z[t]+=U[i]*e})}return z}function bt(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,c=i.allowedAutoPlacements,h=void 0===c?g:c,u=Z(n),d=u?a?m:m.filter(function(t){return Z(t)===u}):l,f=d.filter(function(t){return h.indexOf(t)>=0});0===f.length&&(f=d);var p=f.reduce(function(e,i){return e[i]=_t(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[I(i)],e},{});return Object.keys(p).sort(function(t,e){return p[t]-p[e]})}const vt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,l=t.name;if(!e.modifiersData[l]._skip){for(var h=i.mainAxis,u=void 0===h||h,d=i.altAxis,f=void 0===d||d,p=i.fallbackPlacements,m=i.padding,g=i.boundary,_=i.rootBoundary,b=i.altBoundary,v=i.flipVariations,y=void 0===v||v,w=i.allowedAutoPlacements,A=e.options.placement,E=I(A),T=p||(E!==A&&y?function(t){if(I(t)===a)return[];var e=rt(t);return[lt(t),e,lt(e)]}(A):[rt(A)]),C=[A].concat(T).reduce(function(t,i){return t.concat(I(i)===a?bt(e,{placement:i,boundary:g,rootBoundary:_,padding:m,flipVariations:y,allowedAutoPlacements:w}):i)},[]),O=e.rects.reference,x=e.rects.popper,k=new Map,S=!0,L=C[0],$=0;$=0,j=M?"width":"height",F=_t(e,{placement:D,boundary:g,rootBoundary:_,altBoundary:b,padding:m}),H=M?N?o:r:N?s:n;O[j]>x[j]&&(H=rt(H));var z=rt(H),B=[];if(u&&B.push(F[P]<=0),f&&B.push(F[H]<=0,F[z]<=0),B.every(function(t){return t})){L=D,S=!1;break}k.set(D,B)}if(S)for(var W=function(t){var e=C.find(function(e){var i=k.get(e);if(i)return i.slice(0,t).every(function(t){return t})});if(e)return L=e,"break"},q=y?3:1;q>0&&"break"!==W(q);q--);e.placement!==L&&(e.modifiersData[l]._skip=!0,e.placement=L,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function yt(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function wt(t){return[n,o,s,r].some(function(e){return t[e]>=0})}const At={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=_t(e,{elementContext:"reference"}),a=_t(e,{altBoundary:!0}),l=yt(r,n),c=yt(a,s,o),h=wt(l),u=wt(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:u},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":u})}},Et={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,s=t.name,a=i.offset,l=void 0===a?[0,0]:a,c=g.reduce(function(t,i){return t[i]=function(t,e,i){var s=I(t),a=[r,n].indexOf(s)>=0?-1:1,l="function"==typeof i?i(Object.assign({},e,{placement:t})):i,c=l[0],h=l[1];return c=c||0,h=(h||0)*a,[r,o].indexOf(s)>=0?{x:h,y:c}:{x:c,y:h}}(i,e.rects,l),t},{}),h=c[e.placement],u=h.x,d=h.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=u,e.modifiersData.popperOffsets.y+=d),e.modifiersData[s]=c}},Tt={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=gt({reference:e.rects.reference,element:e.rects.popper,placement:e.placement})},data:{}},Ct={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,a=t.name,l=i.mainAxis,h=void 0===l||l,u=i.altAxis,d=void 0!==u&&u,f=i.boundary,p=i.rootBoundary,m=i.altBoundary,g=i.padding,_=i.tether,b=void 0===_||_,v=i.tetherOffset,y=void 0===v?0:v,w=_t(e,{boundary:f,rootBoundary:p,padding:g,altBoundary:m}),A=I(e.placement),E=Z(e.placement),T=!E,C=Q(A),O="x"===C?"y":"x",x=e.modifiersData.popperOffsets,k=e.rects.reference,S=e.rects.popper,L="function"==typeof y?y(Object.assign({},e.rects,{placement:e.placement})):y,$="number"==typeof L?{mainAxis:L,altAxis:L}:Object.assign({mainAxis:0,altAxis:0},L),D=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,M={x:0,y:0};if(x){if(h){var j,F="y"===C?n:r,H="y"===C?s:o,B="y"===C?"height":"width",W=x[C],q=W+w[F],R=W-w[H],V=b?-S[B]/2:0,U=E===c?k[B]:S[B],Y=E===c?-S[B]:-k[B],G=e.elements.arrow,J=b&&G?z(G):{width:0,height:0},tt=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},et=tt[F],it=tt[H],nt=X(0,k[B],J[B]),st=T?k[B]/2-V-nt-et-$.mainAxis:U-nt-et-$.mainAxis,ot=T?-k[B]/2+V+nt+it+$.mainAxis:Y+nt+it+$.mainAxis,rt=e.elements.arrow&&K(e.elements.arrow),at=rt?"y"===C?rt.clientTop||0:rt.clientLeft||0:0,lt=null!=(j=null==D?void 0:D[C])?j:0,ct=W+ot-lt,ht=X(b?N(q,W+st-lt-at):q,W,b?P(R,ct):R);x[C]=ht,M[C]=ht-W}if(d){var ut,dt="x"===C?n:r,ft="x"===C?s:o,pt=x[O],mt="y"===O?"height":"width",gt=pt+w[dt],bt=pt-w[ft],vt=-1!==[n,r].indexOf(A),yt=null!=(ut=null==D?void 0:D[O])?ut:0,wt=vt?gt:pt-k[mt]-S[mt]-yt+$.altAxis,At=vt?pt+k[mt]+S[mt]-yt-$.altAxis:bt,Et=b&&vt?function(t,e,i){var n=X(t,e,i);return n>i?i:n}(wt,pt,At):X(b?wt:gt,pt,b?At:bt);x[O]=Et,M[O]=Et-pt}e.modifiersData[a]=M}},requiresIfExists:["offset"]};function Ot(t,e,i){void 0===i&&(i=!1);var n,s,o=L(e),r=L(e)&&function(t){var e=t.getBoundingClientRect(),i=M(e.width)/t.offsetWidth||1,n=M(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=R(e),l=H(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==x(e)||ut(a))&&(c=(n=e)!==k(n)&&L(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:ct(n)),L(e)?((h=H(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=ht(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function xt(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach(function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}}),n.push(t)}return t.forEach(function(t){e.set(t.name,t)}),t.forEach(function(t){i.has(t.name)||s(t)}),n}var kt={placement:"bottom",modifiers:[],strategy:"absolute"};function St(){for(var t=arguments.length,e=new Array(t),i=0;iNt.has(t)&&Nt.get(t).get(e)||null,remove(t,e){if(!Nt.has(t))return;const i=Nt.get(t);i.delete(e),0===i.size&&Nt.delete(t)}},jt="transitionend",Ft=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,(t,e)=>`#${CSS.escape(e)}`)),t),Ht=t=>null==t?`${t}`:Object.prototype.toString.call(t).match(/\s([a-z]+)/i)[1].toLowerCase(),zt=t=>{t.dispatchEvent(new Event(jt))},Bt=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),Wt=t=>Bt(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(Ft(t)):null,qt=t=>{if(!Bt(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},Rt=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),Vt=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?Vt(t.parentNode):null},Ut=()=>{},Kt=t=>{t.offsetHeight},Qt=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,Xt=[],Yt=()=>"rtl"===document.documentElement.dir,Gt=t=>{var e;e=()=>{const e=Qt();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(Xt.length||document.addEventListener("DOMContentLoaded",()=>{for(const t of Xt)t()}),Xt.push(e)):e()},Jt=(t,e=[],i=t)=>"function"==typeof t?t.call(...e):i,Zt=(t,e,i=!0)=>{if(!i)return void Jt(t);const n=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let s=!1;const o=({target:i})=>{i===e&&(s=!0,e.removeEventListener(jt,o),Jt(t))};e.addEventListener(jt,o),setTimeout(()=>{s||zt(e)},n)},te=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},ee=/[^.]*(?=\..*)\.|.*/,ie=/\..*/,ne=/::\d+$/,se={}; +/*! + * Bootstrap v5.3.7 (https://getbootstrap.com/) + * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */let oe=1;const re={mouseenter:"mouseover",mouseleave:"mouseout"},ae=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function le(t,e){return e&&`${e}::${oe++}`||t.uidEvent||oe++}function ce(t){const e=le(t);return t.uidEvent=e,se[e]=se[e]||{},se[e]}function he(t,e,i=null){return Object.values(t).find(t=>t.callable===e&&t.delegationSelector===i)}function ue(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=me(t);return ae.has(o)||(o=t),[n,s,o]}function de(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=ue(e,i,n);if(e in re){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=ce(t),c=l[a]||(l[a]={}),h=he(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const u=le(r,e.replace(ee,"")),d=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return _e(s,{delegateTarget:r}),n.oneOff&&ge.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return _e(n,{delegateTarget:t}),i.oneOff&&ge.off(t,n.type,e),e.apply(t,[n])}}(t,r);d.delegationSelector=o?i:null,d.callable=r,d.oneOff=s,d.uidEvent=u,c[u]=d,t.addEventListener(a,d,o)}function fe(t,e,i,n,s){const o=he(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function pe(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&fe(t,e,i,r.callable,r.delegationSelector)}function me(t){return t=t.replace(ie,""),re[t]||t}const ge={on(t,e,i,n){de(t,e,i,n,!1)},one(t,e,i,n){de(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=ue(e,i,n),a=r!==e,l=ce(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))pe(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(ne,"");a&&!e.includes(s)||fe(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;fe(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=Qt();let s=null,o=!0,r=!0,a=!1;e!==me(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=_e(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function _e(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function be(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function ve(t){return t.replace(/[A-Z]/g,t=>`-${t.toLowerCase()}`)}const ye={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${ve(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${ve(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter(t=>t.startsWith("bs")&&!t.startsWith("bsConfig"));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1),e[i]=be(t.dataset[n])}return e},getDataAttribute:(t,e)=>be(t.getAttribute(`data-bs-${ve(e)}`))};class we{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=Bt(e)?ye.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...Bt(e)?ye.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[i,n]of Object.entries(e)){const e=t[i],s=Bt(e)?"element":Ht(e);if(!new RegExp(n).test(s))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${i}" provided type "${s}" but expected type "${n}".`)}}}class Ae extends we{constructor(t,e){super(),(t=Wt(t))&&(this._element=t,this._config=this._getConfig(e),Mt.set(this._element,this.constructor.DATA_KEY,this))}dispose(){Mt.remove(this._element,this.constructor.DATA_KEY),ge.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){Zt(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return Mt.get(Wt(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.7"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const Ee=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map(t=>Ft(t)).join(","):null},Te={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter(t=>t.matches(e)),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(t=>`${t}:not([tabindex^="-"])`).join(",");return this.find(e,t).filter(t=>!Rt(t)&&qt(t))},getSelectorFromElement(t){const e=Ee(t);return e&&Te.findOne(e)?e:null},getElementFromSelector(t){const e=Ee(t);return e?Te.findOne(e):null},getMultipleElementsFromSelector(t){const e=Ee(t);return e?Te.find(e):[]}},Ce=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;ge.on(document,i,`[data-bs-dismiss="${n}"]`,function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),Rt(this))return;const s=Te.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()})},Oe=".bs.alert",xe=`close${Oe}`,ke=`closed${Oe}`;class Se extends Ae{static get NAME(){return"alert"}close(){if(ge.trigger(this._element,xe).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback(()=>this._destroyElement(),this._element,t)}_destroyElement(){this._element.remove(),ge.trigger(this._element,ke),this.dispose()}static jQueryInterface(t){return this.each(function(){const e=Se.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}})}}Ce(Se,"close"),Gt(Se);const Le='[data-bs-toggle="button"]';class $e extends Ae{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each(function(){const e=$e.getOrCreateInstance(this);"toggle"===t&&e[t]()})}}ge.on(document,"click.bs.button.data-api",Le,t=>{t.preventDefault();const e=t.target.closest(Le);$e.getOrCreateInstance(e).toggle()}),Gt($e);const De=".bs.swipe",Ie=`touchstart${De}`,Pe=`touchmove${De}`,Ne=`touchend${De}`,Me=`pointerdown${De}`,je=`pointerup${De}`,Fe={endCallback:null,leftCallback:null,rightCallback:null},He={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class ze extends we{constructor(t,e){super(),this._element=t,t&&ze.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return Fe}static get DefaultType(){return He}static get NAME(){return"swipe"}dispose(){ge.off(this._element,De)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),Jt(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&Jt(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(ge.on(this._element,Me,t=>this._start(t)),ge.on(this._element,je,t=>this._end(t)),this._element.classList.add("pointer-event")):(ge.on(this._element,Ie,t=>this._start(t)),ge.on(this._element,Pe,t=>this._move(t)),ge.on(this._element,Ne,t=>this._end(t)))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const Be=".bs.carousel",We=".data-api",qe="ArrowLeft",Re="ArrowRight",Ve="next",Ue="prev",Ke="left",Qe="right",Xe=`slide${Be}`,Ye=`slid${Be}`,Ge=`keydown${Be}`,Je=`mouseenter${Be}`,Ze=`mouseleave${Be}`,ti=`dragstart${Be}`,ei=`load${Be}${We}`,ii=`click${Be}${We}`,ni="carousel",si="active",oi=".active",ri=".carousel-item",ai=oi+ri,li={[qe]:Qe,[Re]:Ke},ci={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},hi={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class ui extends Ae{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=Te.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===ni&&this.cycle()}static get Default(){return ci}static get DefaultType(){return hi}static get NAME(){return"carousel"}next(){this._slide(Ve)}nextWhenVisible(){!document.hidden&&qt(this._element)&&this.next()}prev(){this._slide(Ue)}pause(){this._isSliding&&zt(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval(()=>this.nextWhenVisible(),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?ge.one(this._element,Ye,()=>this.cycle()):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void ge.one(this._element,Ye,()=>this.to(t));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?Ve:Ue;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&ge.on(this._element,Ge,t=>this._keydown(t)),"hover"===this._config.pause&&(ge.on(this._element,Je,()=>this.pause()),ge.on(this._element,Ze,()=>this._maybeEnableCycle())),this._config.touch&&ze.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of Te.find(".carousel-item img",this._element))ge.on(t,ti,t=>t.preventDefault());const t={leftCallback:()=>this._slide(this._directionToOrder(Ke)),rightCallback:()=>this._slide(this._directionToOrder(Qe)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(()=>this._maybeEnableCycle(),500+this._config.interval))}};this._swipeHelper=new ze(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=li[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=Te.findOne(oi,this._indicatorsElement);e.classList.remove(si),e.removeAttribute("aria-current");const i=Te.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(si),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===Ve,s=e||te(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>ge.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(Xe).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),Kt(s),i.classList.add(l),s.classList.add(l),this._queueCallback(()=>{s.classList.remove(l,c),s.classList.add(si),i.classList.remove(si,c,l),this._isSliding=!1,r(Ye)},i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return Te.findOne(ai,this._element)}_getItems(){return Te.find(ri,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return Yt()?t===Ke?Ue:Ve:t===Ke?Ve:Ue}_orderToDirection(t){return Yt()?t===Ue?Ke:Qe:t===Ue?Qe:Ke}static jQueryInterface(t){return this.each(function(){const e=ui.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)})}}ge.on(document,ii,"[data-bs-slide], [data-bs-slide-to]",function(t){const e=Te.getElementFromSelector(this);if(!e||!e.classList.contains(ni))return;t.preventDefault();const i=ui.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===ye.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())}),ge.on(window,ei,()=>{const t=Te.find('[data-bs-ride="carousel"]');for(const e of t)ui.getOrCreateInstance(e)}),Gt(ui);const di=".bs.collapse",fi=`show${di}`,pi=`shown${di}`,mi=`hide${di}`,gi=`hidden${di}`,_i=`click${di}.data-api`,bi="show",vi="collapse",yi="collapsing",wi=`:scope .${vi} .${vi}`,Ai='[data-bs-toggle="collapse"]',Ei={parent:null,toggle:!0},Ti={parent:"(null|element)",toggle:"boolean"};class Ci extends Ae{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=Te.find(Ai);for(const t of i){const e=Te.getSelectorFromElement(t),i=Te.find(e).filter(t=>t===this._element);null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Ei}static get DefaultType(){return Ti}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter(t=>t!==this._element).map(t=>Ci.getOrCreateInstance(t,{toggle:!1}))),t.length&&t[0]._isTransitioning)return;if(ge.trigger(this._element,fi).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(vi),this._element.classList.add(yi),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove(yi),this._element.classList.add(vi,bi),this._element.style[e]="",ge.trigger(this._element,pi)},this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(ge.trigger(this._element,mi).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,Kt(this._element),this._element.classList.add(yi),this._element.classList.remove(vi,bi);for(const t of this._triggerArray){const e=Te.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove(yi),this._element.classList.add(vi),ge.trigger(this._element,gi)},this._element,!0)}_isShown(t=this._element){return t.classList.contains(bi)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=Wt(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Ai);for(const e of t){const t=Te.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=Te.find(wi,this._config.parent);return Te.find(t,this._config.parent).filter(t=>!e.includes(t))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each(function(){const i=Ci.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}})}}ge.on(document,_i,Ai,function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of Te.getMultipleElementsFromSelector(this))Ci.getOrCreateInstance(t,{toggle:!1}).toggle()}),Gt(Ci);const Oi="dropdown",xi=".bs.dropdown",ki=".data-api",Si="ArrowUp",Li="ArrowDown",$i=`hide${xi}`,Di=`hidden${xi}`,Ii=`show${xi}`,Pi=`shown${xi}`,Ni=`click${xi}${ki}`,Mi=`keydown${xi}${ki}`,ji=`keyup${xi}${ki}`,Fi="show",Hi='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',zi=`${Hi}.${Fi}`,Bi=".dropdown-menu",Wi=Yt()?"top-end":"top-start",qi=Yt()?"top-start":"top-end",Ri=Yt()?"bottom-end":"bottom-start",Vi=Yt()?"bottom-start":"bottom-end",Ui=Yt()?"left-start":"right-start",Ki=Yt()?"right-start":"left-start",Qi={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},Xi={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class Yi extends Ae{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=Te.next(this._element,Bi)[0]||Te.prev(this._element,Bi)[0]||Te.findOne(Bi,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return Qi}static get DefaultType(){return Xi}static get NAME(){return Oi}toggle(){return this._isShown()?this.hide():this.show()}show(){if(Rt(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!ge.trigger(this._element,Ii,t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))ge.on(t,"mouseover",Ut);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Fi),this._element.classList.add(Fi),ge.trigger(this._element,Pi,t)}}hide(){if(Rt(this._element)||!this._isShown())return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){if(!ge.trigger(this._element,$i,t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))ge.off(t,"mouseover",Ut);this._popper&&this._popper.destroy(),this._menu.classList.remove(Fi),this._element.classList.remove(Fi),this._element.setAttribute("aria-expanded","false"),ye.removeDataAttribute(this._menu,"popper"),ge.trigger(this._element,Di,t),this._element.focus()}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!Bt(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Oi.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){if(void 0===Pt)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org/docs/v2/)");let t=this._element;"parent"===this._config.reference?t=this._parent:Bt(this._config.reference)?t=Wt(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const e=this._getPopperConfig();this._popper=It(t,this._menu,e)}_isShown(){return this._menu.classList.contains(Fi)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return Ui;if(t.classList.contains("dropstart"))return Ki;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?qi:Wi:e?Vi:Ri}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(ye.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...Jt(this._config.popperConfig,[void 0,t])}}_selectMenuItem({key:t,target:e}){const i=Te.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(t=>qt(t));i.length&&te(i,e,t===Li,!i.includes(e)).focus()}static jQueryInterface(t){return this.each(function(){const e=Yi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=Te.find(zi);for(const i of e){const e=Yi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Si,Li].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Hi)?this:Te.prev(this,Hi)[0]||Te.next(this,Hi)[0]||Te.findOne(Hi,t.delegateTarget.parentNode),o=Yi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}ge.on(document,Mi,Hi,Yi.dataApiKeydownHandler),ge.on(document,Mi,Bi,Yi.dataApiKeydownHandler),ge.on(document,Ni,Yi.clearMenus),ge.on(document,ji,Yi.clearMenus),ge.on(document,Ni,Hi,function(t){t.preventDefault(),Yi.getOrCreateInstance(this).toggle()}),Gt(Yi);const Gi="backdrop",Ji="show",Zi=`mousedown.bs.${Gi}`,tn={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},en={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class nn extends we{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return tn}static get DefaultType(){return en}static get NAME(){return Gi}show(t){if(!this._config.isVisible)return void Jt(t);this._append();const e=this._getElement();this._config.isAnimated&&Kt(e),e.classList.add(Ji),this._emulateAnimation(()=>{Jt(t)})}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ji),this._emulateAnimation(()=>{this.dispose(),Jt(t)})):Jt(t)}dispose(){this._isAppended&&(ge.off(this._element,Zi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=Wt(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),ge.on(t,Zi,()=>{Jt(this._config.clickCallback)}),this._isAppended=!0}_emulateAnimation(t){Zt(t,this._getElement(),this._config.isAnimated)}}const sn=".bs.focustrap",on=`focusin${sn}`,rn=`keydown.tab${sn}`,an="backward",ln={autofocus:!0,trapElement:null},cn={autofocus:"boolean",trapElement:"element"};class hn extends we{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return ln}static get DefaultType(){return cn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),ge.off(document,sn),ge.on(document,on,t=>this._handleFocusin(t)),ge.on(document,rn,t=>this._handleKeydown(t)),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,ge.off(document,sn))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=Te.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===an?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?an:"forward")}}const un=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",dn=".sticky-top",fn="padding-right",pn="margin-right";class mn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,fn,e=>e+t),this._setElementAttributes(un,fn,e=>e+t),this._setElementAttributes(dn,pn,e=>e-t)}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,fn),this._resetElementAttributes(un,fn),this._resetElementAttributes(dn,pn)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)})}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&ye.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,t=>{const i=ye.getDataAttribute(t,e);null!==i?(ye.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)})}_applyManipulationCallback(t,e){if(Bt(t))e(t);else for(const i of Te.find(t,this._element))e(i)}}const gn=".bs.modal",_n=`hide${gn}`,bn=`hidePrevented${gn}`,vn=`hidden${gn}`,yn=`show${gn}`,wn=`shown${gn}`,An=`resize${gn}`,En=`click.dismiss${gn}`,Tn=`mousedown.dismiss${gn}`,Cn=`keydown.dismiss${gn}`,On=`click${gn}.data-api`,xn="modal-open",kn="show",Sn="modal-static",Ln={backdrop:!0,focus:!0,keyboard:!0},$n={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Dn extends Ae{constructor(t,e){super(t,e),this._dialog=Te.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new mn,this._addEventListeners()}static get Default(){return Ln}static get DefaultType(){return $n}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||ge.trigger(this._element,yn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(xn),this._adjustDialog(),this._backdrop.show(()=>this._showElement(t)))}hide(){this._isShown&&!this._isTransitioning&&(ge.trigger(this._element,_n).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(kn),this._queueCallback(()=>this._hideModal(),this._element,this._isAnimated())))}dispose(){ge.off(window,gn),ge.off(this._dialog,gn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new nn({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new hn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=Te.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),Kt(this._element),this._element.classList.add(kn),this._queueCallback(()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,ge.trigger(this._element,wn,{relatedTarget:t})},this._dialog,this._isAnimated())}_addEventListeners(){ge.on(this._element,Cn,t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())}),ge.on(window,An,()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()}),ge.on(this._element,Tn,t=>{ge.one(this._element,En,e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())})})}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide(()=>{document.body.classList.remove(xn),this._resetAdjustments(),this._scrollBar.reset(),ge.trigger(this._element,vn)})}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(ge.trigger(this._element,bn).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Sn)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Sn),this._queueCallback(()=>{this._element.classList.remove(Sn),this._queueCallback(()=>{this._element.style.overflowY=e},this._dialog)},this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=Yt()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=Yt()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each(function(){const i=Dn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}})}}ge.on(document,On,'[data-bs-toggle="modal"]',function(t){const e=Te.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),ge.one(e,yn,t=>{t.defaultPrevented||ge.one(e,vn,()=>{qt(this)&&this.focus()})});const i=Te.findOne(".modal.show");i&&Dn.getInstance(i).hide(),Dn.getOrCreateInstance(e).toggle(this)}),Ce(Dn),Gt(Dn);const In=".bs.offcanvas",Pn=".data-api",Nn=`load${In}${Pn}`,Mn="show",jn="showing",Fn="hiding",Hn=".offcanvas.show",zn=`show${In}`,Bn=`shown${In}`,Wn=`hide${In}`,qn=`hidePrevented${In}`,Rn=`hidden${In}`,Vn=`resize${In}`,Un=`click${In}${Pn}`,Kn=`keydown.dismiss${In}`,Qn={backdrop:!0,keyboard:!0,scroll:!1},Xn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Yn extends Ae{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Qn}static get DefaultType(){return Xn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||ge.trigger(this._element,zn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new mn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(jn),this._queueCallback(()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Mn),this._element.classList.remove(jn),ge.trigger(this._element,Bn,{relatedTarget:t})},this._element,!0))}hide(){this._isShown&&(ge.trigger(this._element,Wn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(Fn),this._backdrop.hide(),this._queueCallback(()=>{this._element.classList.remove(Mn,Fn),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new mn).reset(),ge.trigger(this._element,Rn)},this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new nn({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():ge.trigger(this._element,qn)}:null})}_initializeFocusTrap(){return new hn({trapElement:this._element})}_addEventListeners(){ge.on(this._element,Kn,t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():ge.trigger(this._element,qn))})}static jQueryInterface(t){return this.each(function(){const e=Yn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}})}}ge.on(document,Un,'[data-bs-toggle="offcanvas"]',function(t){const e=Te.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),Rt(this))return;ge.one(e,Rn,()=>{qt(this)&&this.focus()});const i=Te.findOne(Hn);i&&i!==e&&Yn.getInstance(i).hide(),Yn.getOrCreateInstance(e).toggle(this)}),ge.on(window,Nn,()=>{for(const t of Te.find(Hn))Yn.getOrCreateInstance(t).show()}),ge.on(window,Vn,()=>{for(const t of Te.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&Yn.getOrCreateInstance(t).hide()}),Ce(Yn),Gt(Yn);const Gn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Jn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Zn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,ts=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Jn.has(i)||Boolean(Zn.test(t.nodeValue)):e.filter(t=>t instanceof RegExp).some(t=>t.test(i))},es={allowList:Gn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},is={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},ns={entry:"(string|element|function|null)",selector:"(string|element)"};class ss extends we{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return es}static get DefaultType(){return is}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map(t=>this._resolvePossibleFunction(t)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},ns)}_setContent(t,e,i){const n=Te.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?Bt(e)?this._putElementInTemplate(Wt(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)ts(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return Jt(t,[void 0,this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const os=new Set(["sanitize","allowList","sanitizeFn"]),rs="fade",as="show",ls=".tooltip-inner",cs=".modal",hs="hide.bs.modal",us="hover",ds="focus",fs="click",ps={AUTO:"auto",TOP:"top",RIGHT:Yt()?"left":"right",BOTTOM:"bottom",LEFT:Yt()?"right":"left"},ms={allowList:Gn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},gs={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class _s extends Ae{constructor(t,e){if(void 0===Pt)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return ms}static get DefaultType(){return gs}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),ge.off(this._element.closest(cs),hs,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=ge.trigger(this._element,this.constructor.eventName("show")),e=(Vt(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),ge.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(as),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))ge.on(t,"mouseover",Ut);this._queueCallback(()=>{ge.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1},this.tip,this._isAnimated())}hide(){if(this._isShown()&&!ge.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(as),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))ge.off(t,"mouseover",Ut);this._activeTrigger[fs]=!1,this._activeTrigger[ds]=!1,this._activeTrigger[us]=!1,this._isHovered=null,this._queueCallback(()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),ge.trigger(this._element,this.constructor.eventName("hidden")))},this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(rs,as),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(rs),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new ss({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[ls]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(rs)}_isShown(){return this.tip&&this.tip.classList.contains(as)}_createPopper(t){const e=Jt(this._config.placement,[this,t,this._element]),i=ps[e.toUpperCase()];return It(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return Jt(t,[this._element,this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...Jt(this._config.popperConfig,[void 0,e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)ge.on(this._element,this.constructor.eventName("click"),this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger[fs]=!(e._isShown()&&e._activeTrigger[fs]),e.toggle()});else if("manual"!==e){const t=e===us?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===us?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");ge.on(this._element,t,this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?ds:us]=!0,e._enter()}),ge.on(this._element,i,this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?ds:us]=e._element.contains(t.relatedTarget),e._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},ge.on(this._element.closest(cs),hs,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout(()=>{this._isHovered&&this.show()},this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout(()=>{this._isHovered||this.hide()},this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=ye.getDataAttributes(this._element);for(const t of Object.keys(e))os.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:Wt(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each(function(){const e=_s.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}}Gt(_s);const bs=".popover-header",vs=".popover-body",ys={..._s.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},ws={..._s.DefaultType,content:"(null|string|element|function)"};class As extends _s{static get Default(){return ys}static get DefaultType(){return ws}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[bs]:this._getTitle(),[vs]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each(function(){const e=As.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}}Gt(As);const Es=".bs.scrollspy",Ts=`activate${Es}`,Cs=`click${Es}`,Os=`load${Es}.data-api`,xs="active",ks="[href]",Ss=".nav-link",Ls=`${Ss}, .nav-item > ${Ss}, .list-group-item`,$s={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},Ds={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Is extends Ae{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return $s}static get DefaultType(){return Ds}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=Wt(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map(t=>Number.parseFloat(t))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(ge.off(this._config.target,Cs),ge.on(this._config.target,Cs,ks,t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}}))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver(t=>this._observerCallback(t),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=Te.find(ks,this._config.target);for(const e of t){if(!e.hash||Rt(e))continue;const t=Te.findOne(decodeURI(e.hash),this._element);qt(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(xs),this._activateParents(t),ge.trigger(this._element,Ts,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))Te.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(xs);else for(const e of Te.parents(t,".nav, .list-group"))for(const t of Te.prev(e,Ls))t.classList.add(xs)}_clearActiveClass(t){t.classList.remove(xs);const e=Te.find(`${ks}.${xs}`,t);for(const t of e)t.classList.remove(xs)}static jQueryInterface(t){return this.each(function(){const e=Is.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}})}}ge.on(window,Os,()=>{for(const t of Te.find('[data-bs-spy="scroll"]'))Is.getOrCreateInstance(t)}),Gt(Is);const Ps=".bs.tab",Ns=`hide${Ps}`,Ms=`hidden${Ps}`,js=`show${Ps}`,Fs=`shown${Ps}`,Hs=`click${Ps}`,zs=`keydown${Ps}`,Bs=`load${Ps}`,Ws="ArrowLeft",qs="ArrowRight",Rs="ArrowUp",Vs="ArrowDown",Us="Home",Ks="End",Qs="active",Xs="fade",Ys="show",Gs=".dropdown-toggle",Js=`:not(${Gs})`,Zs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',to=`.nav-link${Js}, .list-group-item${Js}, [role="tab"]${Js}, ${Zs}`,eo=`.${Qs}[data-bs-toggle="tab"], .${Qs}[data-bs-toggle="pill"], .${Qs}[data-bs-toggle="list"]`;class io extends Ae{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),ge.on(this._element,zs,t=>this._keydown(t)))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?ge.trigger(e,Ns,{relatedTarget:t}):null;ge.trigger(t,js,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Qs),this._activate(Te.getElementFromSelector(t)),this._queueCallback(()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),ge.trigger(t,Fs,{relatedTarget:e})):t.classList.add(Ys)},t,t.classList.contains(Xs)))}_deactivate(t,e){t&&(t.classList.remove(Qs),t.blur(),this._deactivate(Te.getElementFromSelector(t)),this._queueCallback(()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),ge.trigger(t,Ms,{relatedTarget:e})):t.classList.remove(Ys)},t,t.classList.contains(Xs)))}_keydown(t){if(![Ws,qs,Rs,Vs,Us,Ks].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter(t=>!Rt(t));let i;if([Us,Ks].includes(t.key))i=e[t.key===Us?0:e.length-1];else{const n=[qs,Vs].includes(t.key);i=te(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),io.getOrCreateInstance(i).show())}_getChildren(){return Te.find(to,this._parent)}_getActiveElem(){return this._getChildren().find(t=>this._elemIsActive(t))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=Te.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=Te.findOne(t,i);s&&s.classList.toggle(n,e)};n(Gs,Qs),n(".dropdown-menu",Ys),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Qs)}_getInnerElement(t){return t.matches(to)?t:Te.findOne(to,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each(function(){const e=io.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}})}}ge.on(document,Hs,Zs,function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),Rt(this)||io.getOrCreateInstance(this).show()}),ge.on(window,Bs,()=>{for(const t of Te.find(eo))io.getOrCreateInstance(t)}),Gt(io);const no=".bs.toast",so=`mouseover${no}`,oo=`mouseout${no}`,ro=`focusin${no}`,ao=`focusout${no}`,lo=`hide${no}`,co=`hidden${no}`,ho=`show${no}`,uo=`shown${no}`,fo="hide",po="show",mo="showing",go={animation:"boolean",autohide:"boolean",delay:"number"},_o={animation:!0,autohide:!0,delay:5e3};class bo extends Ae{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return _o}static get DefaultType(){return go}static get NAME(){return"toast"}show(){ge.trigger(this._element,ho).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(fo),Kt(this._element),this._element.classList.add(po,mo),this._queueCallback(()=>{this._element.classList.remove(mo),ge.trigger(this._element,uo),this._maybeScheduleHide()},this._element,this._config.animation))}hide(){this.isShown()&&(ge.trigger(this._element,lo).defaultPrevented||(this._element.classList.add(mo),this._queueCallback(()=>{this._element.classList.add(fo),this._element.classList.remove(mo,po),ge.trigger(this._element,co)},this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(po),super.dispose()}isShown(){return this._element.classList.contains(po)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){ge.on(this._element,so,t=>this._onInteraction(t,!0)),ge.on(this._element,oo,t=>this._onInteraction(t,!1)),ge.on(this._element,ro,t=>this._onInteraction(t,!0)),ge.on(this._element,ao,t=>this._onInteraction(t,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each(function(){const e=bo.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}})}}Ce(bo),Gt(bo);const vo=Object.freeze(Object.defineProperty({__proto__:null,Alert:Se,Button:$e,Carousel:ui,Collapse:Ci,Dropdown:Yi,Modal:Dn,Offcanvas:Yn,Popover:As,ScrollSpy:Is,Tab:io,Toast:bo,Tooltip:_s},Symbol.toStringTag,{value:"Module"}));[].slice.call(document.querySelectorAll('[data-bs-toggle="dropdown"]')).map(function(t){let e={boundary:"viewport"===t.getAttribute("data-bs-boundary")?document.querySelector(".btn"):"clippingParents"};return new Yi(t,e)}),[].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')).map(function(t){let e={delay:{show:50,hide:50},html:"true"===t.getAttribute("data-bs-html")??!1,placement:t.getAttribute("data-bs-placement")??"auto"};return new _s(t,e)}),[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function(t){let e={delay:{show:50,hide:50},html:"true"===t.getAttribute("data-bs-html")??!1,placement:t.getAttribute("data-bs-placement")??"auto"};return new As(t,e)}),[].slice.call(document.querySelectorAll('[data-bs-toggle="switch-icon"]')).map(function(t){t.addEventListener("click",e=>{e.stopPropagation(),t.classList.toggle("active")})}),(()=>{const t=window.location.hash;t&&[].slice.call(document.querySelectorAll('[data-bs-toggle="tab"]')).filter(e=>e.hash===t).map(t=>{new io(t).show()})})(),[].slice.call(document.querySelectorAll('[data-bs-toggle="toast"]')).map(function(t){if(!t.hasAttribute("data-bs-target"))return;const e=new bo(t.getAttribute("data-bs-target"));t.addEventListener("click",()=>{e.show()})});const yo="tblr-",wo=(t,e)=>{const i=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(t);return i?`rgba(${parseInt(i[1],16)}, ${parseInt(i[2],16)}, ${parseInt(i[3],16)}, ${e})`:null},Ao=Object.freeze(Object.defineProperty({__proto__:null,getColor:(t,e=1)=>{const i=getComputedStyle(document.body).getPropertyValue(`--${yo}${t}`).trim();return 1!==e?wo(i,e):i},hexToRgba:wo,prefix:yo},Symbol.toStringTag,{value:"Module"}));t.Alert=Se,t.Button=$e,t.Carousel=ui,t.Collapse=Ci,t.Dropdown=Yi,t.Modal=Dn,t.Offcanvas=Yn,t.Popover=As,t.ScrollSpy=Is,t.Tab=io,t.Toast=bo,t.Tooltip=_s,t.bootstrap=vo,t.tabler=Ao,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})}); +//# sourceMappingURL=tabler.min.js.map \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..b8cea48 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,501 @@ +import { useEffect, useMemo, useRef, useState, type ReactElement, type ReactNode } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import type { EChartsOption } from "echarts"; +import { + IconArrowsMove, + IconBolt, + IconChartBar, + IconChecklist, + IconClockHour4, + IconDatabaseImport, + IconDeviceDesktop, + IconHistory, + IconLanguage, + IconLayoutDashboard, + IconLock, + IconLogin2, + IconLogout, + IconMoon, + IconPlayerPlay, + IconRefresh, + IconSettings, + IconSun, + IconTemperature, + IconX, +} from "./components/common/Icons"; +import { api } from "./api/client"; +import { EChart } from "./components/common/EChart"; +import { labelForMetric, localeForLanguage, normalizeLanguage, t, translateCompareMode, type Language } from "./i18n"; +import { useAnalytics, useDashboardConfig, useHistoricalImport, useRealtimeHistory, useRealtimeSocket } from "./hooks"; +import { formatDateTime, formatDurationShort, formatPercent, formatShortTime, formatValue } from "./lib/format"; +import type { + AnalyticsPayload, + AuthStatus, + AuthUsersPayload, + BucketPoint, + DashboardConfig, + DistributionPayload, + HistoryPayload, + HistoricalStatus, + KioskSettingsPayload, + MetricValue, + SnapshotGroupRow, + SnapshotPayload, +} from "./types"; + +type ThemeMode = "light" | "dark"; +type TabKey = "realtime" | "archive" | "analytics" | "warehouse" | "kiosk" | "settings"; +type ViewMode = "normal" | "kiosk"; +type WidgetId = "hero" | "quickMetrics" | "history" | "status" | "strings" | "production" | "comparison" | "distribution" | "importStatus"; +type BlockTarget = "hero" | "quick"; + +const STORAGE_KEYS = { + theme: "pv-theme-v4", + language: "pv-language-v4", + kioskWidgets: "pv-kiosk-widgets-v4", + viewMode: "pv-view-mode-v4", + blockConfig: "pv-block-config-v4", + liveMetrics: "pv-live-metrics-v4", + archiveMetrics: "pv-archive-metrics-v4", +}; + +const DEFAULT_KIOSK_WIDGETS: WidgetId[] = ["hero", "history", "strings", "status", "production", "comparison", "importStatus"]; +const DEFAULT_BLOCK_CONFIG: Record = { + hero: ["ac_power", "dc_power_total", "energy_today", "energy_total"], + quick: ["energy_today", "energy_yesterday", "energy_total", "dc_power_total", "today_vs_yesterday"], +}; +const DEFAULT_LIVE_METRICS = ["ac_power", "string_1_power", "string_2_power"]; +function getKioskRouteMode(): "public" | "private" | null { + const pathname = window.location.pathname.replace(/\/+$/, "") || "/"; + if (pathname.endsWith("/kiosk/public")) return "public"; + if (pathname.endsWith("/kiosk/private")) return "private"; + const url = new URL(window.location.href); + if (url.searchParams.get("publicKiosk") === "1") return "public"; + if (url.searchParams.get("privateKiosk") === "1") return "private"; + return null; +} +const KIOSK_ROUTE_MODE = getKioskRouteMode(); +const PUBLIC_KIOSK = KIOSK_ROUTE_MODE === "public"; +const PRIVATE_KIOSK_ROUTE = KIOSK_ROUTE_MODE === "private"; + +const widgetOrder: Array<{ id: WidgetId; tab: TabKey; icon: typeof IconLayoutDashboard }> = [ + { id: "hero", tab: "realtime", icon: IconLayoutDashboard }, + { id: "quickMetrics", tab: "realtime", icon: IconChecklist }, + { id: "history", tab: "realtime", icon: IconHistory }, + { id: "status", tab: "realtime", icon: IconBolt }, + { id: "strings", tab: "realtime", icon: IconArrowsMove }, + { id: "production", tab: "analytics", icon: IconChartBar }, + { id: "comparison", tab: "analytics", icon: IconRefresh }, + { id: "distribution", tab: "analytics", icon: IconChartBar }, + { id: "importStatus", tab: "warehouse", icon: IconDatabaseImport }, +]; + +function readStorage(key: string, fallback: T, parser?: (raw: string) => T): T { + try { + const raw = window.localStorage.getItem(key); + if (!raw) return fallback; + return parser ? parser(raw) : (JSON.parse(raw) as T); + } catch { + return fallback; + } +} +function writeStorage(key: string, value: T): void { + try { window.localStorage.setItem(key, typeof value === "string" ? value : JSON.stringify(value)); } catch {} +} +function parseViewModeFromLocation(): ViewMode { + if (KIOSK_ROUTE_MODE) return "kiosk"; + const url = new URL(window.location.href); + return url.searchParams.get("mode") === "kiosk" ? "kiosk" : "normal"; +} +function syncViewModeToLocation(mode: ViewMode): void { + if (KIOSK_ROUTE_MODE) return; + const url = new URL(window.location.href); + if (mode === "kiosk") url.searchParams.set("mode", "kiosk"); else url.searchParams.delete("mode"); + window.history.replaceState({}, "", url.toString()); +} +function iconForMetric(metricId: string) { + if (metricId.includes("temp")) return ; + if (metricId.includes("energy")) return ; + return ; +} +function buildWidgetLabel(language: Language, widgetId: WidgetId): string { + const labels: Record = { + hero: language === "en" ? "Hero metrics" : "Karty hero", + quickMetrics: t(language, "quickMetrics"), + history: t(language, "chartPowerHistory"), + status: t(language, "systemStatus"), + strings: t(language, "strings"), + production: t(language, "chartProduction"), + comparison: t(language, "chartComparison"), + distribution: t(language, "chartDistribution"), + importStatus: language === "en" ? "Data warehouse" : "Hurtownia danych", + }; + return labels[widgetId]; +} +function buildTablerChartTheme(theme: ThemeMode) { + return theme === "dark" + ? { text: "#cbd5e1", grid: "rgba(255,255,255,0.08)", tooltip: "rgba(15, 23, 42, 0.96)", series: ["#4dabf7", "#20c997", "#f59f00", "#e64980", "#9775fa", "#ff922b", "#66d9e8", "#adb5bd", "#94d82d", "#ffa8a8"] } + : { text: "#334155", grid: "rgba(15,23,42,0.12)", tooltip: "rgba(255,255,255,0.98)", series: ["#206bc4", "#2fb344", "#f59f00", "#d63384", "#7950f2", "#fd7e14", "#1098ad", "#868e96", "#74b816", "#fa5252"] }; +} +function buildLiveHistoryOption(history: HistoryPayload | undefined, theme: ThemeMode, language: Language): EChartsOption { + const palette = buildTablerChartTheme(theme); + const series = history?.series ?? []; + return { + color: palette.series, + tooltip: { trigger: "axis", backgroundColor: palette.tooltip, borderColor: palette.grid, textStyle: { color: palette.text } }, + legend: { top: 0, textStyle: { color: palette.text }, itemGap: 16 }, + grid: { left: 12, right: 16, top: 48, bottom: 12, containLabel: true }, + xAxis: { type: "category", boundaryGap: false, axisLabel: { color: palette.text }, axisLine: { lineStyle: { color: palette.grid } }, data: (series[0]?.points ?? []).map((point: any) => formatShortTime(point.timestamp, localeForLanguage(language))) }, + yAxis: { type: "value", axisLabel: { color: palette.text }, splitLine: { lineStyle: { color: palette.grid } } }, + series: series.map((item, index) => ({ name: item.label, type: "line", smooth: true, connectNulls: true, showSymbol: false, lineStyle: { width: index === 0 ? 3 : 2 }, emphasis: { focus: "series" }, data: item.points.map((point: any) => point.value) })), + }; +} +function buildBarOption(points: BucketPoint[], unit: string, theme: ThemeMode, language: Language): EChartsOption { + const palette = buildTablerChartTheme(theme); + return { + color: [palette.series[0]], + tooltip: { trigger: "axis", backgroundColor: palette.tooltip, borderColor: palette.grid, textStyle: { color: palette.text }, valueFormatter: (value) => formatValue(Number(value), unit, 2, localeForLanguage(language)) }, + grid: { left: 12, right: 16, top: 16, bottom: 40, containLabel: true }, + xAxis: { type: "category", axisLabel: { color: palette.text, rotate: points.length > 12 ? 32 : 0 }, axisLine: { lineStyle: { color: palette.grid } }, data: points.map((point) => point.label) }, + yAxis: { type: "value", name: unit, nameTextStyle: { color: palette.text }, axisLabel: { color: palette.text }, splitLine: { lineStyle: { color: palette.grid } } }, + series: [{ type: "bar", barMaxWidth: 24, itemStyle: { borderRadius: [6, 6, 0, 0] }, data: points.map((point) => point.value) }], + }; +} +function buildComparisonOption(data: AnalyticsPayload | undefined, theme: ThemeMode, language: Language): EChartsOption { + const palette = buildTablerChartTheme(theme); + const current = data?.current ?? []; + const comparisonSeries = (data?.comparisons?.length ? data.comparisons : [{ key: data?.compare_mode ?? "comparison", label: t(language, "comparisonPeriod"), points: data?.comparison ?? [] }]).filter((item) => item.points?.length).map((item) => ({ ...item, label: translateCompareMode(language, item.label || item.key) })); + return { + color: palette.series, + tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, backgroundColor: palette.tooltip, borderColor: palette.grid, textStyle: { color: palette.text } }, + legend: { top: 0, textStyle: { color: palette.text } }, + grid: { left: 16, right: 20, top: 42, bottom: 18, containLabel: true }, + xAxis: { type: "category", axisLabel: { color: palette.text, interval: 0, rotate: current.length > 12 ? 35 : 0 }, axisLine: { lineStyle: { color: palette.grid } }, data: current.map((point) => point.label) }, + yAxis: { type: "value", axisLabel: { color: palette.text }, splitLine: { lineStyle: { color: palette.grid } } }, + series: [ + { name: t(language, "currentPeriod"), type: "bar", barMaxWidth: 18, data: current.map((point) => point.value) }, + ...comparisonSeries.map((seriesItem, index) => ({ name: seriesItem.label, type: "bar" as const, barMaxWidth: 18, data: current.map((_, pointIndex) => seriesItem.points[pointIndex]?.value ?? 0), itemStyle: { opacity: index === 0 ? 0.9 : 0.75 } })), + ], + }; +} +function buildPieOption(data: DistributionPayload | undefined, theme: ThemeMode): EChartsOption { + const palette = buildTablerChartTheme(theme); + const slices = [...(data?.slices ?? [])].sort((a, b) => b.value - a.value).slice(0, 12); + return { + color: palette.series, + tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, backgroundColor: palette.tooltip, borderColor: palette.grid, textStyle: { color: palette.text } }, + grid: { left: 16, right: 28, top: 8, bottom: 8, containLabel: true }, + xAxis: { type: "value", axisLabel: { color: palette.text }, splitLine: { lineStyle: { color: palette.grid } } }, + yAxis: { type: "category", axisLabel: { color: palette.text }, data: slices.map((item) => item.label) }, + series: [{ type: "bar", data: slices.map((item) => ({ value: item.value, label: { show: true, position: "right", formatter: `${item.share}%`, color: palette.text } })), barMaxWidth: 22, itemStyle: { borderRadius: [0, 6, 6, 0] } }], + }; +} + +function liveRangeOptions(language: Language) { + return [ + { key: "today", label: language === "en" ? "Today" : "Dziś" }, + { key: "yesterday", label: language === "en" ? "Yesterday" : "Wczoraj" }, + { key: "6h", label: "6h" }, + { key: "12h", label: "12h" }, + { key: "24h", label: "24h" }, + { key: "48h", label: "48h" }, + { key: "7d", label: "7d" }, + ]; +} +function analyticsRangeOptions(language: Language) { + return [ + { key: "today", label: language === "en" ? "Today" : "Dziś" }, + { key: "yesterday", label: language === "en" ? "Yesterday" : "Wczoraj" }, + { key: "7d", label: "7d" }, + { key: "30d", label: "30d" }, + { key: "90d", label: "90d" }, + { key: "365d", label: language === "en" ? "365 days" : "365 dni" }, + ]; +} +function archiveRangeOptions(language: Language) { + return [ + { key: "1d", label: language === "en" ? "1 day" : "1 dzień" }, + { key: "3d", label: "3d" }, + { key: "7d", label: "7d" }, + { key: "14d", label: "14d" }, + { key: "30d", label: "30d" }, + { key: "60d", label: "60d" }, + { key: "custom", label: language === "en" ? "Custom" : "Ręczny" }, + ]; +} +function getInitialTheme(config?: DashboardConfig): ThemeMode { + return readStorage(STORAGE_KEYS.theme, (config?.defaults.theme as ThemeMode) ?? "dark", (raw) => (raw === "light" ? "light" : "dark")); +} +function getInitialLanguage(config?: DashboardConfig): Language { + return normalizeLanguage(readStorage(STORAGE_KEYS.language, config?.defaults.language ?? "pl", (raw) => raw)); +} +function getVisibleWidgets(ids: WidgetId[]): WidgetId[] { const base = ids.filter((id, index) => ids.indexOf(id) === index); return base.length > 0 ? base : DEFAULT_KIOSK_WIDGETS; } +function toWidgetIds(ids: string[]): WidgetId[] { return getVisibleWidgets(ids.filter((id): id is WidgetId => widgetOrder.some((item) => item.id === id as WidgetId))); } +function getMetricCandidates(snapshot: SnapshotPayload, config?: DashboardConfig) { + const fromConfig = (config?.visible_entities ?? []) + .filter((item) => item.kind === "gauge") + .map((item) => ({ metric_id: item.metric_id, label: item.label, unit: item.unit })); + const map = new Map(); + fromConfig.forEach((item) => map.set(item.metric_id, item)); + return [...map.values()]; +} + +export default function App() { + const queryClient = useQueryClient(); + const publicMode = PUBLIC_KIOSK; + const privateKioskRoute = PRIVATE_KIOSK_ROUTE; + const authQuery = useQuery({ queryKey: ["auth-status", publicMode], queryFn: api.getAuthStatus, staleTime: 20_000, retry: false, enabled: !publicMode }); + const authEnabled = publicMode ? false : (authQuery.data?.enabled ?? true); + const authenticated = publicMode ? true : (authQuery.data ? (!authEnabled || authQuery.data.authenticated) : false); + const configQuery = useDashboardConfig(authenticated || authEnabled === false); + const config = configQuery.data; + const privateKioskSettingsQuery = useQuery({ queryKey: ["kiosk-settings", "private"], queryFn: () => api.getKioskSettings("private"), enabled: !publicMode && (authenticated || authEnabled === false), staleTime: 30_000 }); + const publicKioskSettingsQuery = useQuery({ queryKey: ["kiosk-settings", "public"], queryFn: () => api.getKioskSettings("public"), enabled: publicMode || authenticated || authEnabled === false, staleTime: 30_000 }); + + const [theme, setTheme] = useState(() => getInitialTheme(undefined)); + const [language, setLanguage] = useState(() => getInitialLanguage(undefined)); + const [activeTab, setActiveTab] = useState(publicMode ? "kiosk" : "realtime"); + const [realtimeRange, setRealtimeRange] = useState("6h"); + const [analyticsRange, setAnalyticsRange] = useState("30d"); + const [bucket, setBucket] = useState("day"); + const [compare, setCompare] = useState("previous_year"); + const [analyticsStart, setAnalyticsStart] = useState(""); + const [analyticsEnd, setAnalyticsEnd] = useState(""); + const [compareRanges, setCompareRanges] = useState>([{ key: "cmp_1", label: "Porównanie 1", start: "", end: "" }, { key: "cmp_2", label: "Porównanie 2", start: "", end: "" }]); + const [archiveStart, setArchiveStart] = useState(""); + const [archiveEnd, setArchiveEnd] = useState(""); + const [archiveRange, setArchiveRange] = useState("1d"); + const [liveMetrics, setLiveMetrics] = useState(() => readStorage(STORAGE_KEYS.liveMetrics, DEFAULT_LIVE_METRICS)); + const [archiveMetrics, setArchiveMetrics] = useState(() => readStorage(STORAGE_KEYS.archiveMetrics, DEFAULT_LIVE_METRICS)); + const [viewMode, setViewMode] = useState(() => { const fromUrl = parseViewModeFromLocation(); return fromUrl === "kiosk" ? fromUrl : readStorage(STORAGE_KEYS.viewMode, "normal", (raw) => (raw === "kiosk" ? "kiosk" : "normal")); }); + const [kioskWidgets, setKioskWidgets] = useState(() => getVisibleWidgets(readStorage(STORAGE_KEYS.kioskWidgets, DEFAULT_KIOSK_WIDGETS))); + const [kioskEditorMode, setKioskEditorMode] = useState<"private" | "public">("private"); + const [privateKioskDraft, setPrivateKioskDraft] = useState({ mode: "private", widgets: DEFAULT_KIOSK_WIDGETS, realtime_range: "12h", analytics_range: "30d", analytics_bucket: "day", compare_mode: "none" }); + const [publicKioskDraft, setPublicKioskDraft] = useState({ mode: "public", widgets: DEFAULT_KIOSK_WIDGETS, realtime_range: "12h", analytics_range: "30d", analytics_bucket: "day", compare_mode: "none" }); + const [blockConfig, setBlockConfig] = useState>(() => readStorage(STORAGE_KEYS.blockConfig, DEFAULT_BLOCK_CONFIG)); + const [loginForm, setLoginForm] = useState({ username: "", password: "" }); + const [loginError, setLoginError] = useState(null); + const [newUser, setNewUser] = useState({ username: "", display_name: "", password: "", role: "user" }); + const [passwordReset, setPasswordReset] = useState<{ username: string; password: string }>({ username: "", password: "" }); + const [kioskSaveNotice, setKioskSaveNotice] = useState>({ public: null, private: null }); + const initializedRef = useRef(false); + const defaultKioskSerializedRef = useRef>({ + public: JSON.stringify({ mode: "public", widgets: DEFAULT_KIOSK_WIDGETS, realtime_range: "12h", analytics_range: "30d", analytics_bucket: "day", compare_mode: "none" }), + private: JSON.stringify({ mode: "private", widgets: DEFAULT_KIOSK_WIDGETS, realtime_range: "12h", analytics_range: "30d", analytics_bucket: "day", compare_mode: "none" }), + }); + const lastSyncedKioskRef = useRef>({ + public: defaultKioskSerializedRef.current.public, + private: defaultKioskSerializedRef.current.private, + }); + + useEffect(() => { + if (!config || initializedRef.current) return; + initializedRef.current = true; + setActiveTab((config.defaults.tab as TabKey) || (publicMode ? "kiosk" : "realtime")); + setRealtimeRange(config.defaults.realtime_range); + setAnalyticsRange(config.defaults.analytics_range); + setBucket(config.defaults.analytics_bucket); + setTheme((current) => current || ((config.defaults.theme as ThemeMode) ?? "dark")); + setLanguage((current) => current || normalizeLanguage(config.defaults.language)); + }, [config, publicMode]); + useEffect(() => { + if (!privateKioskSettingsQuery.data) return; + const normalized = { ...privateKioskSettingsQuery.data, mode: "private" as const }; + lastSyncedKioskRef.current.private = JSON.stringify(normalized); + applyKioskDraftChange("private", normalized); + }, [privateKioskSettingsQuery.data]); + useEffect(() => { + if (!publicKioskSettingsQuery.data) return; + const normalized = { ...publicKioskSettingsQuery.data, mode: "public" as const }; + lastSyncedKioskRef.current.public = JSON.stringify(normalized); + applyKioskDraftChange("public", normalized); + }, [publicKioskSettingsQuery.data]); + useEffect(() => { document.documentElement.setAttribute("data-bs-theme", theme); document.body.setAttribute("data-bs-theme", theme); writeStorage(STORAGE_KEYS.theme, theme); }, [theme]); + useEffect(() => { writeStorage(STORAGE_KEYS.language, language); }, [language]); + useEffect(() => { syncViewModeToLocation(viewMode); writeStorage(STORAGE_KEYS.viewMode, viewMode); }, [viewMode]); + useEffect(() => { writeStorage(STORAGE_KEYS.kioskWidgets, kioskWidgets); }, [kioskWidgets]); + useEffect(() => { writeStorage(STORAGE_KEYS.blockConfig, blockConfig); }, [blockConfig]); + useEffect(() => { writeStorage(STORAGE_KEYS.liveMetrics, liveMetrics); }, [liveMetrics]); + useEffect(() => { writeStorage(STORAGE_KEYS.archiveMetrics, archiveMetrics); }, [archiveMetrics]); + + const dataEnabled = authenticated || authEnabled === false; + const { snapshot, connected, lastUpdated } = useRealtimeSocket(dataEnabled); + const metricCandidates = useMemo(() => getMetricCandidates(snapshot, config), [snapshot, config]); + useEffect(() => { + if (!metricCandidates.length) return; + const allowed = new Set(metricCandidates.map((item) => item.metric_id)); + setLiveMetrics((current) => { + const filtered = current.filter((item) => allowed.has(item)); + return filtered.length ? filtered : metricCandidates.slice(0, 3).map((item) => item.metric_id); + }); + setArchiveMetrics((current) => { + const filtered = current.filter((item) => allowed.has(item)); + return filtered.length ? filtered : metricCandidates.slice(0, 3).map((item) => item.metric_id); + }); + }, [metricCandidates]); + const liveHistoryMetrics = useMemo(() => liveMetrics.filter((item) => item !== "inverter_temp"), [liveMetrics]); + const effectiveKioskSettings = publicMode ? publicKioskDraft : privateKioskDraft; + const kioskActive = publicMode || privateKioskRoute || viewMode === "kiosk"; + const effectiveKioskWidgets = toWidgetIds(kioskActive ? effectiveKioskSettings.widgets : kioskWidgets); + const effectiveRealtimeRange = kioskActive ? effectiveKioskSettings.realtime_range : realtimeRange; + const effectiveAnalyticsRange = kioskActive ? effectiveKioskSettings.analytics_range : analyticsRange; + const effectiveBucket = kioskActive ? effectiveKioskSettings.analytics_bucket : bucket; + const effectiveCompare = kioskActive ? effectiveKioskSettings.compare_mode : compare; + const historyQuery = useRealtimeHistory(effectiveRealtimeRange, dataEnabled, { metrics: liveHistoryMetrics, publicKiosk: publicMode }); + const sanitizedCompareRanges = compareRanges.filter((item) => item.start && item.end); + const analyticsOptions = analyticsStart && analyticsEnd && !kioskActive ? { start: analyticsStart, end: analyticsEnd, publicKiosk: publicMode, compareRanges: effectiveCompare === "custom_multi" ? sanitizedCompareRanges : undefined } : { publicKiosk: publicMode, compareRanges: effectiveCompare === "custom_multi" ? sanitizedCompareRanges : undefined }; + const analyticsQuery = useAnalytics(analyticsStart && analyticsEnd && !kioskActive ? "custom" : effectiveAnalyticsRange, effectiveBucket, effectiveCompare, dataEnabled, analyticsOptions); + const historical = useHistoricalImport(dataEnabled && !publicMode); + const archiveQuery = useRealtimeHistory(archiveStart && archiveEnd ? "custom" : archiveRange, dataEnabled, { start: archiveStart || undefined, end: archiveEnd || undefined, metrics: archiveMetrics, publicKiosk: publicMode }); + const usersQuery = useQuery({ queryKey: ["auth-users"], queryFn: api.getUsers, enabled: dataEnabled && (authQuery.data?.role === "admin"), staleTime: 15_000 }); + + const loginMutation = useMutation({ mutationFn: () => api.login(loginForm.username, loginForm.password), onSuccess: async () => { setLoginError(null); setLoginForm((value) => ({ ...value, password: "" })); await queryClient.invalidateQueries({ queryKey: ["auth-status"] }); await queryClient.invalidateQueries({ queryKey: ["dashboard-config"] }); }, onError: (error: Error) => setLoginError(parseError(error) || t(language, "loginError")) }); + const logoutMutation = useMutation({ mutationFn: api.logout, onSuccess: async () => { await queryClient.clear(); await queryClient.invalidateQueries({ queryKey: ["auth-status"] }); } }); + const createUserMutation = useMutation({ mutationFn: () => api.createUser(newUser), onSuccess: async () => { setNewUser({ username: "", display_name: "", password: "", role: "user" }); await usersQuery.refetch(); } }); + const resetPasswordMutation = useMutation({ mutationFn: () => api.resetUserPassword(passwordReset.username, passwordReset.password), onSuccess: async () => { setPasswordReset({ username: "", password: "" }); await usersQuery.refetch(); } }); + + const saveKioskSettingsMutation = useMutation({ + mutationFn: (payload: KioskSettingsPayload) => api.saveKioskSettings(payload), + onSuccess: async (saved, payload) => { + const normalized = { ...saved, mode: payload.mode }; + lastSyncedKioskRef.current[payload.mode] = JSON.stringify(normalized); + if (payload.mode === "public") setPublicKioskDraft(normalized); else setPrivateKioskDraft(normalized); + setKioskSaveNotice((current) => ({ ...current, [payload.mode]: language === "en" ? "Saved." : "Zapisano." })); + await queryClient.invalidateQueries({ queryKey: ["kiosk-settings", payload.mode] }); + }, + onError: (error: Error, payload) => { + const message = parseError(error) || (language === "en" ? "Save failed." : "Nie udało się zapisać."); + setKioskSaveNotice((current) => ({ ...current, [payload.mode]: message })); + }, + }); + const applyKioskDraftChange = (mode: "public" | "private", next: KioskSettingsPayload) => { + const normalized: KioskSettingsPayload = { ...next, mode }; + if (mode === "public") setPublicKioskDraft(normalized); else setPrivateKioskDraft(normalized); + setKioskSaveNotice((current) => ({ ...current, [mode]: null })); + }; + + const canPersistKioskSettings = !publicMode && (authEnabled === false || authQuery.data?.role === "admin"); + const privateKioskDirty = JSON.stringify(privateKioskDraft) !== lastSyncedKioskRef.current.private; + const publicKioskDirty = JSON.stringify(publicKioskDraft) !== lastSyncedKioskRef.current.public; + const currentKioskDirty = kioskEditorMode === "public" ? publicKioskDirty : privateKioskDirty; + const resetKioskDraft = (mode: "public" | "private") => { + const serialized = lastSyncedKioskRef.current[mode] || defaultKioskSerializedRef.current[mode]; + const parsed = JSON.parse(serialized) as KioskSettingsPayload; + applyKioskDraftChange(mode, { ...parsed, mode }); + }; + const saveCurrentKioskSettings = () => { + if (!canPersistKioskSettings || saveKioskSettingsMutation.isPending) return; + const payload = kioskEditorMode === "public" ? publicKioskDraft : privateKioskDraft; + setKioskSaveNotice((current) => ({ ...current, [payload.mode]: null })); + saveKioskSettingsMutation.mutate(payload); + }; + + const locale = localeForLanguage(language); + const widgetLabels = useMemo(() => { const map = new Map(); for (const item of widgetOrder) map.set(item.id, buildWidgetLabel(language, item.id)); return map; }, [language]); + const summary = analyticsQuery.production.data?.summary; + const topStatus = snapshot.status ?? []; + const heroCards = snapshot.hero_cards.filter((card) => blockConfig.hero.includes(card.metric_id)); + const quickMetrics = Object.values(snapshot.kpis ?? {}).filter((metric) => blockConfig.quick.includes(metric.metric_id)); + const isAdmin = authQuery.data?.role === "admin"; + const publicKioskUrl = `${window.location.origin}/kiosk/public`; + const privateKioskUrl = `${window.location.origin}/kiosk/private`; + + const allWidgets: Record = { + hero: , + quickMetrics: , + history: , + status: , + strings: , + production: , + comparison: effectiveCompare !== "none" ? : null, + distribution: , + importStatus: , + }; + const renderWidget = (widgetId: WidgetId) => { const content = allWidgets[widgetId]; if (!content) return null; return
{content}
; }; + + if ((!publicMode && authQuery.isLoading) || (authEnabled && !authenticated && loginMutation.isPending)) return ; + if (authEnabled && !authenticated) return loginMutation.mutate()} onThemeToggle={() => setTheme((current) => (current === "dark" ? "light" : "dark"))} onLanguageToggle={() => setLanguage((current) => (current === "pl" ? "en" : "pl"))} loading={loginMutation.isPending} error={loginError} />; + if (configQuery.isLoading || !config) return ; + + const navbar = ( +
+
+
{config.app.site_name}
{t(language, "operatorPanel")}
+
+ {connected ? t(language, "connected") : t(language, "disconnected")} + + + {!publicMode ? : null} + {!publicMode ? : null} +
+
+
+ ); + const menu = ( +
    + } active={activeTab === "realtime"} onClick={() => setActiveTab("realtime")} label={language === "en" ? "Live" : "Live"} /> + } active={activeTab === "archive"} onClick={() => setActiveTab("archive")} label={language === "en" ? "Historical live" : "Dane chwilowe"} /> + } active={activeTab === "analytics"} onClick={() => setActiveTab("analytics")} label={t(language, "analytics")} /> + } active={activeTab === "warehouse"} onClick={() => setActiveTab("warehouse")} label={language === "en" ? "Data warehouse" : "Hurtownia danych"} /> + } active={activeTab === "kiosk"} onClick={() => setActiveTab("kiosk")} label={t(language, "kiosk")} /> + } active={activeTab === "settings"} onClick={() => setActiveTab("settings")} label={t(language, "settings")} /> +
{t(language, "updatedAt")}: {formatDateTime(lastUpdated, locale)}
+ ); + + if (viewMode === "kiosk" || publicMode) { + return
{config.app.site_name}
{t(language, "kioskHint")}
{!publicMode ? : null}
{effectiveKioskWidgets.map((widgetId) => renderWidget(widgetId))}
; + } + + return ( +
{navbar}{menu}
+ {activeTab === "realtime" && <>
{renderWidget("hero")}
{allWidgets.quickMetrics}
{allWidgets.history}
{allWidgets.status}
{allWidgets.strings}
} + + {activeTab === "archive" && <>
{ setArchiveRange(value); applyArchivePreset(value, setArchiveStart, setArchiveEnd); }} options={archiveRangeOptions(language)} />
{ setArchiveRange("custom"); setArchiveStart(e.target.value); }} />
{ setArchiveRange("custom"); setArchiveEnd(e.target.value); }} />
item.metric_id !== "energy_total")} selected={archiveMetrics.filter((item) => item !== "energy_total")} onChange={setArchiveMetrics} />
} + + {activeTab === "analytics" && <>
{ if (value !== "custom") { setAnalyticsRange(value); setAnalyticsStart(""); setAnalyticsEnd(""); } }} options={[...config.capabilities.ranges.filter((item) => !["6h", "24h"].includes(item.key)).map((item) => ({ key: item.key, label: item.label })), { key: "custom", label: language === "en" ? "Custom" : "Ręczny" }]} /> ({ key: item.key, label: translateBucket(language, item.key) }))} />
{analyticsStart || analyticsEnd || (analyticsRange === "custom") || compare === "custom_multi" ?
setAnalyticsStart(e.target.value)} />
setAnalyticsEnd(e.target.value)} />
{compare === "custom_multi" ?
{language === "en" ? "Comparison ranges" : "Zakresy porównawcze"}
{compareRanges.map((item, index) =>
setCompareRanges(compareRanges.map((current) => current.key === item.key ? { ...current, label: e.target.value } : current))} />
setCompareRanges(compareRanges.map((current) => current.key === item.key ? { ...current, start: e.target.value } : current))} />
setCompareRanges(compareRanges.map((current) => current.key === item.key ? { ...current, end: e.target.value } : current))} />
)}
: null}
: null}
item.key === compare)?.label ?? compare} />
{allWidgets.production}
{allWidgets.distribution}
{compare !== "none" ?
{allWidgets.comparison}
: null}
} + + {activeTab === "warehouse" && <>
historical.start.mutate(payload)} onSyncNow={() => historical.syncNow.mutate()} onCancel={() => historical.cancel.mutate()} />
} + + {activeTab === "kiosk" && <>
applyKioskDraftChange(kioskEditorMode, value)} onModeChange={setKioskEditorMode} selectedMode={kioskEditorMode} labels={widgetLabels} buckets={config.capabilities.buckets} compareModes={config.capabilities.comparison_modes} saving={saveKioskSettingsMutation.isPending && saveKioskSettingsMutation.variables?.mode === kioskEditorMode} dirty={currentKioskDirty} canSave={canPersistKioskSettings} saveNotice={kioskSaveNotice[kioskEditorMode]} onSave={saveCurrentKioskSettings} onReset={() => resetKioskDraft(kioskEditorMode)} />
} + + {activeTab === "settings" && <>
item.metric_id !== "energy_total")} selected={liveMetrics.filter((item) => item !== "energy_total")} onChange={setLiveMetrics} />
{isAdmin ?
createUserMutation.mutate()} passwordReset={passwordReset} onPasswordResetChange={setPasswordReset} onResetPassword={() => resetPasswordMutation.mutate()} />
: null}
} +
+ ); +} + +function parseError(error: Error): string | null { const match = error.message.match(/"detail"\s*:\s*"([^"]+)"/); return match?.[1] ?? error.message; } +function requestFullscreen() { const element = document.documentElement as HTMLElement & { webkitRequestFullscreen?: () => Promise | void; msRequestFullscreen?: () => Promise | void; }; if (element.requestFullscreen) { void element.requestFullscreen(); return; } if (element.webkitRequestFullscreen) { void element.webkitRequestFullscreen(); return; } if (element.msRequestFullscreen) void element.msRequestFullscreen(); } +function translateBucket(language: Language, key: string): string { const map: Record = { day: { pl: "Dzień", en: "Day" }, week: { pl: "Tydzień", en: "Week" }, month: { pl: "Miesiąc", en: "Month" }, year: { pl: "Rok", en: "Year" } }; return map[key]?.[language] ?? key; } +function comparisonOptions(language: Language) { return [{ key: "none", label: translateCompareMode(language, "none") }, { key: "previous_period", label: translateCompareMode(language, "previous_period") }, { key: "previous_year", label: translateCompareMode(language, "previous_year") }, { key: "previous_year_2", label: translateCompareMode(language, "previous_year_2") }, { key: "previous_year_3", label: translateCompareMode(language, "previous_year_3") }, { key: "previous_month_12", label: translateCompareMode(language, "previous_month_12") }, { key: "previous_month_24", label: translateCompareMode(language, "previous_month_24") }, { key: "custom_multi", label: translateCompareMode(language, "custom_multi") }]; } + +function applyArchivePreset(rangeKey: string, setStart: (value: string) => void, setEnd: (value: string) => void) { + if (rangeKey !== "custom") { + setStart(""); + setEnd(""); + } +} +function LoadingScreen({ language }: { language: Language }) { return
{t(language, "loading")}…
; } +function LoginPage({ language, theme, form, onChange, onSubmit, onThemeToggle, onLanguageToggle, loading, error }: { language: Language; theme: ThemeMode; form: { username: string; password: string }; onChange: (value: { username: string; password: string }) => void; onSubmit: () => void; onThemeToggle: () => void; onLanguageToggle: () => void; loading: boolean; error: string | null; }) { return

{t(language, "loginTitle")}

{t(language, "loginSubtitle")}
onChange({ ...form, username: event.target.value })} autoComplete="username" />
onChange({ ...form, password: event.target.value })} autoComplete="current-password" onKeyDown={(event) => event.key === "Enter" && onSubmit()} />
{error ?
{error}
: null}
; } +function NavItem({ icon, active, onClick, label }: { icon: ReactElement; active: boolean; onClick: () => void; label: string }) { return
  • ; } +function PageHeader({ title, subtitle, children }: { title: string; subtitle: string; children?: ReactNode }) { return
    PV Insight

    {title}

    {subtitle}
    {children ?
    {children}
    : null}
    ; } +function SegmentedSelect({ label, value, onChange, options }: { label: string; value: string; onChange: (value: string) => void; options: Array<{ key: string; label: string }> }) { return
    {label}
    {options.map((option) => )}
    ; } +function HeroCards({ cards, locale, language }: { cards: SnapshotPayload["hero_cards"]; locale: string; language: Language }) { return
    {cards.map((card) =>
    {iconForMetric(card.metric_id)}{card.unit || "live"}
    {labelForMetric(language, card.metric_id, card.label)}
    {formatValue(card.value, card.unit, card.unit === "kWh" ? 2 : 2, locale)}
    {card.subtitle}
    )}
    ; } +function QuickMetrics({ metrics, locale, language }: { metrics: MetricValue[]; locale: string; language: Language }) { return

    {t(language, "quickMetrics")}

    {metrics.map((metric) =>
    {iconForMetric(metric.metric_id)}
    {labelForMetric(language, metric.metric_id, metric.label)}
    {metric.unit || t(language, "status")}
    {formatValue(metric.value, metric.unit, 2, locale)}
    )}
    ; } +function LiveHistoryPanel({ data, language, theme, title, subtitle }: { data?: HistoryPayload; language: Language; theme: ThemeMode; title: string; subtitle: string }) { return

    {title}

    {subtitle}
    ; } +function StatusPanel({ metrics, locale, language }: { metrics: MetricValue[]; locale: string; language: Language }) { return

    {t(language, "systemStatus")}

    {metrics.map((metric) =>
    {labelForMetric(language, metric.metric_id, metric.label)}
    {metric.unit || t(language, "status")}
    {formatValue(metric.value, metric.unit, 2, locale)}
    )}
    ; } +function StringsPanel({ rows, locale, language }: { rows: SnapshotGroupRow[]; locale: string; language: Language }) { return

    {t(language, "strings")}

    {rows.length === 0 ?
    {t(language, "noDataDescription")}
    : rows.map((row) =>
    {row.label}
    DC
    {Object.values(row.values).map((metric) =>
    {labelForMetric(language, metric.metric_id, metric.label)}{formatValue(metric.value, metric.unit, 2, locale)}
    )}
    )}
    ; } +function SummaryCards({ summary, language, locale, compareLabel }: { summary?: AnalyticsPayload["summary"]; language: Language; locale: string; compareLabel: string }) { const items = [{ key: t(language, "summaryTotal"), value: formatValue(summary?.total, summary?.unit ?? "kWh", 2, locale), badge: compareLabel }, { key: t(language, "summaryAverage"), value: formatValue(summary?.average_bucket, summary?.unit ?? "kWh", 2, locale) }, { key: t(language, "summaryBest"), value: summary ? `${summary.best_bucket_label} · ${formatValue(summary.best_bucket_value, summary.unit, 2, locale)}` : "--" }, { key: t(language, "summaryCo2"), value: formatValue(summary?.co2_saved_kg, "kg", 1, locale) }]; return
    {items.map((item) =>
    {item.key}
    {item.value}
    {item.badge ?
    {item.badge}
    : null}
    )}
    ; } +function ProductionPanel({ data, language, theme }: { data?: AnalyticsPayload; language: Language; theme: ThemeMode }) { return

    {t(language, "chartProduction")}

    {t(language, "chartProductionSubtitle")}
    ; } +function ComparisonPanel({ data, language, theme }: { data?: AnalyticsPayload; language: Language; theme: ThemeMode }) { return

    {t(language, "chartComparison")}

    ; } +function DistributionPanel({ data, language, theme, locale }: { data?: DistributionPayload; language: Language; theme: ThemeMode; locale: string }) { return

    {t(language, "chartDistribution")}

    {formatValue(data?.total, data?.unit ?? "kWh", 2, locale)}
    ; } +function HistoricalPanel({ status, language, locale, compact = false }: { status?: HistoricalStatus; language: Language; locale: string; compact?: boolean }) { if (!status) return
    {t(language, "noDataDescription")}
    ; return

    {language === "en" ? "Data warehouse" : "Hurtownia danych"}

    {t(language, "importArchiveSubtitle")}
    {!compact ? <>
    {t(language, "activeChunk")}{status.active_chunk_index}/{status.total_chunks}
    {status.recent_chunks.map((chunk) => )}
    {t(language, "recentChunks")}{t(language, "status")}kWh
    #{chunk.chunk_index}
    {chunk.start_date} → {chunk.end_date}
    {chunk.state}{formatValue(chunk.energy_kwh, "kWh", 2, locale)}
    {status.recent_events.map((event, index) =>
    {event.title}
    {event.message}
    {formatShortTime(event.timestamp, locale)}
    )}
    : null}
    ; } +function StatusStat({ label, value }: { label: string; value: string }) { return
    {label}
    {value}
    ; } +function ImportControls({ status, language, onStart, onSyncNow, onCancel }: { status?: HistoricalStatus; language: Language; onStart: (payload: { start_date?: string; end_date?: string; chunk_days?: number; force?: boolean }) => void; onSyncNow: () => void; onCancel: () => void; }) { const [startDate, setStartDate] = useState(status?.available_start_date ?? ""); const [endDate, setEndDate] = useState(status?.available_end_date ?? ""); const [chunkDays, setChunkDays] = useState(String(status?.default_chunk_days ?? 7)); useEffect(() => { if (!status) return; setStartDate((current) => current || status.available_start_date || ""); setEndDate((current) => current || status.available_end_date || ""); setChunkDays((current) => current || String(status.default_chunk_days || 7)); }, [status]); return

    {language === "en" ? "Import controls" : "Sterowanie importem"}

    setStartDate(event.target.value)} />
    setEndDate(event.target.value)} />
    setChunkDays(event.target.value)} />
    ; } +function KioskLayoutPanel({ language, widgets, onChange, labels }: { language: Language; widgets: WidgetId[]; onChange: (value: WidgetId[]) => void; labels: Map; }) { const available = widgetOrder.map((item) => item.id); const selected = widgets; const unselected = available.filter((item) => !selected.includes(item)); const move = (id: WidgetId, direction: -1 | 1) => { const index = selected.indexOf(id); if (index === -1) return; const target = index + direction; if (target < 0 || target >= selected.length) return; const next = [...selected]; [next[index], next[target]] = [next[target], next[index]]; onChange(next); }; const toggle = (id: WidgetId) => { if (selected.includes(id)) { const next = selected.filter((item) => item !== id); onChange(next.length ? next : selected); return; } onChange([...selected, id]); }; return

    {t(language, "kioskLayout")}

    {t(language, "kioskLayoutSubtitle")}
    {t(language, "saveLayout")}
    {t(language, "selected")}
    {selected.map((id) =>
    {labels.get(id)}
    )}
    {t(language, "available")}
    {unselected.map((id) => )}
    ; } +function KioskSettingsEditorPanel({ language, value, onChange, onSave, onReset, selectedMode, onModeChange, labels, buckets, compareModes, saving, dirty, canSave, saveNotice }: { language: Language; value: KioskSettingsPayload; onChange: (value: KioskSettingsPayload) => void; onSave: () => void; onReset: () => void; selectedMode: "public" | "private"; onModeChange: (value: "public" | "private") => void; labels: Map; buckets: Array<{ key: string; label: string }>; compareModes: string[]; saving: boolean; dirty: boolean; canSave: boolean; saveNotice: string | null; }) { const widgets = toWidgetIds(value.widgets); return

    {language === "en" ? "Kiosk settings" : "Ustawienia kiosku"}

    {dirty ? (language === "en" ? "You have local changes." : "Masz lokalne zmiany.") : (language === "en" ? "No unsaved changes." : "Brak niezapisanych zmian.")}{saveNotice ? {saveNotice} : null}
    onChange({ ...value, widgets: widgetsValue })} labels={labels} />
    ; } +function KioskLinkPanel({ language, publicKioskUrl, privateKioskUrl, publicSettings, privateSettings }: { language: Language; publicKioskUrl: string; privateKioskUrl: string; publicSettings: KioskSettingsPayload; privateSettings: KioskSettingsPayload }) { const [copied, setCopied] = useState<"public" | "private" | null>(null); const copy = async (value: string, mode: "public" | "private") => { await navigator.clipboard.writeText(value); setCopied(mode); window.setTimeout(() => setCopied(null), 1500); }; return

    {language === "en" ? "Kiosk links" : "Linki kiosku"}

    {language === "en" ? "Public kiosk" : "Kiosk publiczny"}
    {language === "en" ? "Read-only access without login." : "Podgląd bez logowania, tylko odczyt."}
    {language === "en" ? "Ranges:" : "Zakresy:"} live {publicSettings.realtime_range}, analytics {publicSettings.analytics_range}
    {language === "en" ? "Private kiosk" : "Kiosk prywatny"}
    {language === "en" ? "Requires login and uses private kiosk settings." : "Wymaga logowania i używa prywatnych ustawień kiosku."}
    {language === "en" ? "Ranges:" : "Zakresy:"} live {privateSettings.realtime_range}, analytics {privateSettings.analytics_range}
    ; } +function AppearanceSecurityPanel({ language, theme, setTheme, viewMode, setViewMode, authEnabled, userName }: { language: Language; theme: ThemeMode; setTheme: (value: ThemeMode) => void; viewMode: ViewMode; setViewMode: (value: ViewMode) => void; authEnabled: boolean; userName: string; }) { return

    {t(language, "theme")}

    {t(language, "viewMode")}

    {t(language, "security")}

    {authEnabled ? t(language, "authEnabled") : t(language, "authDisabled")}
    {language === "en" ? "Admin user management is available below." : "Zarządzanie użytkownikami admina jest dostępne niżej."}
    {userName ?
    {userName}
    : null}
    ; } +function MetricSelectorCard({ language, title, items, selected, onChange }: { language: Language; title: string; items: Array<{ metric_id: string; label: string; unit: string }>; selected: string[]; onChange: (value: string[]) => void; }) { const toggle = (metricId: string) => onChange(selected.includes(metricId) ? selected.filter((item) => item !== metricId) : [...selected, metricId]); return

    {title}

    {items.map((item) => )}
    ; } +function LiveChartMetricsPanel({ language, items, selected, onChange }: { language: Language; items: Array<{ metric_id: string; label: string; unit: string }>; selected: string[]; onChange: (value: string[]) => void; }) { return ; } +function BlockVisibilityPanel({ language, items, config, onChange }: { language: Language; items: Array<{ metric_id: string; label: string; unit: string }>; config: Record; onChange: (value: Record) => void; }) { const toggle = (target: BlockTarget, metricId: string) => { const selected = config[target]; onChange({ ...config, [target]: selected.includes(metricId) ? selected.filter((item) => item !== metricId) : [...selected, metricId] }); }; return

    {language === "en" ? "Metric visibility in blocks" : "Widoczność metryk w blokach"}

    Hero metrics
    {items.map((item) => )}
    Quick metrics
    {items.map((item) => )}
    ; } +function AdminUsersPanel({ language, users, newUser, onNewUserChange, onCreate, passwordReset, onPasswordResetChange, onResetPassword }: { language: Language; users: AuthUsersPayload["items"]; newUser: { username: string; display_name: string; password: string; role: string }; onNewUserChange: (value: { username: string; display_name: string; password: string; role: string }) => void; onCreate: () => void; passwordReset: { username: string; password: string }; onPasswordResetChange: (value: { username: string; password: string }) => void; onResetPassword: () => void; }) { return

    {language === "en" ? "Admin user management" : "Zarządzanie użytkownikami"}

    {language === "en" ? "Create user" : "Dodaj użytkownika"}
    onNewUserChange({ ...newUser, username: e.target.value })} />
    onNewUserChange({ ...newUser, display_name: e.target.value })} />
    onNewUserChange({ ...newUser, password: e.target.value })} />
    {language === "en" ? "Reset password" : "Zmiana hasła"}
    onPasswordResetChange({ ...passwordReset, password: e.target.value })} />
    {users.map((user) => )}
    {language === "en" ? "Username" : "Login"}{language === "en" ? "Display name" : "Nazwa"}Role{language === "en" ? "Updated" : "Aktualizacja"}
    {user.username}{user.display_name}{user.role}{formatDateTime(user.updated_at, language === "en" ? "en-GB" : "pl-PL")}
    ; } diff --git a/frontend/src/App.tsx.bak b/frontend/src/App.tsx.bak new file mode 100644 index 0000000..08e1770 --- /dev/null +++ b/frontend/src/App.tsx.bak @@ -0,0 +1,433 @@ +import { useEffect, useMemo, useRef, useState, type ReactElement, type ReactNode } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import type { EChartsOption } from "echarts"; +import { + IconArrowsMove, + IconBolt, + IconChartBar, + IconChecklist, + IconClockHour4, + IconDatabaseImport, + IconDeviceDesktop, + IconHistory, + IconLanguage, + IconLayoutDashboard, + IconLock, + IconLogin2, + IconLogout, + IconMoon, + IconPlayerPlay, + IconRefresh, + IconSettings, + IconSun, + IconTemperature, + IconX, +} from "./components/common/Icons"; +import { api } from "./api/client"; +import { EChart } from "./components/common/EChart"; +import { labelForMetric, localeForLanguage, normalizeLanguage, t, translateCompareMode, type Language } from "./i18n"; +import { useAnalytics, useDashboardConfig, useHistoricalImport, useRealtimeHistory, useRealtimeSocket } from "./hooks"; +import { formatDateTime, formatDurationShort, formatPercent, formatShortTime, formatValue } from "./lib/format"; +import type { + AnalyticsPayload, + AuthStatus, + AuthUsersPayload, + BucketPoint, + DashboardConfig, + DistributionPayload, + HistoryPayload, + HistoricalStatus, + KioskSettingsPayload, + MetricValue, + SnapshotGroupRow, + SnapshotPayload, +} from "./types"; + +type ThemeMode = "light" | "dark"; +type TabKey = "realtime" | "archive" | "analytics" | "warehouse" | "kiosk" | "settings"; +type ViewMode = "normal" | "kiosk"; +type WidgetId = "hero" | "quickMetrics" | "history" | "status" | "strings" | "production" | "comparison" | "distribution" | "importStatus"; +type BlockTarget = "hero" | "quick"; + +const STORAGE_KEYS = { + theme: "pv-theme-v4", + language: "pv-language-v4", + kioskWidgets: "pv-kiosk-widgets-v4", + viewMode: "pv-view-mode-v4", + blockConfig: "pv-block-config-v4", + liveMetrics: "pv-live-metrics-v4", + archiveMetrics: "pv-archive-metrics-v4", +}; + +const DEFAULT_KIOSK_WIDGETS: WidgetId[] = ["hero", "history", "strings", "status", "production", "comparison", "importStatus"]; +const DEFAULT_BLOCK_CONFIG: Record = { + hero: ["ac_power", "dc_power_total", "energy_today", "energy_total"], + quick: ["energy_today", "energy_yesterday", "energy_total", "dc_power_total", "today_vs_yesterday"], +}; +const DEFAULT_LIVE_METRICS = ["ac_power", "string_1_power", "string_2_power"]; +const PUBLIC_KIOSK = new URL(window.location.href).searchParams.get("publicKiosk") === "1"; + +const widgetOrder: Array<{ id: WidgetId; tab: TabKey; icon: typeof IconLayoutDashboard }> = [ + { id: "hero", tab: "realtime", icon: IconLayoutDashboard }, + { id: "quickMetrics", tab: "realtime", icon: IconChecklist }, + { id: "history", tab: "realtime", icon: IconHistory }, + { id: "status", tab: "realtime", icon: IconBolt }, + { id: "strings", tab: "realtime", icon: IconArrowsMove }, + { id: "production", tab: "analytics", icon: IconChartBar }, + { id: "comparison", tab: "analytics", icon: IconRefresh }, + { id: "distribution", tab: "analytics", icon: IconChartBar }, + { id: "importStatus", tab: "warehouse", icon: IconDatabaseImport }, +]; + +function readStorage(key: string, fallback: T, parser?: (raw: string) => T): T { + try { + const raw = window.localStorage.getItem(key); + if (!raw) return fallback; + return parser ? parser(raw) : (JSON.parse(raw) as T); + } catch { + return fallback; + } +} +function writeStorage(key: string, value: T): void { + try { window.localStorage.setItem(key, typeof value === "string" ? value : JSON.stringify(value)); } catch {} +} +function parseViewModeFromLocation(): ViewMode { + const url = new URL(window.location.href); + return url.searchParams.get("mode") === "kiosk" ? "kiosk" : "normal"; +} +function syncViewModeToLocation(mode: ViewMode): void { + const url = new URL(window.location.href); + if (mode === "kiosk") url.searchParams.set("mode", "kiosk"); else url.searchParams.delete("mode"); + window.history.replaceState({}, "", url.toString()); +} +function iconForMetric(metricId: string) { + if (metricId.includes("temp")) return ; + if (metricId.includes("energy")) return ; + return ; +} +function buildWidgetLabel(language: Language, widgetId: WidgetId): string { + const labels: Record = { + hero: language === "en" ? "Hero metrics" : "Karty hero", + quickMetrics: t(language, "quickMetrics"), + history: t(language, "chartPowerHistory"), + status: t(language, "systemStatus"), + strings: t(language, "strings"), + production: t(language, "chartProduction"), + comparison: t(language, "chartComparison"), + distribution: t(language, "chartDistribution"), + importStatus: language === "en" ? "Data warehouse" : "Hurtownia danych", + }; + return labels[widgetId]; +} +function buildTablerChartTheme(theme: ThemeMode) { + return theme === "dark" + ? { text: "#cbd5e1", grid: "rgba(255,255,255,0.08)", tooltip: "rgba(15, 23, 42, 0.96)", series: ["#4dabf7", "#20c997", "#f59f00", "#e64980", "#9775fa", "#ff922b", "#66d9e8", "#adb5bd", "#94d82d", "#ffa8a8"] } + : { text: "#334155", grid: "rgba(15,23,42,0.12)", tooltip: "rgba(255,255,255,0.98)", series: ["#206bc4", "#2fb344", "#f59f00", "#d63384", "#7950f2", "#fd7e14", "#1098ad", "#868e96", "#74b816", "#fa5252"] }; +} +function buildLiveHistoryOption(history: HistoryPayload | undefined, theme: ThemeMode, language: Language): EChartsOption { + const palette = buildTablerChartTheme(theme); + const series = history?.series ?? []; + return { + color: palette.series, + tooltip: { trigger: "axis", backgroundColor: palette.tooltip, borderColor: palette.grid, textStyle: { color: palette.text } }, + legend: { top: 0, textStyle: { color: palette.text }, itemGap: 16 }, + grid: { left: 12, right: 16, top: 48, bottom: 12, containLabel: true }, + xAxis: { type: "category", boundaryGap: false, axisLabel: { color: palette.text }, axisLine: { lineStyle: { color: palette.grid } }, data: (series[0]?.points ?? []).map((point: any) => formatShortTime(point.timestamp, localeForLanguage(language))) }, + yAxis: { type: "value", axisLabel: { color: palette.text }, splitLine: { lineStyle: { color: palette.grid } } }, + series: series.map((item, index) => ({ name: item.label, type: "line", smooth: true, connectNulls: true, showSymbol: false, lineStyle: { width: index === 0 ? 3 : 2 }, emphasis: { focus: "series" }, data: item.points.map((point: any) => point.value) })), + }; +} +function buildBarOption(points: BucketPoint[], unit: string, theme: ThemeMode, language: Language): EChartsOption { + const palette = buildTablerChartTheme(theme); + return { + color: [palette.series[0]], + tooltip: { trigger: "axis", backgroundColor: palette.tooltip, borderColor: palette.grid, textStyle: { color: palette.text }, valueFormatter: (value) => formatValue(Number(value), unit, 2, localeForLanguage(language)) }, + grid: { left: 12, right: 16, top: 16, bottom: 40, containLabel: true }, + xAxis: { type: "category", axisLabel: { color: palette.text, rotate: points.length > 12 ? 32 : 0 }, axisLine: { lineStyle: { color: palette.grid } }, data: points.map((point) => point.label) }, + yAxis: { type: "value", name: unit, nameTextStyle: { color: palette.text }, axisLabel: { color: palette.text }, splitLine: { lineStyle: { color: palette.grid } } }, + series: [{ type: "bar", barMaxWidth: 24, itemStyle: { borderRadius: [6, 6, 0, 0] }, data: points.map((point) => point.value) }], + }; +} +function buildComparisonOption(data: AnalyticsPayload | undefined, theme: ThemeMode, language: Language): EChartsOption { + const palette = buildTablerChartTheme(theme); + const current = data?.current ?? []; + const comparisonSeries = (data?.comparisons?.length ? data.comparisons : [{ key: data?.compare_mode ?? "comparison", label: t(language, "comparisonPeriod"), points: data?.comparison ?? [] }]).filter((item) => item.points?.length).map((item) => ({ ...item, label: translateCompareMode(language, item.label || item.key) })); + return { + color: palette.series, + tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, backgroundColor: palette.tooltip, borderColor: palette.grid, textStyle: { color: palette.text } }, + legend: { top: 0, textStyle: { color: palette.text } }, + grid: { left: 16, right: 20, top: 42, bottom: 18, containLabel: true }, + xAxis: { type: "category", axisLabel: { color: palette.text, interval: 0, rotate: current.length > 12 ? 35 : 0 }, axisLine: { lineStyle: { color: palette.grid } }, data: current.map((point) => point.label) }, + yAxis: { type: "value", axisLabel: { color: palette.text }, splitLine: { lineStyle: { color: palette.grid } } }, + series: [ + { name: t(language, "currentPeriod"), type: "bar", barMaxWidth: 18, data: current.map((point) => point.value) }, + ...comparisonSeries.map((seriesItem, index) => ({ name: seriesItem.label, type: "bar" as const, barMaxWidth: 18, data: current.map((_, pointIndex) => seriesItem.points[pointIndex]?.value ?? 0), itemStyle: { opacity: index === 0 ? 0.9 : 0.75 } })), + ], + }; +} +function buildPieOption(data: DistributionPayload | undefined, theme: ThemeMode): EChartsOption { + const palette = buildTablerChartTheme(theme); + const slices = [...(data?.slices ?? [])].sort((a, b) => b.value - a.value).slice(0, 12); + return { + color: palette.series, + tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, backgroundColor: palette.tooltip, borderColor: palette.grid, textStyle: { color: palette.text } }, + grid: { left: 16, right: 28, top: 8, bottom: 8, containLabel: true }, + xAxis: { type: "value", axisLabel: { color: palette.text }, splitLine: { lineStyle: { color: palette.grid } } }, + yAxis: { type: "category", axisLabel: { color: palette.text }, data: slices.map((item) => item.label) }, + series: [{ type: "bar", data: slices.map((item) => ({ value: item.value, label: { show: true, position: "right", formatter: `${item.share}%`, color: palette.text } })), barMaxWidth: 22, itemStyle: { borderRadius: [0, 6, 6, 0] } }], + }; +} + +function liveRangeOptions(language: Language) { + return [ + { key: "today", label: language === "en" ? "Today" : "Dziś" }, + { key: "yesterday", label: language === "en" ? "Yesterday" : "Wczoraj" }, + { key: "6h", label: "6h" }, + { key: "12h", label: "12h" }, + { key: "24h", label: "24h" }, + { key: "48h", label: "48h" }, + { key: "7d", label: "7d" }, + ]; +} +function analyticsRangeOptions(language: Language) { + return [ + { key: "today", label: language === "en" ? "Today" : "Dziś" }, + { key: "yesterday", label: language === "en" ? "Yesterday" : "Wczoraj" }, + { key: "7d", label: "7d" }, + { key: "30d", label: "30d" }, + { key: "90d", label: "90d" }, + { key: "365d", label: language === "en" ? "365 days" : "365 dni" }, + ]; +} +function archiveRangeOptions(language: Language) { + return [ + { key: "1d", label: language === "en" ? "1 day" : "1 dzień" }, + { key: "3d", label: "3d" }, + { key: "7d", label: "7d" }, + { key: "14d", label: "14d" }, + { key: "30d", label: "30d" }, + { key: "60d", label: "60d" }, + { key: "custom", label: language === "en" ? "Custom" : "Ręczny" }, + ]; +} +function getInitialTheme(config?: DashboardConfig): ThemeMode { + return readStorage(STORAGE_KEYS.theme, (config?.defaults.theme as ThemeMode) ?? "dark", (raw) => (raw === "light" ? "light" : "dark")); +} +function getInitialLanguage(config?: DashboardConfig): Language { + return normalizeLanguage(readStorage(STORAGE_KEYS.language, config?.defaults.language ?? "pl", (raw) => raw)); +} +function getVisibleWidgets(ids: WidgetId[]): WidgetId[] { const base = ids.filter((id, index) => ids.indexOf(id) === index); return base.length > 0 ? base : DEFAULT_KIOSK_WIDGETS; } +function toWidgetIds(ids: string[]): WidgetId[] { return getVisibleWidgets(ids.filter((id): id is WidgetId => widgetOrder.some((item) => item.id === id as WidgetId))); } +function getMetricCandidates(snapshot: SnapshotPayload, config?: DashboardConfig) { + const fromConfig = (config?.visible_entities ?? []) + .filter((item) => item.kind === "gauge") + .map((item) => ({ metric_id: item.metric_id, label: item.label, unit: item.unit })); + const map = new Map(); + fromConfig.forEach((item) => map.set(item.metric_id, item)); + return [...map.values()]; +} + +export default function App() { + const queryClient = useQueryClient(); + const publicMode = PUBLIC_KIOSK; + const authQuery = useQuery({ queryKey: ["auth-status", publicMode], queryFn: api.getAuthStatus, staleTime: 20_000, retry: false, enabled: !publicMode }); + const authEnabled = publicMode ? false : (authQuery.data?.enabled ?? true); + const authenticated = publicMode ? true : (authQuery.data ? (!authEnabled || authQuery.data.authenticated) : false); + const configQuery = useDashboardConfig(authenticated || authEnabled === false); + const config = configQuery.data; + const privateKioskSettingsQuery = useQuery({ queryKey: ["kiosk-settings", "private"], queryFn: () => api.getKioskSettings("private"), enabled: authenticated || authEnabled === false, staleTime: 30_000 }); + const publicKioskSettingsQuery = useQuery({ queryKey: ["kiosk-settings", "public"], queryFn: () => api.getKioskSettings("public"), enabled: authenticated || authEnabled === false || publicMode, staleTime: 30_000 }); + + const [theme, setTheme] = useState(() => getInitialTheme(undefined)); + const [language, setLanguage] = useState(() => getInitialLanguage(undefined)); + const [activeTab, setActiveTab] = useState(publicMode ? "kiosk" : "realtime"); + const [realtimeRange, setRealtimeRange] = useState("6h"); + const [analyticsRange, setAnalyticsRange] = useState("30d"); + const [bucket, setBucket] = useState("day"); + const [compare, setCompare] = useState("previous_year"); + const [analyticsStart, setAnalyticsStart] = useState(""); + const [analyticsEnd, setAnalyticsEnd] = useState(""); + const [compareRanges, setCompareRanges] = useState>([{ key: "cmp_1", label: "Porównanie 1", start: "", end: "" }, { key: "cmp_2", label: "Porównanie 2", start: "", end: "" }]); + const [archiveStart, setArchiveStart] = useState(""); + const [archiveEnd, setArchiveEnd] = useState(""); + const [archiveRange, setArchiveRange] = useState("1d"); + const [liveMetrics, setLiveMetrics] = useState(() => readStorage(STORAGE_KEYS.liveMetrics, DEFAULT_LIVE_METRICS)); + const [archiveMetrics, setArchiveMetrics] = useState(() => readStorage(STORAGE_KEYS.archiveMetrics, DEFAULT_LIVE_METRICS)); + const [viewMode, setViewMode] = useState(() => { const fromUrl = parseViewModeFromLocation(); return fromUrl === "kiosk" ? fromUrl : readStorage(STORAGE_KEYS.viewMode, "normal", (raw) => (raw === "kiosk" ? "kiosk" : "normal")); }); + const [kioskWidgets, setKioskWidgets] = useState(() => getVisibleWidgets(readStorage(STORAGE_KEYS.kioskWidgets, DEFAULT_KIOSK_WIDGETS))); + const [kioskEditorMode, setKioskEditorMode] = useState<"private" | "public">("private"); + const [privateKioskDraft, setPrivateKioskDraft] = useState({ mode: "private", widgets: DEFAULT_KIOSK_WIDGETS, realtime_range: "12h", analytics_range: "30d", analytics_bucket: "day", compare_mode: "none" }); + const [publicKioskDraft, setPublicKioskDraft] = useState({ mode: "public", widgets: DEFAULT_KIOSK_WIDGETS, realtime_range: "12h", analytics_range: "30d", analytics_bucket: "day", compare_mode: "none" }); + const [blockConfig, setBlockConfig] = useState>(() => readStorage(STORAGE_KEYS.blockConfig, DEFAULT_BLOCK_CONFIG)); + const [loginForm, setLoginForm] = useState({ username: "", password: "" }); + const [loginError, setLoginError] = useState(null); + const [newUser, setNewUser] = useState({ username: "", display_name: "", password: "", role: "user" }); + const [passwordReset, setPasswordReset] = useState<{ username: string; password: string }>({ username: "", password: "" }); + const initializedRef = useRef(false); + + useEffect(() => { + if (!config || initializedRef.current) return; + initializedRef.current = true; + setActiveTab((config.defaults.tab as TabKey) || (publicMode ? "kiosk" : "realtime")); + setRealtimeRange(config.defaults.realtime_range); + setAnalyticsRange(config.defaults.analytics_range); + setBucket(config.defaults.analytics_bucket); + setTheme((current) => current || ((config.defaults.theme as ThemeMode) ?? "dark")); + setLanguage((current) => current || normalizeLanguage(config.defaults.language)); + }, [config, publicMode]); + useEffect(() => { if (privateKioskSettingsQuery.data) setPrivateKioskDraft(privateKioskSettingsQuery.data); }, [privateKioskSettingsQuery.data]); + useEffect(() => { if (publicKioskSettingsQuery.data) setPublicKioskDraft(publicKioskSettingsQuery.data); }, [publicKioskSettingsQuery.data]); + useEffect(() => { document.documentElement.setAttribute("data-bs-theme", theme); document.body.setAttribute("data-bs-theme", theme); writeStorage(STORAGE_KEYS.theme, theme); }, [theme]); + useEffect(() => { writeStorage(STORAGE_KEYS.language, language); }, [language]); + useEffect(() => { syncViewModeToLocation(viewMode); writeStorage(STORAGE_KEYS.viewMode, viewMode); }, [viewMode]); + useEffect(() => { writeStorage(STORAGE_KEYS.kioskWidgets, kioskWidgets); }, [kioskWidgets]); + useEffect(() => { writeStorage(STORAGE_KEYS.blockConfig, blockConfig); }, [blockConfig]); + useEffect(() => { writeStorage(STORAGE_KEYS.liveMetrics, liveMetrics); }, [liveMetrics]); + useEffect(() => { writeStorage(STORAGE_KEYS.archiveMetrics, archiveMetrics); }, [archiveMetrics]); + + const dataEnabled = authenticated || authEnabled === false; + const { snapshot, connected, lastUpdated } = useRealtimeSocket(dataEnabled); + const metricCandidates = useMemo(() => getMetricCandidates(snapshot, config), [snapshot, config]); + useEffect(() => { + if (!metricCandidates.length) return; + const allowed = new Set(metricCandidates.map((item) => item.metric_id)); + setLiveMetrics((current) => { + const filtered = current.filter((item) => allowed.has(item)); + return filtered.length ? filtered : metricCandidates.slice(0, 3).map((item) => item.metric_id); + }); + setArchiveMetrics((current) => { + const filtered = current.filter((item) => allowed.has(item)); + return filtered.length ? filtered : metricCandidates.slice(0, 3).map((item) => item.metric_id); + }); + }, [metricCandidates]); + const liveHistoryMetrics = useMemo(() => liveMetrics.filter((item) => item !== "inverter_temp"), [liveMetrics]); + const effectiveKioskSettings = publicMode ? publicKioskDraft : privateKioskDraft; + const kioskActive = publicMode || viewMode === "kiosk"; + const effectiveKioskWidgets = toWidgetIds(kioskActive ? effectiveKioskSettings.widgets : kioskWidgets); + const effectiveRealtimeRange = kioskActive ? effectiveKioskSettings.realtime_range : realtimeRange; + const effectiveAnalyticsRange = kioskActive ? effectiveKioskSettings.analytics_range : analyticsRange; + const effectiveBucket = kioskActive ? effectiveKioskSettings.analytics_bucket : bucket; + const effectiveCompare = kioskActive ? effectiveKioskSettings.compare_mode : compare; + const historyQuery = useRealtimeHistory(effectiveRealtimeRange, dataEnabled, { metrics: liveHistoryMetrics, publicKiosk: publicMode }); + const sanitizedCompareRanges = compareRanges.filter((item) => item.start && item.end); + const analyticsOptions = analyticsStart && analyticsEnd && !kioskActive ? { start: analyticsStart, end: analyticsEnd, publicKiosk: publicMode, compareRanges: effectiveCompare === "custom_multi" ? sanitizedCompareRanges : undefined } : { publicKiosk: publicMode, compareRanges: effectiveCompare === "custom_multi" ? sanitizedCompareRanges : undefined }; + const analyticsQuery = useAnalytics(analyticsStart && analyticsEnd && !kioskActive ? "custom" : effectiveAnalyticsRange, effectiveBucket, effectiveCompare, dataEnabled, analyticsOptions); + const historical = useHistoricalImport(dataEnabled && !publicMode); + const archiveQuery = useRealtimeHistory(archiveStart && archiveEnd ? "custom" : archiveRange, dataEnabled, { start: archiveStart || undefined, end: archiveEnd || undefined, metrics: archiveMetrics, publicKiosk: publicMode }); + const usersQuery = useQuery({ queryKey: ["auth-users"], queryFn: api.getUsers, enabled: dataEnabled && (authQuery.data?.role === "admin"), staleTime: 15_000 }); + + const loginMutation = useMutation({ mutationFn: () => api.login(loginForm.username, loginForm.password), onSuccess: async () => { setLoginError(null); setLoginForm((value) => ({ ...value, password: "" })); await queryClient.invalidateQueries({ queryKey: ["auth-status"] }); await queryClient.invalidateQueries({ queryKey: ["dashboard-config"] }); }, onError: (error: Error) => setLoginError(parseError(error) || t(language, "loginError")) }); + const logoutMutation = useMutation({ mutationFn: api.logout, onSuccess: async () => { await queryClient.clear(); await queryClient.invalidateQueries({ queryKey: ["auth-status"] }); } }); + const createUserMutation = useMutation({ mutationFn: () => api.createUser(newUser), onSuccess: async () => { setNewUser({ username: "", display_name: "", password: "", role: "user" }); await usersQuery.refetch(); } }); + const resetPasswordMutation = useMutation({ mutationFn: () => api.resetUserPassword(passwordReset.username, passwordReset.password), onSuccess: async () => { setPasswordReset({ username: "", password: "" }); await usersQuery.refetch(); } }); + + const saveKioskSettingsMutation = useMutation({ mutationFn: (payload: KioskSettingsPayload) => api.saveKioskSettings(payload), onSuccess: async (_, payload) => { await queryClient.invalidateQueries({ queryKey: ["kiosk-settings", payload.mode] }); } }); + + const locale = localeForLanguage(language); + const widgetLabels = useMemo(() => { const map = new Map(); for (const item of widgetOrder) map.set(item.id, buildWidgetLabel(language, item.id)); return map; }, [language]); + const summary = analyticsQuery.production.data?.summary; + const topStatus = snapshot.status ?? []; + const heroCards = snapshot.hero_cards.filter((card) => blockConfig.hero.includes(card.metric_id)); + const quickMetrics = Object.values(snapshot.kpis ?? {}).filter((metric) => blockConfig.quick.includes(metric.metric_id)); + const isAdmin = authQuery.data?.role === "admin"; + const kioskUrl = `${window.location.origin}${window.location.pathname}?mode=kiosk&publicKiosk=1`; + + const allWidgets: Record = { + hero: , + quickMetrics: , + history: , + status: , + strings: , + production: , + comparison: compare !== "none" ? : null, + distribution: , + importStatus: , + }; + const renderWidget = (widgetId: WidgetId) => { const content = allWidgets[widgetId]; if (!content) return null; return
    {content}
    ; }; + + if ((!publicMode && authQuery.isLoading) || (authEnabled && !authenticated && loginMutation.isPending)) return ; + if (authEnabled && !authenticated) return loginMutation.mutate()} onThemeToggle={() => setTheme((current) => (current === "dark" ? "light" : "dark"))} onLanguageToggle={() => setLanguage((current) => (current === "pl" ? "en" : "pl"))} loading={loginMutation.isPending} error={loginError} />; + if (configQuery.isLoading || !config) return ; + + const navbar = ( +
    +
    +
    {config.app.site_name}
    {t(language, "operatorPanel")}
    +
    + {connected ? t(language, "connected") : t(language, "disconnected")} + + + {!publicMode ? : null} + {!publicMode ? : null} +
    +
    +
    + ); + const menu = ( +
      + } active={activeTab === "realtime"} onClick={() => setActiveTab("realtime")} label={language === "en" ? "Live" : "Live"} /> + } active={activeTab === "archive"} onClick={() => setActiveTab("archive")} label={language === "en" ? "Historical live" : "Dane chwilowe"} /> + } active={activeTab === "analytics"} onClick={() => setActiveTab("analytics")} label={t(language, "analytics")} /> + } active={activeTab === "warehouse"} onClick={() => setActiveTab("warehouse")} label={language === "en" ? "Data warehouse" : "Hurtownia danych"} /> + } active={activeTab === "kiosk"} onClick={() => setActiveTab("kiosk")} label={t(language, "kiosk")} /> + } active={activeTab === "settings"} onClick={() => setActiveTab("settings")} label={t(language, "settings")} /> +
    {t(language, "updatedAt")}: {formatDateTime(lastUpdated, locale)}
    + ); + + if (viewMode === "kiosk" || publicMode) { + return
    {config.app.site_name}
    {t(language, "kioskHint")}
    {!publicMode ? : null}
    {effectiveKioskWidgets.map((widgetId) => renderWidget(widgetId))}
    ; + } + + return ( +
    {navbar}{menu}
    + {activeTab === "realtime" && <>
    {renderWidget("hero")}
    {allWidgets.quickMetrics}
    {allWidgets.history}
    {allWidgets.status}
    {allWidgets.strings}
    } + + {activeTab === "archive" && <>
    { setArchiveRange(value); applyArchivePreset(value, setArchiveStart, setArchiveEnd); }} options={archiveRangeOptions(language)} />
    { setArchiveRange("custom"); setArchiveStart(e.target.value); }} />
    { setArchiveRange("custom"); setArchiveEnd(e.target.value); }} />
    item.metric_id !== "energy_total")} selected={archiveMetrics.filter((item) => item !== "energy_total")} onChange={setArchiveMetrics} />
    } + + {activeTab === "analytics" && <>
    { if (value !== "custom") { setAnalyticsRange(value); setAnalyticsStart(""); setAnalyticsEnd(""); } }} options={[...config.capabilities.ranges.filter((item) => !["6h", "24h"].includes(item.key)).map((item) => ({ key: item.key, label: item.label })), { key: "custom", label: language === "en" ? "Custom" : "Ręczny" }]} /> ({ key: item.key, label: translateBucket(language, item.key) }))} />
    {analyticsStart || analyticsEnd || (analyticsRange === "custom") || compare === "custom_multi" ?
    setAnalyticsStart(e.target.value)} />
    setAnalyticsEnd(e.target.value)} />
    {compare === "custom_multi" ?
    {language === "en" ? "Comparison ranges" : "Zakresy porównawcze"}
    {compareRanges.map((item, index) =>
    setCompareRanges(compareRanges.map((current) => current.key === item.key ? { ...current, label: e.target.value } : current))} />
    setCompareRanges(compareRanges.map((current) => current.key === item.key ? { ...current, start: e.target.value } : current))} />
    setCompareRanges(compareRanges.map((current) => current.key === item.key ? { ...current, end: e.target.value } : current))} />
    )}
    : null}
    : null}
    item.key === compare)?.label ?? compare} />
    {allWidgets.production}
    {allWidgets.distribution}
    {compare !== "none" ?
    {allWidgets.comparison}
    : null}
    } + + {activeTab === "warehouse" && <>
    historical.start.mutate(payload)} onSyncNow={() => historical.syncNow.mutate()} onCancel={() => historical.cancel.mutate()} />
    } + + {activeTab === "kiosk" && <>
    kioskEditorMode === "public" ? setPublicKioskDraft(value) : setPrivateKioskDraft(value)} onModeChange={setKioskEditorMode} selectedMode={kioskEditorMode} labels={widgetLabels} buckets={config.capabilities.buckets} compareModes={config.capabilities.comparison_modes} saving={saveKioskSettingsMutation.isPending && saveKioskSettingsMutation.variables?.mode === kioskEditorMode} onSave={() => saveKioskSettingsMutation.mutate(kioskEditorMode === "public" ? publicKioskDraft : privateKioskDraft)} />
    } + + {activeTab === "settings" && <>
    item.metric_id !== "energy_total")} selected={liveMetrics.filter((item) => item !== "energy_total")} onChange={setLiveMetrics} />
    {isAdmin ?
    createUserMutation.mutate()} passwordReset={passwordReset} onPasswordResetChange={setPasswordReset} onResetPassword={() => resetPasswordMutation.mutate()} />
    : null}
    } +
    + ); +} + +function parseError(error: Error): string | null { const match = error.message.match(/"detail"\s*:\s*"([^"]+)"/); return match?.[1] ?? error.message; } +function requestFullscreen() { const element = document.documentElement as HTMLElement & { webkitRequestFullscreen?: () => Promise | void; msRequestFullscreen?: () => Promise | void; }; if (element.requestFullscreen) { void element.requestFullscreen(); return; } if (element.webkitRequestFullscreen) { void element.webkitRequestFullscreen(); return; } if (element.msRequestFullscreen) void element.msRequestFullscreen(); } +function translateBucket(language: Language, key: string): string { const map: Record = { day: { pl: "Dzień", en: "Day" }, week: { pl: "Tydzień", en: "Week" }, month: { pl: "Miesiąc", en: "Month" }, year: { pl: "Rok", en: "Year" } }; return map[key]?.[language] ?? key; } +function comparisonOptions(language: Language) { return [{ key: "none", label: translateCompareMode(language, "none") }, { key: "previous_period", label: translateCompareMode(language, "previous_period") }, { key: "previous_year", label: translateCompareMode(language, "previous_year") }, { key: "previous_year_2", label: translateCompareMode(language, "previous_year_2") }, { key: "previous_year_3", label: translateCompareMode(language, "previous_year_3") }, { key: "previous_month_12", label: translateCompareMode(language, "previous_month_12") }, { key: "previous_month_24", label: translateCompareMode(language, "previous_month_24") }, { key: "custom_multi", label: translateCompareMode(language, "custom_multi") }]; } + +function applyArchivePreset(rangeKey: string, setStart: (value: string) => void, setEnd: (value: string) => void) { + if (rangeKey !== "custom") { + setStart(""); + setEnd(""); + } +} +function LoadingScreen({ language }: { language: Language }) { return
    {t(language, "loading")}…
    ; } +function LoginPage({ language, theme, form, onChange, onSubmit, onThemeToggle, onLanguageToggle, loading, error }: { language: Language; theme: ThemeMode; form: { username: string; password: string }; onChange: (value: { username: string; password: string }) => void; onSubmit: () => void; onThemeToggle: () => void; onLanguageToggle: () => void; loading: boolean; error: string | null; }) { return

    {t(language, "loginTitle")}

    {t(language, "loginSubtitle")}
    onChange({ ...form, username: event.target.value })} autoComplete="username" />
    onChange({ ...form, password: event.target.value })} autoComplete="current-password" onKeyDown={(event) => event.key === "Enter" && onSubmit()} />
    {error ?
    {error}
    : null}
    ; } +function NavItem({ icon, active, onClick, label }: { icon: ReactElement; active: boolean; onClick: () => void; label: string }) { return
  • ; } +function PageHeader({ title, subtitle, children }: { title: string; subtitle: string; children?: ReactNode }) { return
    PV Insight

    {title}

    {subtitle}
    {children ?
    {children}
    : null}
    ; } +function SegmentedSelect({ label, value, onChange, options }: { label: string; value: string; onChange: (value: string) => void; options: Array<{ key: string; label: string }> }) { return
    {label}
    {options.map((option) => )}
    ; } +function HeroCards({ cards, locale, language }: { cards: SnapshotPayload["hero_cards"]; locale: string; language: Language }) { return
    {cards.map((card) =>
    {iconForMetric(card.metric_id)}{card.unit || "live"}
    {labelForMetric(language, card.metric_id, card.label)}
    {formatValue(card.value, card.unit, card.unit === "kWh" ? 2 : 2, locale)}
    {card.subtitle}
    )}
    ; } +function QuickMetrics({ metrics, locale, language }: { metrics: MetricValue[]; locale: string; language: Language }) { return

    {t(language, "quickMetrics")}

    {metrics.map((metric) =>
    {iconForMetric(metric.metric_id)}
    {labelForMetric(language, metric.metric_id, metric.label)}
    {metric.unit || t(language, "status")}
    {formatValue(metric.value, metric.unit, 2, locale)}
    )}
    ; } +function LiveHistoryPanel({ data, language, theme, title, subtitle }: { data?: HistoryPayload; language: Language; theme: ThemeMode; title: string; subtitle: string }) { return

    {title}

    {subtitle}
    ; } +function StatusPanel({ metrics, locale, language }: { metrics: MetricValue[]; locale: string; language: Language }) { return

    {t(language, "systemStatus")}

    {metrics.map((metric) =>
    {labelForMetric(language, metric.metric_id, metric.label)}
    {metric.unit || t(language, "status")}
    {formatValue(metric.value, metric.unit, 2, locale)}
    )}
    ; } +function StringsPanel({ rows, locale, language }: { rows: SnapshotGroupRow[]; locale: string; language: Language }) { return

    {t(language, "strings")}

    {rows.length === 0 ?
    {t(language, "noDataDescription")}
    : rows.map((row) =>
    {row.label}
    DC
    {Object.values(row.values).map((metric) =>
    {labelForMetric(language, metric.metric_id, metric.label)}{formatValue(metric.value, metric.unit, 2, locale)}
    )}
    )}
    ; } +function SummaryCards({ summary, language, locale, compareLabel }: { summary?: AnalyticsPayload["summary"]; language: Language; locale: string; compareLabel: string }) { const items = [{ key: t(language, "summaryTotal"), value: formatValue(summary?.total, summary?.unit ?? "kWh", 2, locale), badge: compareLabel }, { key: t(language, "summaryAverage"), value: formatValue(summary?.average_bucket, summary?.unit ?? "kWh", 2, locale) }, { key: t(language, "summaryBest"), value: summary ? `${summary.best_bucket_label} · ${formatValue(summary.best_bucket_value, summary.unit, 2, locale)}` : "--" }, { key: t(language, "summaryCo2"), value: formatValue(summary?.co2_saved_kg, "kg", 1, locale) }]; return
    {items.map((item) =>
    {item.key}
    {item.value}
    {item.badge ?
    {item.badge}
    : null}
    )}
    ; } +function ProductionPanel({ data, language, theme }: { data?: AnalyticsPayload; language: Language; theme: ThemeMode }) { return

    {t(language, "chartProduction")}

    {t(language, "chartProductionSubtitle")}
    ; } +function ComparisonPanel({ data, language, theme }: { data?: AnalyticsPayload; language: Language; theme: ThemeMode }) { return

    {t(language, "chartComparison")}

    ; } +function DistributionPanel({ data, language, theme, locale }: { data?: DistributionPayload; language: Language; theme: ThemeMode; locale: string }) { return

    {t(language, "chartDistribution")}

    {formatValue(data?.total, data?.unit ?? "kWh", 2, locale)}
    ; } +function HistoricalPanel({ status, language, locale, compact = false }: { status?: HistoricalStatus; language: Language; locale: string; compact?: boolean }) { if (!status) return
    {t(language, "noDataDescription")}
    ; return

    {language === "en" ? "Data warehouse" : "Hurtownia danych"}

    {t(language, "importArchiveSubtitle")}
    {!compact ? <>
    {t(language, "activeChunk")}{status.active_chunk_index}/{status.total_chunks}
    {status.recent_chunks.map((chunk) => )}
    {t(language, "recentChunks")}{t(language, "status")}kWh
    #{chunk.chunk_index}
    {chunk.start_date} → {chunk.end_date}
    {chunk.state}{formatValue(chunk.energy_kwh, "kWh", 2, locale)}
    {status.recent_events.map((event, index) =>
    {event.title}
    {event.message}
    {formatShortTime(event.timestamp, locale)}
    )}
    : null}
    ; } +function StatusStat({ label, value }: { label: string; value: string }) { return
    {label}
    {value}
    ; } +function ImportControls({ status, language, onStart, onSyncNow, onCancel }: { status?: HistoricalStatus; language: Language; onStart: (payload: { start_date?: string; end_date?: string; chunk_days?: number; force?: boolean }) => void; onSyncNow: () => void; onCancel: () => void; }) { const [startDate, setStartDate] = useState(status?.available_start_date ?? ""); const [endDate, setEndDate] = useState(status?.available_end_date ?? ""); const [chunkDays, setChunkDays] = useState(String(status?.default_chunk_days ?? 7)); useEffect(() => { if (!status) return; setStartDate((current) => current || status.available_start_date || ""); setEndDate((current) => current || status.available_end_date || ""); setChunkDays((current) => current || String(status.default_chunk_days || 7)); }, [status]); return

    {language === "en" ? "Import controls" : "Sterowanie importem"}

    setStartDate(event.target.value)} />
    setEndDate(event.target.value)} />
    setChunkDays(event.target.value)} />
    ; } +function KioskLayoutPanel({ language, widgets, onChange, labels }: { language: Language; widgets: WidgetId[]; onChange: (value: WidgetId[]) => void; labels: Map; }) { const available = widgetOrder.map((item) => item.id); const selected = widgets; const unselected = available.filter((item) => !selected.includes(item)); const move = (id: WidgetId, direction: -1 | 1) => { const index = selected.indexOf(id); if (index === -1) return; const target = index + direction; if (target < 0 || target >= selected.length) return; const next = [...selected]; [next[index], next[target]] = [next[target], next[index]]; onChange(next); }; const toggle = (id: WidgetId) => { if (selected.includes(id)) { const next = selected.filter((item) => item !== id); onChange(next.length ? next : selected); return; } onChange([...selected, id]); }; return

    {t(language, "kioskLayout")}

    {t(language, "kioskLayoutSubtitle")}
    {t(language, "saveLayout")}
    {t(language, "selected")}
    {selected.map((id) =>
    {labels.get(id)}
    )}
    {t(language, "available")}
    {unselected.map((id) => )}
    ; } +function KioskSettingsEditorPanel({ language, value, onChange, onSave, selectedMode, onModeChange, labels, buckets, compareModes, saving }: { language: Language; value: KioskSettingsPayload; onChange: (value: KioskSettingsPayload) => void; onSave: () => void; selectedMode: "public" | "private"; onModeChange: (value: "public" | "private") => void; labels: Map; buckets: Array<{ key: string; label: string }>; compareModes: string[]; saving: boolean; }) { const widgets = toWidgetIds(value.widgets); return

    {language === "en" ? "Kiosk settings" : "Ustawienia kiosku"}

    onChange({ ...value, widgets: widgetsValue })} labels={labels} />
    ; } +function KioskLinkPanel({ language, kioskUrl, settings }: { language: Language; kioskUrl: string; settings: KioskSettingsPayload }) { const [copied, setCopied] = useState(false); return

    {language === "en" ? "Public kiosk link" : "Publiczny link kiosku"}

    {language === "en" ? "This link opens kiosk mode without login for read-only public display." : "Ten link otwiera kiosk bez logowania, tylko do publicznego podglądu."}
    {language === "en" ? "Current public ranges:" : "Aktualne zakresy publiczne:"} live {settings.realtime_range}, analytics {settings.analytics_range}
    ; } +function AppearanceSecurityPanel({ language, theme, setTheme, viewMode, setViewMode, authEnabled, userName }: { language: Language; theme: ThemeMode; setTheme: (value: ThemeMode) => void; viewMode: ViewMode; setViewMode: (value: ViewMode) => void; authEnabled: boolean; userName: string; }) { return

    {t(language, "theme")}

    {t(language, "viewMode")}

    {t(language, "security")}

    {authEnabled ? t(language, "authEnabled") : t(language, "authDisabled")}
    {language === "en" ? "Admin user management is available below." : "Zarządzanie użytkownikami admina jest dostępne niżej."}
    {userName ?
    {userName}
    : null}
    ; } +function MetricSelectorCard({ language, title, items, selected, onChange }: { language: Language; title: string; items: Array<{ metric_id: string; label: string; unit: string }>; selected: string[]; onChange: (value: string[]) => void; }) { const toggle = (metricId: string) => onChange(selected.includes(metricId) ? selected.filter((item) => item !== metricId) : [...selected, metricId]); return

    {title}

    {items.map((item) => )}
    ; } +function LiveChartMetricsPanel({ language, items, selected, onChange }: { language: Language; items: Array<{ metric_id: string; label: string; unit: string }>; selected: string[]; onChange: (value: string[]) => void; }) { return ; } +function BlockVisibilityPanel({ language, items, config, onChange }: { language: Language; items: Array<{ metric_id: string; label: string; unit: string }>; config: Record; onChange: (value: Record) => void; }) { const toggle = (target: BlockTarget, metricId: string) => { const selected = config[target]; onChange({ ...config, [target]: selected.includes(metricId) ? selected.filter((item) => item !== metricId) : [...selected, metricId] }); }; return

    {language === "en" ? "Metric visibility in blocks" : "Widoczność metryk w blokach"}

    Hero metrics
    {items.map((item) => )}
    Quick metrics
    {items.map((item) => )}
    ; } +function AdminUsersPanel({ language, users, newUser, onNewUserChange, onCreate, passwordReset, onPasswordResetChange, onResetPassword }: { language: Language; users: AuthUsersPayload["items"]; newUser: { username: string; display_name: string; password: string; role: string }; onNewUserChange: (value: { username: string; display_name: string; password: string; role: string }) => void; onCreate: () => void; passwordReset: { username: string; password: string }; onPasswordResetChange: (value: { username: string; password: string }) => void; onResetPassword: () => void; }) { return

    {language === "en" ? "Admin user management" : "Zarządzanie użytkownikami"}

    {language === "en" ? "Create user" : "Dodaj użytkownika"}
    onNewUserChange({ ...newUser, username: e.target.value })} />
    onNewUserChange({ ...newUser, display_name: e.target.value })} />
    onNewUserChange({ ...newUser, password: e.target.value })} />
    {language === "en" ? "Reset password" : "Zmiana hasła"}
    onPasswordResetChange({ ...passwordReset, password: e.target.value })} />
    {users.map((user) => )}
    {language === "en" ? "Username" : "Login"}{language === "en" ? "Display name" : "Nazwa"}Role{language === "en" ? "Updated" : "Aktualizacja"}
    {user.username}{user.display_name}{user.role}{formatDateTime(user.updated_at, language === "en" ? "en-GB" : "pl-PL")}
    ; } diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..3b17f60 --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,145 @@ +import type { + AnalyticsPayload, + AuthStatus, + AuthUsersPayload, + DashboardConfig, + KioskSettingsPayload, + DistributionPayload, + HistoryPayload, + HistoricalStartPayload, + HistoricalStatus, + SnapshotPayload, +} from "../types"; +import { + demoAnalytics, + demoAuthStatus, + demoConfig, + demoDistribution, + demoHistory, + demoHistoricalStatus, + demoSnapshot, +} from "../demo/data"; + +function defaultApiBase(): string { + const explicit = import.meta.env.VITE_API_BASE_URL; + if (explicit) return explicit; + + const { protocol, hostname, port } = window.location; + if (port === "5173" || port === "4173") { + return `${protocol}//${hostname}:8105/api/v1`; + } + + return "/api/v1"; +} + +const API_BASE = defaultApiBase(); +const DEMO_MODE = String(import.meta.env.VITE_DEMO_MODE ?? "").toLowerCase() === "true"; +const locationUrl = new URL(window.location.href); +const PUBLIC_KIOSK = locationUrl.searchParams.get("publicKiosk") === "1" || locationUrl.pathname.endsWith("/kiosk/public"); + +async function request(path: string, init?: RequestInit): Promise { + const method = (init?.method || "GET").toUpperCase(); + const resolvedPath = PUBLIC_KIOSK && method === "GET" ? `${path}${path.includes("?") ? "&" : "?"}publicKiosk=1` : path; + const response = await fetch(`${API_BASE}${resolvedPath}`, { + credentials: "include", + headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) }, + ...init, + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(text || `HTTP ${response.status}`); + } + + const contentType = response.headers.get("content-type") ?? ""; + if (!contentType.includes("application/json")) { + return {} as T; + } + return response.json() as Promise; +} + +function clone(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +async function demoResponse(factory: () => T): Promise { + await new Promise((resolve) => window.setTimeout(resolve, 120)); + return clone(factory()); +} + +export const api = { + getConfig: () => (DEMO_MODE ? demoResponse(() => demoConfig) : request("/dashboard/config")), + getKioskSettings: (mode: "public" | "private") => (DEMO_MODE ? demoResponse(() => ({ mode, widgets: ["hero", "history", "strings", "status", "production", "comparison", "importStatus"], realtime_range: "12h", analytics_range: "30d", analytics_bucket: "day", compare_mode: "none" })) : request(`/dashboard/kiosk-settings?mode=${mode}`)), + saveKioskSettings: (payload: KioskSettingsPayload) => (DEMO_MODE ? demoResponse(() => payload) : request("/dashboard/kiosk-settings", { method: "PUT", body: JSON.stringify(payload) })), + getAuthStatus: () => (DEMO_MODE ? demoResponse(() => demoAuthStatus) : request("/auth/status")), + login: (username: string, password: string) => + DEMO_MODE + ? demoResponse(() => ({ ...demoAuthStatus, authenticated: true, user: username || "demo", display_name: username || "demo" })) + : request("/auth/login", { method: "POST", body: JSON.stringify({ username, password }) }), + logout: () => (DEMO_MODE ? demoResponse(() => ({ ...demoAuthStatus, authenticated: false })) : request("/auth/logout", { method: "POST", body: JSON.stringify({}) })), + getRealtimeSnapshot: () => (DEMO_MODE ? demoResponse(() => demoSnapshot()) : request("/realtime/snapshot")), + getRealtimeHistory: (range: string, options?: { start?: string; end?: string; metrics?: string[]; publicKiosk?: boolean }) => { + const params = new URLSearchParams(); + params.set("range", range); + if (options?.start) params.set("start", options.start); + if (options?.end) params.set("end", options.end); + if (options?.metrics?.length) params.set("metrics", options.metrics.join(",")); + if (options?.publicKiosk) params.set("publicKiosk", "1"); + return DEMO_MODE ? demoResponse(() => ({ ...demoHistory, range_key: range })) : request(`/realtime/history?${params.toString()}`); + }, + getAnalytics: (range: string, bucket: string, compare: string, options?: { start?: string; end?: string; publicKiosk?: boolean; compareRanges?: Array<{ start: string; end: string; label: string; key?: string }> }) => { + const params = new URLSearchParams(); + params.set("range", range); + params.set("bucket", bucket); + params.set("compare", compare); + if (options?.start) params.set("start", options.start); + if (options?.end) params.set("end", options.end); + if (options?.publicKiosk) params.set("publicKiosk", "1"); + if (options?.compareRanges?.length) params.set("compare_ranges", JSON.stringify(options.compareRanges)); + return DEMO_MODE + ? demoResponse(() => ({ + ...demoAnalytics, + bucket, + compare_mode: compare, + meta: { ...demoAnalytics.meta, window: { ...demoAnalytics.meta.window, range_key: range } }, + })) + : request(`/analytics/production?${params.toString()}`); + }, + getDistribution: (range: string, bucket: string, options?: { start?: string; end?: string; publicKiosk?: boolean }) => { + const params = new URLSearchParams(); + params.set("range", range); + params.set("bucket", bucket); + if (options?.start) params.set("start", options.start); + if (options?.end) params.set("end", options.end); + if (options?.publicKiosk) params.set("publicKiosk", "1"); + return DEMO_MODE ? demoResponse(() => ({ ...demoDistribution, bucket })) : request(`/analytics/distribution?${params.toString()}`); + }, + getHistoricalStatus: () => (DEMO_MODE ? demoResponse(() => demoHistoricalStatus) : request("/historical/status")), + startHistoricalImport: (payload: HistoricalStartPayload) => + DEMO_MODE + ? demoResponse(() => ({ ...demoHistoricalStatus, ...payload, message: "Tryb demo: import uruchomiony", running: true })) + : request("/historical/start", { method: "POST", body: JSON.stringify(payload) }), + syncHistoricalNow: () => + DEMO_MODE + ? demoResponse(() => ({ ...demoHistoricalStatus, message: "Tryb demo: synchronizacja brakujacych dni" })) + : request("/historical/sync-now", { method: "POST", body: JSON.stringify({}) }), + cancelHistoricalImport: () => + DEMO_MODE + ? demoResponse(() => ({ ...demoHistoricalStatus, running: false, state: "cancelled", message: "Tryb demo: anulowano" })) + : request("/historical/cancel", { method: "POST", body: JSON.stringify({}) }), + getUsers: () => (DEMO_MODE ? demoResponse(() => ({ items: [] })) : request("/auth/users")), + createUser: (payload: { username: string; password: string; role: string; display_name?: string }) => + DEMO_MODE ? demoResponse(() => payload) : request("/auth/users", { method: "POST", body: JSON.stringify(payload) }), + resetUserPassword: (username: string, password: string) => + DEMO_MODE ? demoResponse(() => ({ username })) : request(`/auth/users/${encodeURIComponent(username)}/reset-password`, { method: "POST", body: JSON.stringify({ password }) }), +}; + +export const wsBaseUrl = (): string => { + const explicit = import.meta.env.VITE_WS_BASE_URL; + if (explicit) return explicit.endsWith("/") ? explicit.slice(0, -1) : explicit; + + const { protocol, hostname, port } = window.location; + const wsProtocol = protocol === "https:" ? "wss:" : "ws:"; + const raw = (port === "5173" || port === "4173") ? `${wsProtocol}//${hostname}:8105` : `${wsProtocol}//${window.location.host}`; + return raw.endsWith("/") ? raw.slice(0, -1) : raw; +}; diff --git a/frontend/src/components/analytics/ComparisonChart.tsx b/frontend/src/components/analytics/ComparisonChart.tsx new file mode 100644 index 0000000..adb75b5 --- /dev/null +++ b/frontend/src/components/analytics/ComparisonChart.tsx @@ -0,0 +1,66 @@ +import type { EChartsOption } from "echarts"; +import { Card } from "../common/Card"; +import { EChart } from "../common/EChart"; +import type { BucketPoint } from "../../types"; + +interface ComparisonChartProps { + current: BucketPoint[]; + comparison: BucketPoint[]; + unit: string; + compareMode: string; +} + +export function ComparisonChart({ current, comparison, unit, compareMode }: ComparisonChartProps) { + const option: EChartsOption = { + tooltip: { + trigger: "axis", + backgroundColor: "rgba(2, 6, 23, 0.95)", + borderColor: "rgba(255,255,255,0.08)", + textStyle: { color: "#e2e8f0" }, + }, + legend: { + top: 0, + textStyle: { color: "#cbd5e1" }, + }, + grid: { + left: 18, + right: 16, + top: 46, + bottom: 24, + containLabel: true, + }, + xAxis: { + type: "category", + axisLabel: { color: "#94a3b8", rotate: current.length > 12 ? 35 : 0 }, + axisLine: { lineStyle: { color: "rgba(255,255,255,0.08)" } }, + data: current.map((item) => item.label), + }, + yAxis: { + type: "value", + name: unit, + axisLabel: { color: "#94a3b8" }, + splitLine: { lineStyle: { color: "rgba(255,255,255,0.06)" } }, + }, + series: [ + { + name: "Aktualny okres", + type: "bar", + barGap: "20%", + itemStyle: { color: "#60a5fa", borderRadius: [10, 10, 0, 0] }, + data: current.map((item) => item.value), + }, + { + name: compareMode === "previous_year" ? "Poprzedni rok" : "Poprzedni okres", + type: "bar", + itemStyle: { color: "#f59e0b", borderRadius: [10, 10, 0, 0] }, + data: current.map((_, index) => comparison[index]?.value ?? 0), + }, + ], + }; + + return ( + + + + ); +} diff --git a/frontend/src/components/analytics/DistributionPieChart.tsx b/frontend/src/components/analytics/DistributionPieChart.tsx new file mode 100644 index 0000000..ea5d468 --- /dev/null +++ b/frontend/src/components/analytics/DistributionPieChart.tsx @@ -0,0 +1,51 @@ +import type { EChartsOption } from "echarts"; +import { Card } from "../common/Card"; +import { EChart } from "../common/EChart"; +import type { DistributionPayload } from "../../types"; + +interface DistributionPieChartProps { + distribution?: DistributionPayload; +} + +export function DistributionPieChart({ distribution }: DistributionPieChartProps) { + const option: EChartsOption = { + tooltip: { + trigger: "item", + backgroundColor: "rgba(2, 6, 23, 0.95)", + borderColor: "rgba(255,255,255,0.08)", + textStyle: { color: "#e2e8f0" }, + }, + legend: { + orient: "vertical", + right: 0, + top: "center", + textStyle: { color: "#cbd5e1" }, + }, + series: [ + { + type: "pie", + radius: ["42%", "68%"], + center: ["38%", "50%"], + padAngle: 2, + itemStyle: { + borderColor: "#020617", + borderWidth: 4, + }, + label: { + color: "#e2e8f0", + formatter: "{b}: {d}%", + }, + data: distribution?.slices.map((item) => ({ + name: item.label, + value: item.value, + })) ?? [], + }, + ], + }; + + return ( + + + + ); +} diff --git a/frontend/src/components/analytics/PeriodControls.tsx b/frontend/src/components/analytics/PeriodControls.tsx new file mode 100644 index 0000000..d2bfee3 --- /dev/null +++ b/frontend/src/components/analytics/PeriodControls.tsx @@ -0,0 +1,76 @@ +import { Card } from "../common/Card"; + +interface PeriodControlsProps { + rangeKey: string; + bucket: string; + compare: string; + ranges: Array<{ key: string; label: string }>; + buckets: Array<{ key: string; label: string }>; + compareModes: Array<{ key: string; label: string }>; + onRangeChange: (value: string) => void; + onBucketChange: (value: string) => void; + onCompareChange: (value: string) => void; +} + +export function PeriodControls({ + rangeKey, + bucket, + compare, + ranges, + buckets, + compareModes, + onRangeChange, + onBucketChange, + onCompareChange, +}: PeriodControlsProps) { + return ( + +
    + + + + + +
    +
    + ); +} diff --git a/frontend/src/components/analytics/ProductionBarChart.tsx b/frontend/src/components/analytics/ProductionBarChart.tsx new file mode 100644 index 0000000..fb89263 --- /dev/null +++ b/frontend/src/components/analytics/ProductionBarChart.tsx @@ -0,0 +1,56 @@ +import type { EChartsOption } from "echarts"; +import { Card } from "../common/Card"; +import { EChart } from "../common/EChart"; +import type { BucketPoint } from "../../types"; + +interface ProductionBarChartProps { + current: BucketPoint[]; + unit: string; +} + +export function ProductionBarChart({ current, unit }: ProductionBarChartProps) { + const option: EChartsOption = { + tooltip: { + trigger: "axis", + backgroundColor: "rgba(2, 6, 23, 0.95)", + borderColor: "rgba(255,255,255,0.08)", + textStyle: { color: "#e2e8f0" }, + }, + grid: { + left: 18, + right: 16, + top: 30, + bottom: 24, + containLabel: true, + }, + xAxis: { + type: "category", + axisLabel: { color: "#94a3b8", rotate: current.length > 12 ? 35 : 0 }, + axisLine: { lineStyle: { color: "rgba(255,255,255,0.08)" } }, + data: current.map((item) => item.label), + }, + yAxis: { + type: "value", + name: unit, + axisLabel: { color: "#94a3b8" }, + splitLine: { lineStyle: { color: "rgba(255,255,255,0.06)" } }, + }, + series: [ + { + type: "bar", + barMaxWidth: 26, + itemStyle: { + borderRadius: [10, 10, 0, 0], + color: "#34d399", + }, + data: current.map((item) => item.value), + }, + ], + }; + + return ( + + + + ); +} diff --git a/frontend/src/components/analytics/SummaryCards.tsx b/frontend/src/components/analytics/SummaryCards.tsx new file mode 100644 index 0000000..060c1c3 --- /dev/null +++ b/frontend/src/components/analytics/SummaryCards.tsx @@ -0,0 +1,34 @@ +import { Card } from "../common/Card"; +import { formatValue } from "../../lib/format"; +import type { AnalyticsSummary } from "../../types"; + +interface SummaryCardsProps { + summary: AnalyticsSummary; +} + +export function SummaryCards({ summary }: SummaryCardsProps) { + const tiles = [ + { label: "Produkcja", value: formatValue(summary.total, summary.unit, 2) }, + { label: "Srednio / bucket", value: formatValue(summary.average_bucket, summary.unit, 2) }, + { label: "Najlepszy bucket", value: `${summary.best_bucket_label || "--"} / ${formatValue(summary.best_bucket_value, summary.unit, 2)}` }, + { label: "CO2 mniej", value: formatValue(summary.co2_saved_kg, "kg", 2) }, + { + label: "Delta vs porownanie", + value: + summary.comparison_delta_pct === null || summary.comparison_delta_pct === undefined + ? "--" + : formatValue(summary.comparison_delta_pct, "%", 2), + }, + ]; + + return ( +
    + {tiles.map((tile) => ( + +
    {tile.label}
    +
    {tile.value}
    +
    + ))} +
    + ); +} diff --git a/frontend/src/components/common/Badge.tsx b/frontend/src/components/common/Badge.tsx new file mode 100644 index 0000000..dd0bf0b --- /dev/null +++ b/frontend/src/components/common/Badge.tsx @@ -0,0 +1,21 @@ +import clsx from "clsx"; +import type { PropsWithChildren } from "react"; + +interface BadgeProps extends PropsWithChildren { + tone?: "ok" | "warn" | "critical" | "neutral"; +} + +export function Badge({ tone = "neutral", children }: BadgeProps) { + const palette = { + ok: "border-emerald-400/30 bg-emerald-500/10 text-emerald-200", + warn: "border-amber-400/30 bg-amber-500/10 text-amber-200", + critical: "border-rose-400/30 bg-rose-500/10 text-rose-200", + neutral: "border-white/10 bg-white/5 text-slate-200", + }; + + return ( + + {children} + + ); +} diff --git a/frontend/src/components/common/Card.tsx b/frontend/src/components/common/Card.tsx new file mode 100644 index 0000000..01d7ed8 --- /dev/null +++ b/frontend/src/components/common/Card.tsx @@ -0,0 +1,31 @@ +import clsx from "clsx"; +import type { PropsWithChildren, ReactNode } from "react"; + +interface CardProps extends PropsWithChildren { + title?: string; + subtitle?: string; + action?: ReactNode; + className?: string; +} + +export function Card({ title, subtitle, action, className, children }: CardProps) { + return ( +
    + {(title || subtitle || action) && ( +
    +
    + {title &&

    {title}

    } + {subtitle &&

    {subtitle}

    } +
    + {action} +
    + )} + {children} +
    + ); +} diff --git a/frontend/src/components/common/EChart.tsx b/frontend/src/components/common/EChart.tsx new file mode 100644 index 0000000..2b23178 --- /dev/null +++ b/frontend/src/components/common/EChart.tsx @@ -0,0 +1,31 @@ +import { useEffect, useRef } from "react"; +import * as echarts from "echarts"; +import type { EChartsOption } from "echarts"; + +interface EChartProps { + option: EChartsOption; + className?: string; +} + +export function EChart({ option, className = "h-80 w-full" }: EChartProps) { + const ref = useRef(null); + + useEffect(() => { + if (!ref.current) { + return; + } + + const chart = echarts.init(ref.current); + chart.setOption(option); + + const observer = new ResizeObserver(() => chart.resize()); + observer.observe(ref.current); + + return () => { + observer.disconnect(); + chart.dispose(); + }; + }, [option]); + + return
    ; +} diff --git a/frontend/src/components/common/EmptyState.tsx b/frontend/src/components/common/EmptyState.tsx new file mode 100644 index 0000000..6e52ae0 --- /dev/null +++ b/frontend/src/components/common/EmptyState.tsx @@ -0,0 +1,13 @@ +interface EmptyStateProps { + title: string; + description: string; +} + +export function EmptyState({ title, description }: EmptyStateProps) { + return ( +
    +

    {title}

    +

    {description}

    +
    + ); +} diff --git a/frontend/src/components/common/Icons.tsx b/frontend/src/components/common/Icons.tsx new file mode 100644 index 0000000..cdf59ac --- /dev/null +++ b/frontend/src/components/common/Icons.tsx @@ -0,0 +1,84 @@ +import type { SVGProps } from "react"; + +type IconProps = SVGProps & { size?: number }; + +function BaseIcon({ size = 18, children, ...props }: IconProps) { + return ( + + ); +} + +export function IconBolt(props: IconProps) { + return ; +} +export function IconChartBar(props: IconProps) { + return ; +} +export function IconChecklist(props: IconProps) { + return ; +} +export function IconClockHour4(props: IconProps) { + return ; +} +export function IconDatabaseImport(props: IconProps) { + return ; +} +export function IconDeviceDesktop(props: IconProps) { + return ; +} +export function IconHistory(props: IconProps) { + return ; +} +export function IconLanguage(props: IconProps) { + return ; +} +export function IconLayoutDashboard(props: IconProps) { + return ; +} +export function IconLock(props: IconProps) { + return ; +} +export function IconLogin2(props: IconProps) { + return ; +} +export function IconLogout(props: IconProps) { + return ; +} +export function IconMoon(props: IconProps) { + return ; +} +export function IconPlayerPlay(props: IconProps) { + return ; +} +export function IconRefresh(props: IconProps) { + return ; +} +export function IconSettings(props: IconProps) { + return ; +} +export function IconSun(props: IconProps) { + return ; +} +export function IconTemperature(props: IconProps) { + return ; +} +export function IconX(props: IconProps) { + return ; +} +export function IconArrowsMove(props: IconProps) { + return ; +} diff --git a/frontend/src/components/common/ValuePair.tsx b/frontend/src/components/common/ValuePair.tsx new file mode 100644 index 0000000..0796bdc --- /dev/null +++ b/frontend/src/components/common/ValuePair.tsx @@ -0,0 +1,17 @@ +import { formatValue } from "../../lib/format"; +import type { MetricValue } from "../../types"; + +interface ValuePairProps { + metric?: MetricValue; +} + +export function ValuePair({ metric }: ValuePairProps) { + return ( +
    +
    {metric?.label ?? "--"}
    +
    + {metric ? formatValue(metric.value, metric.unit, metric.precision) : "--"} +
    +
    + ); +} diff --git a/frontend/src/components/layout/AppShell.tsx b/frontend/src/components/layout/AppShell.tsx new file mode 100644 index 0000000..2fda6a4 --- /dev/null +++ b/frontend/src/components/layout/AppShell.tsx @@ -0,0 +1,16 @@ +import type { PropsWithChildren, ReactNode } from "react"; + +interface AppShellProps extends PropsWithChildren { + header: ReactNode; +} + +export function AppShell({ header, children }: AppShellProps) { + return ( +
    +
    + {header} +
    {children}
    +
    +
    + ); +} diff --git a/frontend/src/components/layout/TopNav.tsx b/frontend/src/components/layout/TopNav.tsx new file mode 100644 index 0000000..00d57ff --- /dev/null +++ b/frontend/src/components/layout/TopNav.tsx @@ -0,0 +1,56 @@ +import clsx from "clsx"; +import { Badge } from "../common/Badge"; +import { formatDateTime } from "../../lib/format"; + +interface TopNavProps { + siteName: string; + connected: boolean; + lastUpdated?: string | null; + activeTab: string; + onTabChange: (tab: "realtime" | "analytics" | "settings") => void; +} + +const tabs = [ + { id: "realtime", label: "Na zywo" }, + { id: "analytics", label: "Analityka" }, + { id: "settings", label: "Konfiguracja" }, +] as const; + +export function TopNav({ siteName, connected, lastUpdated, activeTab, onTabChange }: TopNavProps) { + return ( +
    +
    +
    +

    PV Insight

    +

    {siteName}

    +

    Panel live + analityka liczona z surowych danych InfluxDB

    +
    + +
    + {connected ? "Live polling" : "Brak odpowiedzi API"} +
    + Ostatnia aktualizacja: {formatDateTime(lastUpdated)} +
    +
    +
    + + +
    + ); +} diff --git a/frontend/src/components/realtime/HeroKpiGrid.tsx b/frontend/src/components/realtime/HeroKpiGrid.tsx new file mode 100644 index 0000000..4414791 --- /dev/null +++ b/frontend/src/components/realtime/HeroKpiGrid.tsx @@ -0,0 +1,32 @@ +import clsx from "clsx"; +import { Card } from "../common/Card"; +import { formatValue } from "../../lib/format"; +import type { HeroCard } from "../../types"; + +interface HeroKpiGridProps { + cards: HeroCard[]; +} + +const accents: Record = { + emerald: "from-emerald-400/15 to-emerald-500/5 ring-emerald-300/20", + amber: "from-amber-400/15 to-amber-500/5 ring-amber-300/20", + rose: "from-rose-400/15 to-rose-500/5 ring-rose-300/20", + slate: "from-white/10 to-white/5 ring-white/10", +}; + +export function HeroKpiGrid({ cards }: HeroKpiGridProps) { + return ( +
    + {cards.map((card) => ( + +
    {card.label}
    +
    {formatValue(card.value, card.unit, 2)}
    +
    {card.subtitle}
    +
    + ))} +
    + ); +} diff --git a/frontend/src/components/realtime/KpiStrip.tsx b/frontend/src/components/realtime/KpiStrip.tsx new file mode 100644 index 0000000..70ebe0f --- /dev/null +++ b/frontend/src/components/realtime/KpiStrip.tsx @@ -0,0 +1,35 @@ +import { Card } from "../common/Card"; +import { formatValue } from "../../lib/format"; +import type { MetricValue } from "../../types"; + +interface KpiStripProps { + items: Record; +} + +const order = [ + "energy_today", + "energy_yesterday", + "today_vs_yesterday", + "dc_power_total", + "energy_total", +]; + +export function KpiStrip({ items }: KpiStripProps) { + return ( +
    + {order + .filter((metricId) => items[metricId]) + .map((metricId) => { + const metric = items[metricId]; + return ( + +
    {metric.label}
    +
    + {formatValue(metric.value, metric.unit, metric.precision)} +
    +
    + ); + })} +
    + ); +} diff --git a/frontend/src/components/realtime/LiveHistoryChart.tsx b/frontend/src/components/realtime/LiveHistoryChart.tsx new file mode 100644 index 0000000..2ede682 --- /dev/null +++ b/frontend/src/components/realtime/LiveHistoryChart.tsx @@ -0,0 +1,64 @@ +import type { EChartsOption } from "echarts"; +import { Card } from "../common/Card"; +import { EChart } from "../common/EChart"; +import type { HistoryPayload } from "../../types"; + +interface LiveHistoryChartProps { + history?: HistoryPayload; + title?: string; +} + +export function LiveHistoryChart({ history, title = "Dane chwilowe" }: LiveHistoryChartProps) { + const option: EChartsOption = { + tooltip: { + trigger: "axis", + backgroundColor: "rgba(2, 6, 23, 0.95)", + borderColor: "rgba(255,255,255,0.08)", + textStyle: { color: "#e2e8f0" }, + }, + legend: { + top: 0, + textStyle: { color: "#cbd5e1" }, + }, + grid: { + left: 18, + right: 16, + top: 46, + bottom: 24, + containLabel: true, + }, + xAxis: { + type: "category", + boundaryGap: false, + axisLabel: { color: "#94a3b8" }, + axisLine: { lineStyle: { color: "rgba(255,255,255,0.08)" } }, + data: history?.series[0]?.points.map((point) => + new Date(point.timestamp).toLocaleTimeString("pl-PL", { hour: "2-digit", minute: "2-digit" }) + ) ?? [], + }, + yAxis: { + type: "value", + axisLabel: { color: "#94a3b8" }, + splitLine: { lineStyle: { color: "rgba(255,255,255,0.06)" } }, + }, + series: + history?.series.map((item) => ({ + name: `${item.label} (${item.unit})`, + type: "line", + smooth: true, + showSymbol: false, + lineStyle: { width: 3 }, + areaStyle: { opacity: 0.08 }, + data: item.points.map((point) => point.value), + })) ?? [], + }; + + return ( + + + + ); +} diff --git a/frontend/src/components/realtime/LiveStatusBoard.tsx b/frontend/src/components/realtime/LiveStatusBoard.tsx new file mode 100644 index 0000000..f988cce --- /dev/null +++ b/frontend/src/components/realtime/LiveStatusBoard.tsx @@ -0,0 +1,30 @@ +import { Badge } from "../common/Badge"; +import { Card } from "../common/Card"; +import { formatValue } from "../../lib/format"; +import type { MetricValue } from "../../types"; + +interface LiveStatusBoardProps { + status: MetricValue[]; +} + +export function LiveStatusBoard({ status }: LiveStatusBoardProps) { + if (!status.length) { + return null; + } + + return ( + +
    + {status.map((metric) => ( +
    +
    +
    {metric.label}
    + {metric.status} +
    +
    {formatValue(metric.value, metric.unit, metric.precision)}
    +
    + ))} +
    +
    + ); +} diff --git a/frontend/src/components/realtime/PhaseGrid.tsx b/frontend/src/components/realtime/PhaseGrid.tsx new file mode 100644 index 0000000..3ac2a60 --- /dev/null +++ b/frontend/src/components/realtime/PhaseGrid.tsx @@ -0,0 +1,26 @@ +import { Card } from "../common/Card"; +import { ValuePair } from "../common/ValuePair"; +import type { SnapshotGroupRow } from "../../types"; + +interface PhaseGridProps { + rows: SnapshotGroupRow[]; +} + +export function PhaseGrid({ rows }: PhaseGridProps) { + return ( + +
    + {rows.map((row) => ( +
    +
    {row.label}
    +
    + + + +
    +
    + ))} +
    +
    + ); +} diff --git a/frontend/src/components/realtime/StringGrid.tsx b/frontend/src/components/realtime/StringGrid.tsx new file mode 100644 index 0000000..7a0a3a8 --- /dev/null +++ b/frontend/src/components/realtime/StringGrid.tsx @@ -0,0 +1,34 @@ +import { Card } from "../common/Card"; +import { ValuePair } from "../common/ValuePair"; +import type { SnapshotGroupRow } from "../../types"; + +interface StringGridProps { + rows: SnapshotGroupRow[]; +} + +const slotOrder = ["power", "voltage"] as const; + +export function StringGrid({ rows }: StringGridProps) { + return ( + +
    + {rows.map((row) => { + const visibleSlots = slotOrder.filter((slot) => row.values[slot]); + return ( +
    +
    +
    {row.label}
    +
    {row.id}
    +
    +
    1 ? "sm:grid-cols-2" : "sm:grid-cols-1"}`}> + {visibleSlots.map((slot) => ( + + ))} +
    +
    + ); + })} +
    +
    + ); +} diff --git a/frontend/src/components/settings/ConfigPanel.tsx b/frontend/src/components/settings/ConfigPanel.tsx new file mode 100644 index 0000000..ab8f493 --- /dev/null +++ b/frontend/src/components/settings/ConfigPanel.tsx @@ -0,0 +1,82 @@ +import { Card } from "../common/Card"; +import type { DashboardConfig } from "../../types"; +import { HistoricalImportPanel } from "./HistoricalImportPanel"; + +interface ConfigPanelProps { + config: DashboardConfig; +} + +export function ConfigPanel({ config }: ConfigPanelProps) { + return ( +
    + +
    +
    +
    Backend
    +

    Flask + modularne serwisy, bez uvicorna i bez pydantic-core. Odczyt z InfluxDB idzie po HTTP API, a agregaty historyczne trafiaja do lokalnego cache SQLite.

    +
    +
    +
    Frontend
    +

    React + TypeScript + Vite + Tailwind, responsywne karty, live charts i widok mobilny bez osobnej wersji aplikacji.

    +
    +
    +
    Logika danych
    +

    Produkcja dzienna, tygodniowa, miesieczna i roczna jest liczona z surowych danych Influxa na podstawie licznika energii calkowitej, a gdy go brak — z mocy AC. Pelne dni sa cache'owane lokalnie.

    +
    +
    +
    + + +
    +
    +
    Site name
    +
    {config.app.site_name}
    +
    +
    +
    Installed power
    +
    {config.app.installed_power_kwp} kWp
    +
    +
    +
    Timezone
    +
    {config.app.timezone}
    +
    +
    +
    Moduly live / analytics / history
    +
    + live: {String(config.capabilities.realtime_enabled)} / analytics: {String(config.capabilities.analytics_enabled)} / history: {String(config.capabilities.historical_import_enabled)} +
    +
    +
    +
    + + {config.capabilities.historical_import_enabled ? : null} + + +
    + + + + + + + + + + + + {config.visible_entities.map((item) => ( + + + + + + + + ))} + +
    MetricLabelEntity IDMeasurementUnit
    {item.metric_id}{item.label}{item.entity_id}{item.measurement}{item.unit}
    +
    +
    +
    + ); +} diff --git a/frontend/src/components/settings/HistoricalImportPanel.tsx b/frontend/src/components/settings/HistoricalImportPanel.tsx new file mode 100644 index 0000000..1798c97 --- /dev/null +++ b/frontend/src/components/settings/HistoricalImportPanel.tsx @@ -0,0 +1,74 @@ +import { useEffect, useMemo, useState } from "react"; +import { useHistoricalImport } from "../../hooks"; +import type { DashboardConfig, HistoricalActivityEvent, HistoricalChunkProgress } from "../../types"; +import { Badge } from "../common/Badge"; +import { Card } from "../common/Card"; +import { formatDate, formatDateTime, formatDurationShort, formatPercent, formatValue } from "../../lib/format"; + +interface HistoricalImportPanelProps { config: DashboardConfig; } + +function eventTone(level?: string): "ok" | "warn" | "critical" | "neutral" { if (level === "success") return "ok"; if (level === "warn") return "warn"; if (level === "error") return "critical"; return "neutral"; } +function chunkTone(state?: string): "ok" | "warn" | "critical" | "neutral" { if (state === "completed") return "ok"; if (state === "running") return "warn"; if (state === "failed") return "critical"; return "neutral"; } + +function StatCard({ label, value, helper }: { label: string; value: string; helper: string }) { + return
    {label}
    {value}
    {helper}
    ; +} + +function ChunkRow({ chunk, activeChunkIndex }: { chunk: HistoricalChunkProgress; activeChunkIndex: number }) { + const isActive = chunk.chunk_index === activeChunkIndex || chunk.state === "running"; + return ( +
    +
    Chunk {chunk.chunk_index}/{chunk.total_chunks}
    {formatDate(chunk.start_date)} - {formatDate(chunk.end_date)}
    {isActive ? aktywny : null}{chunk.state}
    +
    Przetworzone
    {chunk.processed_days}
    Import
    {chunk.imported_days}
    Pominiete
    {chunk.skipped_days}
    Energia
    {formatValue(chunk.energy_kwh, "kWh", 2)}
    +
    {chunk.note}{chunk.duration_seconds ? `czas ${formatDurationShort(chunk.duration_seconds)}` : "w toku"}
    +
    + ); +} + +function EventRow({ event }: { event: HistoricalActivityEvent }) { + return ( +
    {event.level}
    {event.title}
    {formatDateTime(event.timestamp)}
    {event.message}
    {event.day ? Dzien: {formatDate(event.day)} : null}{event.chunk_index ? Chunk: #{event.chunk_index} : null}
    + ); +} + +export function HistoricalImportPanel({ config }: HistoricalImportPanelProps) { + const { status, start, syncNow, cancel } = useHistoricalImport(); + const payload = status.data; + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); + const [chunkDays, setChunkDays] = useState(String(config.capabilities.history.default_chunk_days || 7)); + const [force, setForce] = useState(false); + + useEffect(() => { setChunkDays(String(config.capabilities.history.default_chunk_days || 7)); }, [config.capabilities.history.default_chunk_days]); + + const progress = useMemo(() => { if (!payload || payload.total_days <= 0) return 0; return Math.min(100, Math.round((payload.processed_days / payload.total_days) * 100)); }, [payload]); + const visibleChunks = useMemo(() => [...(payload?.recent_chunks ?? [])].sort((l, r) => r.chunk_index - l.chunk_index), [payload?.recent_chunks]); + const visibleEvents = useMemo(() => [...(payload?.recent_events ?? [])].sort((l, r) => new Date(r.timestamp).getTime() - new Date(l.timestamp).getTime()), [payload?.recent_events]); + + const busy = start.isPending || syncNow.isPending || cancel.isPending; + const mutationError = start.error?.message || syncNow.error?.message || cancel.error?.message || null; + const availableRangeReady = Boolean(payload?.available_start_date && payload?.available_end_date); + + return ( + +
    +
    + + + + +
    +
    +
    +

    Gdy pola dat sa puste, backend sam wykryje zakres do importu na podstawie pierwszej probki w InfluxDB i ostatniego dnia juz zapisanego w SQLite.

    +
    {availableRangeReady ? : null}
    + {mutationError ?
    {mutationError}
    : null} + {status.error ?
    {status.error.message}
    : null} +
    +
    Operacyjny status zadania
    {payload?.message || "Brak aktywnego zadania"}
    {payload?.job_id ? job {payload.job_id} : null}{payload?.running ? "w trakcie" : payload?.state || "idle"}
    Chunk {payload?.active_chunk_index ?? 0} / {payload?.total_chunks ?? 0}{progress}%
    +
    Dostepny zakres w InfluxDB
    {formatDate(payload?.available_start_date)} - {formatDate(payload?.available_end_date)}
    {payload?.coverage?.available_days ?? 0} dni wykrytego archiwum
    Zakres zapisany lokalnie
    {formatDate(payload?.coverage?.first_day)} - {formatDate(payload?.coverage?.last_day)}
    {payload?.coverage?.imported_days ?? 0} dni w cache, {formatValue(payload?.coverage?.total_energy_kwh ?? 0, "kWh", 1)}
    Aktualny chunk
    {formatDate(payload?.current_chunk_start)} - {formatDate(payload?.current_chunk_end)}
    Ostatni dzien: {formatDate(payload?.current_date)}
    Czasy i opoznienia
    elapsed {formatDurationShort(payload?.elapsed_seconds)}
    start {formatDateTime(payload?.started_at)} / koniec {formatDateTime(payload?.finished_at)}
    {payload?.last_error ?
    Blad: {payload.last_error}
    : null}
    +
    {visibleChunks.length ? visibleChunks.map((chunk) => ) :
    Lista chunkow pojawi sie po uruchomieniu pierwszego backfillu.
    }
    {visibleEvents.length ? visibleEvents.map((event, index) => ) :
    Historia operacji pojawi sie po starcie zadania.
    }
    +
    + + ); +} diff --git a/frontend/src/components/status/FaultBanner.tsx b/frontend/src/components/status/FaultBanner.tsx new file mode 100644 index 0000000..64a7af6 --- /dev/null +++ b/frontend/src/components/status/FaultBanner.tsx @@ -0,0 +1,24 @@ +import { Card } from "../common/Card"; + +interface FaultBannerProps { + faults: string[]; +} + +export function FaultBanner({ faults }: FaultBannerProps) { + if (!faults.length) { + return null; + } + + return ( + +
    +
    Alarm falownika
    + {faults.map((fault) => ( +
    + {fault} +
    + ))} +
    +
    + ); +} diff --git a/frontend/src/demo/data.ts b/frontend/src/demo/data.ts new file mode 100644 index 0000000..5d44bfa --- /dev/null +++ b/frontend/src/demo/data.ts @@ -0,0 +1,187 @@ +import type { + AnalyticsPayload, + DashboardConfig, + DistributionPayload, + HistoryPayload, + HistoricalStatus, + SnapshotPayload, +} from "../types"; + +function isoAt(offsetMinutes: number): string { + return new Date(Date.now() + offsetMinutes * 60 * 1000).toISOString(); +} + +export const demoConfig: DashboardConfig = { + app: { + name: "pv-insight", + version: "1.3.0", + site_name: "PV Insight / Sofar Demo", + timezone: "Europe/Warsaw", + installed_power_kwp: 9.86, + }, + defaults: { + realtime_range: "6h", + analytics_range: "30d", + analytics_bucket: "day", + tab: "realtime", + theme: "dark", + language: "pl", + }, + auth: { enabled: false }, + i18n: { default_language: "pl", supported_languages: ["pl", "en"] }, + capabilities: { + modules: { realtime: true, analytics: true, history: true }, + strings_enabled: true, + strings_count: 2, + phases_enabled: false, + phases_count: 0, + analytics_enabled: true, + realtime_enabled: true, + comparison_modes: ["none", "previous_period", "previous_year"], + ranges: [ + { key: "6h", label: "6h" }, + { key: "24h", label: "24h" }, + { key: "1d", label: "1 dzień" }, + { key: "3d", label: "3 dni" }, + { key: "7d", label: "7 dni" }, + { key: "14d", label: "14 dni" }, + { key: "30d", label: "30 dni" }, + { key: "60d", label: "60 dni" }, + { key: "365d", label: "365 dni" }, + { key: "ytd", label: "YTD" }, + ], + buckets: [ + { key: "day", label: "Dzien" }, + { key: "week", label: "Tydzien" }, + { key: "month", label: "Miesiac" }, + { key: "year", label: "Rok" }, + ], + historical_import_enabled: true, + history: { enabled: true, default_chunk_days: 7, auto_sync_enabled: true, auto_sync_interval_minutes: 30 }, + }, + visible_entities: [ + { metric_id: "ac_power", label: "Moc AC calkowita", entity_id: "sofarsolar_ac_power", measurement: "W", unit: "W", kind: "gauge" }, + { metric_id: "energy_total", label: "Energia total", entity_id: "sofarsolar_energy_total", measurement: "kWh", unit: "kWh", kind: "counter" }, + { metric_id: "string_1_power", label: "Moc stringu DC1", entity_id: "sofarsolar_dc1_power", measurement: "W", unit: "W", kind: "gauge" }, + { metric_id: "string_1_voltage", label: "Napiecie stringu DC1", entity_id: "sofarsolar_dc1_voltage", measurement: "V", unit: "V", kind: "gauge" }, + { metric_id: "string_2_power", label: "Moc stringu DC2", entity_id: "sofarsolar_dc2_power", measurement: "W", unit: "W", kind: "gauge" }, + { metric_id: "string_2_voltage", label: "Napiecie stringu DC2", entity_id: "sofarsolar_dc2_voltage", measurement: "V", unit: "V", kind: "gauge" }, + { metric_id: "inverter_temperature", label: "Temperatura falownika", entity_id: "sofarsolar_temprature_inverter", measurement: "°C", unit: "°C", kind: "gauge" }, + ], +}; + +export const demoSnapshot = (): SnapshotPayload => ({ + updated_at: new Date().toISOString(), + hero_cards: [ + { metric_id: "ac_power", label: "Produkcja AC", value: 6840, unit: "W", accent: "emerald", subtitle: "Aktualna moc oddawana przez falownik" }, + { metric_id: "energy_today", label: "Dzisiaj", value: 31.8, unit: "kWh", accent: "amber", subtitle: "Liczone z energy_total / fallback z AC power" }, + { metric_id: "dc1_power", label: "String DC1", value: 3450, unit: "W", accent: "emerald", subtitle: "Wschod" }, + { metric_id: "dc2_power", label: "String DC2", value: 3310, unit: "W", accent: "emerald", subtitle: "Zachod" }, + { metric_id: "inverter_temperature", label: "Temp. falownika", value: 47.3, unit: "°C", accent: "rose", subtitle: "Live status termiczny" }, + ], + kpis: { + energy_today: { metric_id: "energy_today", label: "Energia dzis", unit: "kWh", value: 31.8, precision: 2, kind: "counter", status: "ok" }, + energy_yesterday: { metric_id: "energy_yesterday", label: "Energia wczoraj", unit: "kWh", value: 28.4, precision: 2, kind: "counter", status: "ok" }, + today_vs_yesterday: { metric_id: "today_vs_yesterday", label: "Dzis vs wczoraj", unit: "%", value: 12, precision: 0, kind: "gauge", status: "ok" }, + dc_power_total: { metric_id: "dc_power_total", label: "Moc DC total", unit: "W", value: 6760, precision: 0, kind: "gauge", status: "ok" }, + energy_total: { metric_id: "energy_total", label: "Energia total", unit: "kWh", value: 18264.3, precision: 1, kind: "counter", status: "ok" }, + }, + strings: [ + { id: "dc1", label: "String 1 / Wschod", meta: {}, values: { power: { metric_id: "string_1_power", label: "Moc", unit: "W", value: 3450, precision: 0, kind: "gauge", status: "ok" }, voltage: { metric_id: "string_1_voltage", label: "Napiecie", unit: "V", value: 382, precision: 0, kind: "gauge", status: "ok" } } }, + { id: "dc2", label: "String 2 / Zachod", meta: {}, values: { power: { metric_id: "string_2_power", label: "Moc", unit: "W", value: 3310, precision: 0, kind: "gauge", status: "ok" }, voltage: { metric_id: "string_2_voltage", label: "Napiecie", unit: "V", value: 375, precision: 0, kind: "gauge", status: "ok" } } }, + ], + phases: [], + status: [ + { metric_id: "inverter_temperature", label: "Temperatura falownika", unit: "°C", value: 47.3, precision: 1, kind: "gauge", status: "ok" }, + { metric_id: "data_freshness", label: "Swiezosc danych", unit: "", value: "3 s temu", precision: 0, kind: "text", status: "ok" }, + ], + faults: [], +}); + +function historyPoints(values: number[]) { + return values.map((value, index) => ({ timestamp: isoAt(-(values.length - index) * 5), value })); +} + +export const demoHistory: HistoryPayload = { + range_key: "6h", + start: isoAt(-360), + end: isoAt(0), + series: [ + { metric_id: "ac_power", label: "Moc AC", unit: "W", points: historyPoints([0, 120, 860, 1840, 2760, 3920, 5180, 6020, 6840, 6500, 5710, 4980]) }, + { metric_id: "dc1_power", label: "DC1", unit: "W", points: historyPoints([0, 80, 620, 1320, 2140, 2860, 3250, 3490, 3450, 3300, 2920, 2480]) }, + { metric_id: "dc2_power", label: "DC2", unit: "W", points: historyPoints([0, 40, 240, 520, 880, 1260, 1930, 2530, 3310, 3200, 2790, 2410]) }, + { metric_id: "inverter_temperature", label: "Temp. falownika", unit: "°C", points: historyPoints([22, 24, 27, 31, 35, 39, 42, 45, 47.3, 46.8, 44.1, 41.2]) }, + ], +}; + +const currentBars = [18.3, 22.1, 25.7, 21.9, 23.2, 27.6, 30.1, 28.4, 19.8, 16.2, 24.4, 31.8].map((value, index) => ({ label: `${index + 1} mar`, start: isoAt(-(12 - index) * 1440), end: isoAt(-(11 - index) * 1440), value })); +const comparisonBars = [16.1, 19.2, 22.4, 19.5, 20.2, 23.4, 26.8, 25.1, 17.9, 15.4, 21.6, 27.3].map((value, index) => ({ label: `${index + 1} mar`, start: isoAt(-(377 - index) * 1440), end: isoAt(-(376 - index) * 1440), value })); + +export const demoAnalytics: AnalyticsPayload = { + unit: "kWh", + bucket: "day", + compare_mode: "previous_year", + current: currentBars, + comparison: comparisonBars, + summary: { total: 289.5, unit: "kWh", average_bucket: 24.1, best_bucket_label: "12 mar", best_bucket_value: 31.8, co2_saved_kg: 230.1, comparison_total: 255, comparison_delta_pct: 13.5 }, + meta: { window: { start: isoAt(-30 * 1440), end: isoAt(0), range_key: "30d" }, source: "sqlite+influx" }, +}; + +export const demoDistribution: DistributionPayload = { + unit: "kWh", + bucket: "day", + total: 289.5, + slices: [ { label: "DC1 / Wschod", value: 154.2, share: 53.3 }, { label: "DC2 / Zachod", value: 135.3, share: 46.7 } ], +}; + +export const demoHistoricalStatus: HistoricalStatus = { + enabled: true, + running: true, + state: "running", + job_id: "hist-9f31ab", + started_at: isoAt(-18), + finished_at: null, + requested_start_date: "2022-01-01", + requested_end_date: "2025-12-31", + total_days: 1461, + processed_days: 1096, + imported_days: 1028, + skipped_days: 68, + chunk_days: 7, + total_chunks: 209, + active_chunk_index: 157, + current_date: "2025-01-08", + current_chunk_start: "2025-01-05", + current_chunk_end: "2025-01-11", + elapsed_seconds: 1080, + estimated_remaining_seconds: 360, + avg_days_per_minute: 60.89, + last_error: null, + message: "Przetwarzanie zakresu 2025-01-05 -> 2025-01-11", + coverage: { imported_days: 1028, first_day: "2022-01-01", last_day: "2025-01-04", total_energy_kwh: 18264.3, available_days: 1461, missing_days: 433, coverage_pct: 70.4 }, + available_start_date: "2022-01-01", + available_end_date: "2025-12-31", + default_chunk_days: 7, + recent_chunks: [ + { chunk_index: 154, total_chunks: 209, start_date: "2024-12-15", end_date: "2024-12-21", processed_days: 7, imported_days: 7, skipped_days: 0, energy_kwh: 96.3, state: "completed", started_at: isoAt(-9.8), finished_at: isoAt(-9.1), duration_seconds: 42, note: "Chunk zakonczony: import 7, pominiete 0" }, + { chunk_index: 155, total_chunks: 209, start_date: "2024-12-22", end_date: "2024-12-28", processed_days: 7, imported_days: 7, skipped_days: 0, energy_kwh: 88.7, state: "completed", started_at: isoAt(-9.0), finished_at: isoAt(-8.2), duration_seconds: 46, note: "Chunk zakonczony: import 7, pominiete 0" }, + { chunk_index: 156, total_chunks: 209, start_date: "2024-12-29", end_date: "2025-01-04", processed_days: 7, imported_days: 7, skipped_days: 0, energy_kwh: 92.6, state: "completed", started_at: isoAt(-8.1), finished_at: isoAt(-7.4), duration_seconds: 44, note: "Chunk zakonczony: import 7, pominiete 0" }, + { chunk_index: 157, total_chunks: 209, start_date: "2025-01-05", end_date: "2025-01-11", processed_days: 4, imported_days: 4, skipped_days: 0, energy_kwh: 51.4, state: "running", started_at: isoAt(-6.5), finished_at: null, duration_seconds: null, note: "Aktywny chunk 2025-01-05 -> 2025-01-11" }, + ], + recent_events: [ + { timestamp: isoAt(-1.1), level: "success", title: "Zaimportowano dzien", message: "Zaimportowano 2025-01-08 (13.10 kWh). Energia: 13.10 kWh.", day: "2025-01-08", chunk_index: 157 }, + { timestamp: isoAt(-1.9), level: "success", title: "Zaimportowano dzien", message: "Zaimportowano 2025-01-07 (12.84 kWh). Energia: 12.84 kWh.", day: "2025-01-07", chunk_index: 157 }, + { timestamp: isoAt(-2.5), level: "success", title: "Zaimportowano dzien", message: "Zaimportowano 2025-01-06 (12.56 kWh). Energia: 12.56 kWh.", day: "2025-01-06", chunk_index: 157 }, + { timestamp: isoAt(-3.2), level: "success", title: "Zaimportowano dzien", message: "Zaimportowano 2025-01-05 (12.90 kWh). Energia: 12.90 kWh.", day: "2025-01-05", chunk_index: 157 }, + { timestamp: isoAt(-3.7), level: "info", title: "Chunk 157/209", message: "Start zakresu 2025-01-05 -> 2025-01-11", day: null, chunk_index: 157 }, + { timestamp: isoAt(-6.8), level: "success", title: "Chunk 156/209 zakonczony", message: "Zakres 2024-12-29 -> 2025-01-04, import 7, pominiete 0, energia 92.60 kWh", day: null, chunk_index: 156 }, + ], +}; + + +export const demoAuthStatus = { + enabled: false, + authenticated: true, + user: "demo", + display_name: "Demo Operator", +}; diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts new file mode 100644 index 0000000..cce4e30 --- /dev/null +++ b/frontend/src/hooks/index.ts @@ -0,0 +1,5 @@ +export * from "./useAnalytics"; +export * from "./useDashboardConfig"; +export * from "./useHistoricalImport"; +export * from "./useRealtimeHistory"; +export * from "./useRealtimeSocket"; diff --git a/frontend/src/hooks/useAnalytics.ts b/frontend/src/hooks/useAnalytics.ts new file mode 100644 index 0000000..723ab78 --- /dev/null +++ b/frontend/src/hooks/useAnalytics.ts @@ -0,0 +1,26 @@ +import { useQuery } from "@tanstack/react-query"; +import { api } from "../api/client"; + +export function useAnalytics( + rangeKey: string, + bucket: string, + compare: string, + enabled = true, + options?: { start?: string; end?: string; publicKiosk?: boolean; compareRanges?: Array<{ start: string; end: string; label: string; key?: string }> }, +) { + const production = useQuery({ + queryKey: ["analytics", rangeKey, bucket, compare, options?.start, options?.end, options?.publicKiosk, JSON.stringify(options?.compareRanges ?? [])], + queryFn: () => api.getAnalytics(rangeKey, bucket, compare, options), + staleTime: 60 * 1000, + enabled, + }); + + const distribution = useQuery({ + queryKey: ["distribution", rangeKey, bucket, options?.start, options?.end, options?.publicKiosk], + queryFn: () => api.getDistribution(rangeKey, bucket, options), + staleTime: 60 * 1000, + enabled, + }); + + return { production, distribution }; +} diff --git a/frontend/src/hooks/useDashboardConfig.ts b/frontend/src/hooks/useDashboardConfig.ts new file mode 100644 index 0000000..1ea6491 --- /dev/null +++ b/frontend/src/hooks/useDashboardConfig.ts @@ -0,0 +1,11 @@ +import { useQuery } from "@tanstack/react-query"; +import { api } from "../api/client"; + +export function useDashboardConfig(enabled = true) { + return useQuery({ + queryKey: ["dashboard-config"], + queryFn: api.getConfig, + staleTime: 5 * 60 * 1000, + enabled, + }); +} diff --git a/frontend/src/hooks/useHistoricalImport.ts b/frontend/src/hooks/useHistoricalImport.ts new file mode 100644 index 0000000..dd1d0a7 --- /dev/null +++ b/frontend/src/hooks/useHistoricalImport.ts @@ -0,0 +1,40 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { api } from "../api/client"; +import type { HistoricalStartPayload } from "../types"; + +export function useHistoricalImport(enabled = true) { + const queryClient = useQueryClient(); + + const status = useQuery({ + queryKey: ["historical-status"], + queryFn: api.getHistoricalStatus, + staleTime: 5 * 1000, + enabled, + refetchInterval: (query) => (query.state.data?.running ? 3000 : 15000), + }); + + const start = useMutation({ + mutationFn: (payload: HistoricalStartPayload) => api.startHistoricalImport(payload), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["historical-status"] }); + await queryClient.invalidateQueries({ queryKey: ["analytics"] }); + await queryClient.invalidateQueries({ queryKey: ["distribution"] }); + }, + }); + + const syncNow = useMutation({ + mutationFn: api.syncHistoricalNow, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["historical-status"] }); + }, + }); + + const cancel = useMutation({ + mutationFn: api.cancelHistoricalImport, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["historical-status"] }); + }, + }); + + return { status, start, syncNow, cancel }; +} diff --git a/frontend/src/hooks/useRealtimeHistory.ts b/frontend/src/hooks/useRealtimeHistory.ts new file mode 100644 index 0000000..450d146 --- /dev/null +++ b/frontend/src/hooks/useRealtimeHistory.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@tanstack/react-query"; +import { api } from "../api/client"; + +export function useRealtimeHistory( + rangeKey: string, + enabled = true, + options?: { start?: string; end?: string; metrics?: string[]; publicKiosk?: boolean }, +) { + return useQuery({ + queryKey: ["realtime-history", rangeKey, options?.start, options?.end, options?.metrics?.join(","), options?.publicKiosk], + queryFn: () => api.getRealtimeHistory(rangeKey, options), + staleTime: 20 * 1000, + refetchInterval: options?.start || options?.end ? false : 30 * 1000, + enabled, + }); +} diff --git a/frontend/src/hooks/useRealtimeSocket.ts b/frontend/src/hooks/useRealtimeSocket.ts new file mode 100644 index 0000000..80760e2 --- /dev/null +++ b/frontend/src/hooks/useRealtimeSocket.ts @@ -0,0 +1,66 @@ +import { useEffect, useMemo, useState } from "react"; +import { api } from "../api/client"; +import type { SnapshotPayload } from "../types"; + +const EMPTY_SNAPSHOT: SnapshotPayload = { + hero_cards: [], + kpis: {}, + strings: [], + phases: [], + status: [], + faults: [], +}; + +const POLL_MS = Number(import.meta.env.VITE_LIVE_POLL_MS ?? 8000); + +export function useRealtimeSocket(enabled = true) { + const [snapshot, setSnapshot] = useState(EMPTY_SNAPSHOT); + const [connected, setConnected] = useState(false); + + useEffect(() => { + if (!enabled) { + setSnapshot(EMPTY_SNAPSHOT); + setConnected(false); + return; + } + + let isActive = true; + let timer: number | null = null; + + const poll = async () => { + try { + const data = await api.getRealtimeSnapshot(); + if (!isActive) { + return; + } + setSnapshot(data); + setConnected(true); + } catch { + if (isActive) { + setConnected(false); + } + } finally { + if (isActive) { + timer = window.setTimeout(poll, POLL_MS); + } + } + }; + + void poll(); + + return () => { + isActive = false; + if (timer !== null) { + window.clearTimeout(timer); + } + }; + }, [enabled]); + + const lastUpdated = useMemo(() => snapshot.updated_at ?? null, [snapshot.updated_at]); + + return { + snapshot, + connected, + lastUpdated, + }; +} diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts new file mode 100644 index 0000000..290288f --- /dev/null +++ b/frontend/src/i18n.ts @@ -0,0 +1,270 @@ +export type Language = "pl" | "en"; + +const messages = { + pl: { + live: "Live", + analytics: "Analityka", + settings: "Ustawienia", + kiosk: "Kiosk", + operatorPanel: "Panel operatora", + poweredBy: "Tabler UI", + connected: "Połączono", + disconnected: "Brak połączenia", + updatedAt: "Aktualizacja", + loginTitle: "Logowanie", + loginSubtitle: "Minitoring fotowoltaiki", + username: "Login", + password: "Hasło", + signIn: "Zaloguj", + signOut: "Wyloguj", + theme: "Motyw", + light: "Jasny", + dark: "Ciemny", + language: "Język", + polish: "Polski", + english: "English", + openKiosk: "Otwórz kiosk", + exitKiosk: "Wyjdź z kiosku", + fullscreen: "Pełny ekran", + demoMode: "Tryb demo", + realtimeOverview: "Przegląd na żywo", + realtimeSubtitle: "Najważniejsze parametry falownika i stringów", + analyticsOverview: "Produkcja długoterminowa", + analyticsSubtitle: "Dzień / tydzień / miesiąc / rok i porównania okresów", + settingsSubtitle: "Import archiwum, wygląd, kiosk, bezpieczeństwo", + noData: "Brak danych", + noDataDescription: "Brak odpowiedzi z backendu lub InfluxDB.", + chartPowerHistory: "Historia mocy i temperatury", + chartProduction: "Produkcja", + chartProductionSubtitle: "Agregacja w wybranym bucketcie", + chartComparison: "Porównanie okresów", + chartDistribution: "Rozkład produkcji", + currentPeriod: "Bieżący okres", + comparisonPeriod: "Porównanie", + summaryTotal: "Suma", + summaryAverage: "Średnia / bucket", + summaryBest: "Najlepszy bucket", + summaryCo2: "Oszczędzone CO₂", + compareNone: "Bez porównania", + comparePreviousPeriod: "Poprzedni okres", + comparePreviousYear: "Poprzedni rok", + comparePreviousYear2: "2 lata wstecz", + comparePreviousYear3: "3 lata wstecz", + comparePreviousMonth12: "12 miesięcy wstecz", + comparePreviousMonth24: "24 miesiące wstecz", + compareCustomMulti: "Własne zakresy", + range: "Zakres", + bucket: "Bucket", + liveRange: "Zakres live", + systemStatus: "Status systemu", + strings: "Stringi DC", + quickMetrics: "Szybkie metryki", + importArchive: "Import archiwalny z InfluxDB", + importArchiveSubtitle: "Backfill chunkami i auto-sync brakujących dni", + startDate: "Data od", + endDate: "Data do", + chunkDays: "Chunk (dni)", + startImport: "Start importu", + syncMissing: "Synchronizuj brakujące", + cancel: "Anuluj", + status: "Status", + coverage: "Pokrycie", + importedDays: "Zaimportowane dni", + missingDays: "Brakujące dni", + throughput: "Przepustowość", + eta: "ETA", + activeChunk: "Aktywny chunk", + recentChunks: "Ostatnie chunki", + recentEvents: "Ostatnie zdarzenia", + kioskLayout: "Układ kiosku", + kioskLayoutSubtitle: "Wybierz widżety i kolejność widoku kiosku", + saveLayout: "Układ zapisuje się automatycznie", + kioskHint: "Tryb kiosku.", + widgetSelect: "Widżety", + moveUp: "W górę", + moveDown: "W dół", + selected: "Wybrane", + available: "Dostępne", + security: "Bezpieczeństwo", + authEnabled: "Logowanie aktywne", + authDisabled: "Logowanie wyłączone", + changePasswordHint: "Zmiana loginu i hasła odbywa się przez .env backendu.", + authRequired: "Wymagane logowanie", + loginError: "Nie udało się zalogować.", + loading: "Ładowanie", + inverterTemp: "Temperatura falownika", + acPower: "Moc AC", + energyTotal: "Energia łączna", + energyToday: "Energia dziś", + energyYesterday: "Energia wczoraj", + todayVsYesterday: "Dziś vs wczoraj", + dcPowerTotal: "Moc DC łącznie", + unknown: "Nieznane", + yes: "Tak", + no: "Nie", + setFullHistory: "Ustaw pełną historię", + viewMode: "Tryb widoku", + normalMode: "Normalny", + kioskMode: "Kiosk", + simplifiedCards: "Karty uproszczone", + }, + en: { + live: "Live", + analytics: "Analytics", + settings: "Settings", + kiosk: "Kiosk", + operatorPanel: "Operator panel", + poweredBy: "Tabler UI", + connected: "Connected", + disconnected: "Disconnected", + updatedAt: "Updated", + loginTitle: "Sign in", + loginSubtitle: "PV monitoring", + username: "Username", + password: "Password", + signIn: "Sign in", + signOut: "Sign out", + theme: "Theme", + light: "Light", + dark: "Dark", + language: "Language", + polish: "Polski", + english: "English", + openKiosk: "Open kiosk", + exitKiosk: "Exit kiosk", + fullscreen: "Fullscreen", + demoMode: "Demo mode", + realtimeOverview: "Live overview", + realtimeSubtitle: "Key inverter and string metrics", + analyticsOverview: "Long-term production", + analyticsSubtitle: "Day / week / month / year and period comparisons", + settingsSubtitle: "Archive import, appearance, kiosk, security", + noData: "No data", + noDataDescription: "No response from backend or InfluxDB.", + chartPowerHistory: "Power and temperature history", + chartProduction: "Production", + chartProductionSubtitle: "Aggregated by selected bucket", + chartComparison: "Period comparison", + chartDistribution: "Production distribution", + currentPeriod: "Current period", + comparisonPeriod: "Comparison", + summaryTotal: "Total", + summaryAverage: "Average / bucket", + summaryBest: "Best bucket", + summaryCo2: "CO₂ saved", + compareNone: "No comparison", + comparePreviousPeriod: "Previous period", + comparePreviousYear: "Previous year", + comparePreviousYear2: "2 years back", + comparePreviousYear3: "3 years back", + comparePreviousMonth12: "12 months back", + comparePreviousMonth24: "24 months back", + compareCustomMulti: "Custom ranges", + range: "Range", + bucket: "Bucket", + liveRange: "Live range", + systemStatus: "System status", + strings: "DC strings", + quickMetrics: "Quick metrics", + importArchive: "Historical import from InfluxDB", + importArchiveSubtitle: "Chunked backfill and auto-sync for missing days", + startDate: "Start date", + endDate: "End date", + chunkDays: "Chunk (days)", + startImport: "Start import", + syncMissing: "Sync missing", + cancel: "Cancel", + status: "Status", + coverage: "Coverage", + importedDays: "Imported days", + missingDays: "Missing days", + throughput: "Throughput", + eta: "ETA", + activeChunk: "Active chunk", + recentChunks: "Recent chunks", + recentEvents: "Recent events", + kioskLayout: "Kiosk layout", + kioskLayoutSubtitle: "Choose widgets and their order for kiosk view", + saveLayout: "Layout is saved automatically", + kioskHint: "Kiosk mode.", + widgetSelect: "Widgets", + moveUp: "Move up", + moveDown: "Move down", + selected: "Selected", + available: "Available", + security: "Security", + authEnabled: "Login enabled", + authDisabled: "Login disabled", + changePasswordHint: "Change username and password in backend .env.", + authRequired: "Authentication required", + loginError: "Sign in failed.", + loading: "Loading", + inverterTemp: "Inverter temperature", + acPower: "AC power", + energyTotal: "Total energy", + energyToday: "Energy today", + energyYesterday: "Energy yesterday", + todayVsYesterday: "Today vs yesterday", + dcPowerTotal: "Total DC power", + unknown: "Unknown", + yes: "Yes", + no: "No", + setFullHistory: "Use full history", + viewMode: "View mode", + normalMode: "Normal", + kioskMode: "Kiosk", + simplifiedCards: "Simplified cards", + }, +} as const; + +export type MessageKey = keyof typeof messages.pl; + +const metricLabels: Record = { + ac_power: { pl: "Moc AC", en: "AC power" }, + energy_total: { pl: "Energia łączna", en: "Total energy" }, + energy_today: { pl: "Energia dziś", en: "Energy today" }, + energy_yesterday: { pl: "Energia wczoraj", en: "Energy yesterday" }, + today_vs_yesterday: { pl: "Dziś vs wczoraj", en: "Today vs yesterday" }, + dc_power_total: { pl: "Moc DC łącznie", en: "Total DC power" }, + inverter_temp: { pl: "Temperatura falownika", en: "Inverter temperature" }, +}; + +export function t(language: Language, key: MessageKey): string { + return messages[language][key] ?? messages.pl[key]; +} + +export function labelForMetric(language: Language, metricId: string, fallback: string): string { + return metricLabels[metricId]?.[language] ?? fallback; +} + +export function localeForLanguage(language: Language): string { + return language === "en" ? "en-GB" : "pl-PL"; +} + +export function normalizeLanguage(value: string | null | undefined): Language { + return value === "en" ? "en" : "pl"; +} + + +export function translateCompareMode(language: Language, mode: string): string { + switch (mode) { + case "none": + return language === "en" ? "Comparison" : "Porównanie"; + case "previous_period": + return t(language, "comparePreviousPeriod"); + case "previous_year": + return t(language, "comparePreviousYear"); + case "previous_year_2": + return t(language, "comparePreviousYear2"); + case "previous_year_3": + return t(language, "comparePreviousYear3"); + case "previous_month_12": + return t(language, "comparePreviousMonth12"); + case "previous_month_24": + return t(language, "comparePreviousMonth24"); + case "custom_multi": + return t(language, "compareCustomMulti"); + default: + return mode; + } +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..e228b96 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,148 @@ +:root { + --tblr-border-radius: 0.55rem; + --tblr-border-radius-lg: 0.7rem; + --tblr-border-radius-sm: 0.4rem; + --tblr-card-border-radius: 0.7rem; + --tblr-shadow-sm: 0 0.125rem 0.25rem rgba(15, 23, 42, 0.05); + --pv-shell-bg: #f4f6f9; + --pv-card-shadow: 0 1px 2px rgba(15, 23, 42, 0.06), 0 12px 24px rgba(15, 23, 42, 0.04); +} + +[data-bs-theme="dark"] { + --pv-shell-bg: #0b1220; + --pv-card-shadow: 0 1px 2px rgba(0, 0, 0, 0.28), 0 14px 30px rgba(0, 0, 0, 0.24); +} + +html, +body, +#root { + min-height: 100%; +} + +body { + background: var(--pv-shell-bg); +} + +.page { + min-height: 100vh; +} + +.page-body { + padding-top: 1rem; + padding-bottom: 1.5rem; +} + +.pv-navbar, +.pv-subnav { + backdrop-filter: blur(14px); +} + +.pv-card, +.login-card { + border-width: 1px; + box-shadow: var(--pv-card-shadow); +} + +.pv-card .card-header, +.pv-card .card-body, +.login-card .card-body { + padding: 1rem 1rem; +} + +.pv-hero-card .display-6 { + letter-spacing: -0.03em; +} + +.pv-chart { + width: 100%; + height: 340px; +} + +.pv-chart-sm { + width: 100%; + height: 280px; +} + +.status-row, +.string-panel { + background: rgba(127, 127, 127, 0.03); +} + +.kiosk-shell { + background: + radial-gradient(circle at top right, rgba(32, 107, 196, 0.08), transparent 30%), + var(--pv-shell-bg); +} + +.login-page-shell { + background: + radial-gradient(circle at top, rgba(32, 107, 196, 0.12), transparent 28%), + var(--pv-shell-bg); +} + +.btn-group > .btn, +.form-control, +.card, +.border, +.progress, +.badge, +.alert { + border-radius: 0.65rem !important; +} + +.card-table tbody tr:last-child td { + border-bottom-width: 0; +} + +.nav-link.active { + font-weight: 600; +} + +.table-responsive { + overflow-x: auto; +} + +@media (max-width: 768px) { + .pv-chart, + .pv-chart-sm { + height: 260px; + } + + .page-body { + padding-top: 0.75rem; + } +} + + +.pv-nav-link { + display: inline-flex !important; + align-items: center; + justify-content: center; + gap: 0.6rem; + min-height: 2.75rem; + padding: 0.7rem 1rem !important; +} + +.pv-nav-icon { + width: 1.25rem; + min-width: 1.25rem; + height: 1.25rem; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 1.25rem; + line-height: 1; +} + +.pv-nav-icon svg { + display: block; + width: 1.1rem; + height: 1.1rem; +} + +.pv-nav-title { + display: inline-flex; + align-items: center; + line-height: 1.1; + white-space: nowrap; +} diff --git a/frontend/src/lib/format.ts b/frontend/src/lib/format.ts new file mode 100644 index 0000000..444ebb9 --- /dev/null +++ b/frontend/src/lib/format.ts @@ -0,0 +1,81 @@ +export function formatValue( + value: number | string | null | undefined, + unit = "", + precision = 2, + locale = "pl-PL", +): string { + if (value === null || value === undefined || value === "") { + return "--"; + } + + if (typeof value === "number") { + return `${value.toLocaleString(locale, { + minimumFractionDigits: 0, + maximumFractionDigits: precision, + })}${unit ? ` ${unit}` : ""}`; + } + + return unit ? `${value} ${unit}` : String(value); +} + +export function formatDateTime(value?: string | null, locale = "pl-PL"): string { + if (!value) { + return "--"; + } + return new Date(value).toLocaleString(locale, { + dateStyle: "short", + timeStyle: "short", + }); +} + +export function formatDate(value?: string | null, locale = "pl-PL"): string { + if (!value) { + return "--"; + } + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) { + return value; + } + return parsed.toLocaleDateString(locale); +} + +export function formatShortTime(value?: string | null, locale = "pl-PL"): string { + if (!value) { + return "--"; + } + return new Date(value).toLocaleTimeString(locale, { + hour: "2-digit", + minute: "2-digit", + }); +} + +export function formatDurationShort(value?: number | null, locale = "pl-PL"): string { + if (value === null || value === undefined || Number.isNaN(value)) { + return "--"; + } + + const totalSeconds = Math.max(Math.round(value), 0); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + if (locale.startsWith("pl")) { + if (hours > 0) return `${hours}h ${minutes}m`; + if (minutes > 0) return `${minutes}m ${seconds}s`; + return `${seconds}s`; + } + + if (hours > 0) return `${hours}h ${minutes}m`; + if (minutes > 0) return `${minutes}m ${seconds}s`; + return `${seconds}s`; +} + +export function formatPercent(value?: number | null, precision = 1, locale = "pl-PL"): string { + if (value === null || value === undefined || Number.isNaN(value)) { + return "--"; + } + return `${value.toLocaleString(locale, { + minimumFractionDigits: 0, + maximumFractionDigits: precision, + })}%`; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..1560e9a --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import App from "./App"; +import "./index.css"; + +const queryClient = new QueryClient(); + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + + + +); diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 0000000..c3cf82a --- /dev/null +++ b/frontend/src/types.ts @@ -0,0 +1,275 @@ +export type StatusTone = "ok" | "warn" | "critical" | "neutral"; + +export interface HeroCard { + metric_id: string; + label: string; + value: number | string | null; + unit: string; + accent: string; + subtitle: string; +} + +export interface MetricValue { + metric_id: string; + label: string; + unit: string; + value: number | string | null; + timestamp?: string | null; + precision: number; + kind: "gauge" | "counter" | "text"; + status: StatusTone; +} + +export interface SnapshotGroupRow { + id: string; + label: string; + values: Record; + meta: Record; +} + +export interface SnapshotPayload { + updated_at?: string | null; + hero_cards: HeroCard[]; + kpis: Record; + strings: SnapshotGroupRow[]; + phases: SnapshotGroupRow[]; + status: MetricValue[]; + faults: string[]; +} + +export interface SeriesPoint { + timestamp: string; + value: number | null; +} + +export interface SeriesPayload { + metric_id: string; + label: string; + unit: string; + color?: string | null; + points: SeriesPoint[]; +} + +export interface HistoryPayload { + range_key: string; + start: string; + end: string; + series: SeriesPayload[]; +} + +export interface BucketPoint { + label: string; + start: string; + end: string; + value: number; +} + +export interface AnalyticsSummary { + total: number; + unit: string; + average_bucket: number; + best_bucket_label: string; + best_bucket_value: number; + co2_saved_kg: number; + comparison_total?: number | null; + comparison_delta_pct?: number | null; +} + +export interface AnalyticsPayload { + unit: string; + bucket: string; + compare_mode: string; + current: BucketPoint[]; + comparison: BucketPoint[]; + comparisons?: Array<{ + key: string; + label: string; + start: string; + end: string; + total: number; + delta_pct?: number | null; + points: BucketPoint[]; + }>; + summary: AnalyticsSummary; + meta: { + window: { + start: string; + end: string; + range_key: string; + }; + source?: string; + }; +} + +export interface DistributionSlice { + label: string; + value: number; + share: number; +} + +export interface DistributionPayload { + unit: string; + bucket: string; + total: number; + slices: DistributionSlice[]; +} + +export interface HistoricalCoverage { + imported_days: number; + first_day?: string | null; + last_day?: string | null; + total_energy_kwh: number; + available_days: number; + missing_days: number; + coverage_pct?: number | null; +} + +export interface HistoricalChunkProgress { + chunk_index: number; + total_chunks: number; + start_date: string; + end_date: string; + processed_days: number; + imported_days: number; + skipped_days: number; + energy_kwh: number; + state: string; + started_at?: string | null; + finished_at?: string | null; + duration_seconds?: number | null; + note: string; +} + +export interface HistoricalActivityEvent { + timestamp: string; + level: string; + title: string; + message: string; + day?: string | null; + chunk_index?: number | null; +} + +export interface HistoricalStatus { + enabled: boolean; + running: boolean; + state: string; + job_id?: string | null; + started_at?: string | null; + finished_at?: string | null; + requested_start_date?: string | null; + requested_end_date?: string | null; + total_days: number; + processed_days: number; + imported_days: number; + skipped_days: number; + chunk_days: number; + total_chunks: number; + active_chunk_index: number; + current_date?: string | null; + current_chunk_start?: string | null; + current_chunk_end?: string | null; + elapsed_seconds?: number | null; + estimated_remaining_seconds?: number | null; + avg_days_per_minute?: number | null; + last_error?: string | null; + message: string; + coverage: HistoricalCoverage; + available_start_date?: string | null; + available_end_date?: string | null; + default_chunk_days: number; + recent_chunks: HistoricalChunkProgress[]; + recent_events: HistoricalActivityEvent[]; +} + +export interface HistoricalStartPayload { + start_date?: string; + end_date?: string; + chunk_days?: number; + force?: boolean; +} + +export interface AuthStatus { + enabled: boolean; + authenticated: boolean; + user?: string | null; + display_name?: string | null; + role?: string | null; +} + +export interface DashboardConfig { + app: { + name: string; + version: string; + site_name: string; + timezone: string; + installed_power_kwp: number; + }; + defaults: { + realtime_range: string; + analytics_range: string; + analytics_bucket: string; + tab: string; + theme: string; + language: string; + }; + auth?: { + enabled: boolean; + }; + i18n?: { + default_language: string; + supported_languages: string[]; + }; + capabilities: { + modules: Record; + strings_enabled: boolean; + strings_count: number; + phases_enabled: boolean; + phases_count: number; + analytics_enabled: boolean; + realtime_enabled: boolean; + comparison_modes: string[]; + ranges: Array<{ key: string; label: string }>; + buckets: Array<{ key: string; label: string }>; + historical_import_enabled: boolean; + history: { + enabled: boolean; + default_chunk_days: number; + auto_sync_enabled: boolean; + auto_sync_interval_minutes: number; + }; + }; + visible_entities: Array<{ + metric_id: string; + label: string; + entity_id: string; + measurement: string; + unit: string; + kind: string; + }>; +} + + +export interface AuthUserItem { + username: string; + display_name: string; + role: string; + is_active: boolean; + created_at?: string | null; + updated_at?: string | null; +} + +export interface AuthUsersPayload { + items: AuthUserItem[]; +} + + +export interface KioskSettingsPayload { + mode: "public" | "private"; + widgets: string[]; + realtime_range: string; + analytics_range: string; + analytics_bucket: string; + compare_mode: string; + updated_at?: string | null; + updated_by?: string | null; +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..0ef0c49 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "noEmit": true + }, + "include": ["src"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..beec05d --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + server: { + host: "0.0.0.0", + port: 5173 + }, + preview: { + host: "0.0.0.0", + port: 4173 + } +}); diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100755 index 0000000..f3349fb --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +cleanup() { + if [ -n "${BACKEND_PID:-}" ]; then + kill "$BACKEND_PID" >/dev/null 2>&1 || true + fi +} + +trap cleanup EXIT INT TERM + +"$ROOT_DIR/scripts/dev_backend.sh" & +BACKEND_PID=$! + +sleep 2 +"$ROOT_DIR/scripts/dev_frontend.sh" diff --git a/scripts/dev_backend.sh b/scripts/dev_backend.sh new file mode 100755 index 0000000..b84273b --- /dev/null +++ b/scripts/dev_backend.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BACKEND_DIR="$ROOT_DIR/backend" +PYTHON_BIN="${PYTHON_BIN:-python3.14}" + +if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then + PYTHON_BIN="${PYTHON_FALLBACK_BIN:-python3}" +fi + +cd "$BACKEND_DIR" + +if [ ! -d ".venv" ]; then + "$PYTHON_BIN" -m venv .venv +fi + +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -r requirements.txt +python run.py diff --git a/scripts/dev_frontend.sh b/scripts/dev_frontend.sh new file mode 100755 index 0000000..3393a1a --- /dev/null +++ b/scripts/dev_frontend.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +FRONTEND_DIR="$ROOT_DIR/frontend" + +cd "$FRONTEND_DIR" + +if [ ! -d "node_modules" ]; then + if [ -f "package-lock.json" ]; then + npm ci + else + npm install + fi +fi + +npm run dev -- --host 0.0.0.0 diff --git a/scripts/dev_frontend_demo.sh b/scripts/dev_frontend_demo.sh new file mode 100755 index 0000000..b08be9c --- /dev/null +++ b/scripts/dev_frontend_demo.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/../frontend" +if [ ! -d "node_modules" ]; then + if [ -f "package-lock.json" ]; then + npm ci + else + npm install + fi +fi +VITE_DEMO_MODE=true npm run dev -- --host 0.0.0.0 diff --git a/scripts/import_history.sh b/scripts/import_history.sh new file mode 100755 index 0000000..30d3b81 --- /dev/null +++ b/scripts/import_history.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BACKEND_DIR="$ROOT_DIR/backend" +PYTHON_BIN="${PYTHON_BIN:-python3.14}" + +if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then + PYTHON_BIN="${PYTHON_FALLBACK_BIN:-python3}" +fi + +cd "$BACKEND_DIR" + +if [ ! -d ".venv" ]; then + "$PYTHON_BIN" -m venv .venv +fi + +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -r requirements.txt +python backfill.py "$@" diff --git a/scripts/prod_backend.sh b/scripts/prod_backend.sh new file mode 100755 index 0000000..bc51688 --- /dev/null +++ b/scripts/prod_backend.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BACKEND_DIR="$ROOT_DIR/backend" +PYTHON_BIN="${PYTHON_BIN:-python3.14}" + +if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then + PYTHON_BIN="${PYTHON_FALLBACK_BIN:-python3}" +fi + +cd "$BACKEND_DIR" + +if [ ! -d ".venv" ]; then + "$PYTHON_BIN" -m venv .venv +fi + +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -r requirements.txt +python run_prod.py diff --git a/scripts/prod_down.sh b/scripts/prod_down.sh new file mode 100755 index 0000000..8732cb3 --- /dev/null +++ b/scripts/prod_down.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +docker compose down diff --git a/scripts/prod_frontend_build.sh b/scripts/prod_frontend_build.sh new file mode 100755 index 0000000..038d186 --- /dev/null +++ b/scripts/prod_frontend_build.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +FRONTEND_DIR="$ROOT_DIR/frontend" + +cd "$FRONTEND_DIR" + +if [ ! -d "node_modules" ]; then + if [ -f "package-lock.json" ]; then + npm ci + else + npm install + fi +fi + +npm run build diff --git a/scripts/prod_frontend_preview.sh b/scripts/prod_frontend_preview.sh new file mode 100755 index 0000000..70afeaf --- /dev/null +++ b/scripts/prod_frontend_preview.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +FRONTEND_DIR="$ROOT_DIR/frontend" + +cd "$FRONTEND_DIR" + +if [ ! -d "node_modules" ]; then + if [ -f "package-lock.json" ]; then + npm ci + else + npm install + fi +fi + +npm run preview -- --host 0.0.0.0 diff --git a/scripts/prod_up.sh b/scripts/prod_up.sh new file mode 100755 index 0000000..38bc579 --- /dev/null +++ b/scripts/prod_up.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +docker compose up -d --build