diff --git a/.claude/skills/README.md b/.claude/skills/README.md new file mode 100644 index 000000000..2c1021aba --- /dev/null +++ b/.claude/skills/README.md @@ -0,0 +1,45 @@ +# .claude/ + +Project-local Claude Code configuration for NetBox. + +The tool-agnostic content layer for this repo is [`AGENTS.md`](../AGENTS.md) at the repo root, with its `CLAUDE.md` shim. This `.claude/` directory is the Claude-specific action layer that complements `AGENTS.md` with project-local skills, slash commands, and per-developer settings. + +## Layout + +- `skills/` — Project-local Claude Code skills. Each skill is its own subdirectory containing a `SKILL.md` describing what it does and when to use it. Use this for repo-specific procedures. +- `commands/` — Project-local slash commands. One Markdown file per command: `commands/.md`. Use this for `/foo` shortcuts that only make sense in this repo. +- `settings.local.json` — Per-developer Claude Code settings (tool permissions, MCP server paths, IDE preferences). **Never committed** — this filename is in the repo's `.gitignore`. + +## When to add a skill (vs. inlining in AGENTS.md or promoting upstream) + +Add a skill here when: + +- The procedure is repo-specific (it would not be useful in other NBL repos as-is). +- The procedure is non-trivial (more than a one-line note that fits naturally inside `AGENTS.md`). +- The procedure is a recipe an agent or engineer might re-run, not a one-off. + +## When to add a slash command + +Add a command here when: + +- The action is something you find yourself typing the same prompt for repeatedly. +- The repo has a non-obvious workflow that benefits from a shortcut. + +## Conventions + +- Skill and command names use `lowercase-kebab-case`, matching the [folder naming convention in `AGENTS.md`](../AGENTS.md). +- Each skill directory has a `SKILL.md` (the entry point); supporting files (references, examples, sample data) live alongside it inside the skill's directory. +- Each command is a single Markdown file named for the slash command: `commands/.md`. +- Skills and commands document *why* they make the choices they do — the rationale is more durable than the bare instruction. + +## How to add your first skill + +1. Pick a kebab-case name describing the action: e.g., `parse-linear-issues`, `render-delivery-row`. +2. `mkdir .claude/skills//` and create `SKILL.md` inside it. +3. The `SKILL.md` opens with a short YAML-ish header (name, description, version) and then the prompt content. +4. Open a PR — the new directory and its `SKILL.md` are tracked once committed. + +## References + +- [`AGENTS.md`](../AGENTS.md) — this repo's primary agent-context file (open standard). +- [Claude Code skills documentation](https://docs.claude.com/en/docs/claude-code/skills) — what a `SKILL.md` looks like and how Claude Code resolves them. diff --git a/.claude/skills/add-config-param/SKILL.md b/.claude/skills/add-config-param/SKILL.md new file mode 100644 index 000000000..429705cb9 --- /dev/null +++ b/.claude/skills/add-config-param/SKILL.md @@ -0,0 +1,217 @@ +--- +name: add-config-param +description: Step-by-step guide for adding a new configuration parameter to NetBox, covering both static parameters (settings.py) and dynamic parameters (database-backed, editable via the admin UI). Use when the user asks to add a new configuration option, setting, or parameter to NetBox. +--- + +# Adding a Configuration Parameter to NetBox + +NetBox has two distinct kinds of configuration parameters. Choose the right one before writing any code: + +| Type | Where defined | Changed by | Takes effect | +|---|---|---|---| +| **Static** | `settings.py` via `getattr(configuration, ...)` | Editing `configuration.py` + restart | On WSGI restart | +| **Dynamic** | `config/parameters.py` `PARAMS` tuple | Admin UI or `configuration.py` | Immediately (cached in Redis) | + +**Use dynamic** when: +- Operators need to tune the value without a service restart +- The parameter controls UI behavior or defaults (banners, page sizes, default values) +- Examples: `PAGINATE_COUNT`, `MAINTENANCE_MODE`, `BANNER_TOP` + +**Use static** when: +- The value must not change at runtime (auth backends, database config, secret keys) +- The value controls infrastructure that requires a restart anyway +- Examples: `ALLOWED_HOSTS`, `REMOTE_AUTH_BACKEND`, `LOGGING` + +--- + +## Adding a Dynamic Configuration Parameter + +Dynamic parameters are defined in `netbox/netbox/config/parameters.py`, stored in the `ConfigRevision.data` JSONField, cached in Redis, and editable via Admin > System > Configuration History. + +### Step 1 — Add to `PARAMS` + +**File:** `netbox/netbox/config/parameters.py` + +Add a `ConfigParam` entry to the `PARAMS` tuple, grouped logically with related parameters: + +```python +ConfigParam( + name='MY_PARAM', + label=_('My param'), + default=, + description=_("One-sentence description of what this controls"), + field=forms.BooleanField, # or IntegerField, CharField, JSONField, SimpleArrayField + # field_kwargs only when extra widget/validation config is needed: + field_kwargs={ + 'widget': forms.Textarea(attrs={'class': 'vLargeTextField'}), + }, +), +``` + +**Common `field` choices:** + +| Field | Use for | +|---|---| +| `forms.CharField` (default) | Short strings | +| `forms.BooleanField` | On/off toggles | +| `forms.IntegerField` | Counts, sizes, timeouts | +| `forms.JSONField` | Dicts/lists with free-form structure | +| `SimpleArrayField` | Lists of strings (add `field_kwargs={'base_field': forms.CharField()}`) | + +The `default` value is returned whenever no `ConfigRevision` row exists and the parameter is not hard-coded in `configuration.py`. + +### Step 2 — Use the parameter in code + +Access via `get_config()` (request-scoped, cached) or the `ConfigItem` callable (deferred): + +```python +from netbox.config import get_config + +# One-time read: +value = get_config().MY_PARAM + +# Deferred (evaluated later): +from netbox.config import ConfigItem +MY_PARAM = ConfigItem('MY_PARAM') +``` + +`get_config()` returns the `Config` object which tries: +1. Hard-coded value in Django `settings` (set by `configuration.py`) +2. Redis-cached active `ConfigRevision` +3. `ConfigParam.default` + +### Step 3 — Document in the configuration docs + +Add a section to the appropriate file under `docs/configuration/`: + +| File | Category | +|---|---| +| `miscellaneous.md` | General / doesn't fit elsewhere | +| `default-values.md` | Default values for object fields | +| `security.md` | Auth, permissions, URL validation | +| `data-validation.md` | `CUSTOM_VALIDATORS`, `PROTECTION_RULES` | +| `graphql-api.md` | GraphQL settings | +| `error-reporting.md` | Sentry, logging | +| `remote-authentication.md` | Remote auth settings | +| `development.md` | Developer-only flags | +| `system.md` | Low-level system settings | + +Template for a dynamic parameter doc section: + +```markdown +## MY_PARAM + +!!! tip "Dynamic Configuration Parameter" + +Default: `` + +One or two sentences describing what the parameter does, what values are accepted, +and any side effects. +``` + +### Step 4 — Register in the dynamic params index + +**File:** `docs/configuration/index.md` + +Add the new parameter to the bulleted list under "Dynamic Configuration Parameters", keeping the list alphabetically ordered: + +```markdown +* [`MY_PARAM`](./miscellaneous.md#my_param) +``` + +### Step 5 — Optionally add to the example config + +If the parameter is important enough that operators should know they can hard-code it, add a commented entry to `netbox/netbox/configuration_example.py`: + +```python +# MY_PARAM = +``` + +Place it near related parameters. + +### No migration needed + +Dynamic parameters are stored in the `ConfigRevision.data` JSONField, which already exists. No database migration is required when adding a new `ConfigParam`. + +--- + +## Adding a Static Configuration Parameter + +Static parameters live in `settings.py` and are read at startup from `configuration.py`. They take effect only after the WSGI service is restarted. + +### Step 1 — Add to `settings.py` + +**File:** `netbox/netbox/settings.py` + +Add a line in the "Set static config parameters" block, alphabetically within its logical group: + +```python +MY_PARAM = getattr(configuration, 'MY_PARAM', ) +``` + +For required parameters (no default), use `getattr(configuration, 'MY_PARAM')` with no fallback and add the parameter name to the required check near the top: + +```python +for parameter in ('ALLOWED_HOSTS', 'MY_PARAM', 'SECRET_KEY', 'REDIS'): + if not hasattr(configuration, parameter): + raise ImproperlyConfigured(f"Required parameter {parameter} is missing from configuration.") +``` + +### Step 2 — Add validation (if needed) + +If the parameter has constrained values, add an `ImproperlyConfigured` check immediately after the `getattr` line: + +```python +MY_PARAM = getattr(configuration, 'MY_PARAM', 'option_a') +if MY_PARAM not in ('option_a', 'option_b'): + raise ImproperlyConfigured(f"MY_PARAM must be 'option_a' or 'option_b' (found {MY_PARAM})") +``` + +For complex validation (importable paths, valid URLs, etc.) follow the patterns of `PROXY_ROUTERS` or `RELEASE_CHECK_URL` in `settings.py`. + +### Step 3 — Add to the example config + +**File:** `netbox/netbox/configuration_example.py` + +Add a commented entry with a brief inline comment explaining the parameter: + +```python +# MY_PARAM = 'default_value' # Short description of what this does +``` + +### Step 4 — Document + +Add a section to the appropriate `docs/configuration/*.md` file: + +```markdown +## MY_PARAM + +Default: `` + +One or two sentences describing the parameter, accepted values, and any constraints. + +--- +``` + +Static parameters do **not** get the `!!! tip "Dynamic Configuration Parameter"` admonition. + +--- + +## Common Gotchas + +- **Dynamic params don't need a migration** — the value is stored in the `ConfigRevision.data` JSONField which already exists. +- **Hard-coding a dynamic param in `configuration.py` overrides the UI** — the loop at the bottom of `settings.py` (`for param in CONFIG_PARAMS: ...`) sets the Django setting, which `Config.__getattr__` checks first. Document this behaviour in the parameter's doc page. +- **`forms.BooleanField` with `required=False`**: the `ConfigFormMetaclass` always adds `required=False`, so a `BooleanField` correctly represents a three-state (True / False / unset-use-default) UI. No extra `field_kwargs` needed for booleans. +- **`SimpleArrayField` needs `base_field`**: always pass `field_kwargs={'base_field': forms.CharField()}`. +- **No `ruff format`** on existing files — use `ruff check` only. + +## References + +- Dynamic param definitions: `netbox/netbox/config/parameters.py` +- Config loading / `Config` class: `netbox/netbox/config/__init__.py` +- `ConfigRevision` model: `netbox/core/models/config.py` +- `ConfigRevisionForm` (metaclass): `netbox/core/forms/model_forms.py` +- Static config loading: `netbox/netbox/settings.py` lines 67–213 +- Example config: `netbox/netbox/configuration_example.py` +- Config tests: `netbox/netbox/tests/test_config.py` +- Documentation: `docs/configuration/` diff --git a/.claude/skills/add-model-field/SKILL.md b/.claude/skills/add-model-field/SKILL.md new file mode 100644 index 000000000..a3ebaf3e2 --- /dev/null +++ b/.claude/skills/add-model-field/SKILL.md @@ -0,0 +1,410 @@ +--- +name: add-model-field +description: Step-by-step checklist for adding a new field to an existing NetBox model, covering all required touch points (model, migration, validation, serializer, forms, filterset, table, panel/template, search, GraphQL, tests, docs). Use when the user asks to add a field or attribute to an existing model. +--- + +# Adding a Field to an Existing NetBox Model + +Adding a field to an existing model touches many files. The scope depends on the field type and how it will be used. Work through the checklist below in order — each section builds on the previous. + +## Before You Start + +Determine upfront: +- **Field type**: scalar (CharField, IntegerField, etc.), FK/M2M, GenericForeignKey, or a special type like JSONField +- **Nullable/optional?** Most new fields should be `blank=True, null=True` unless there's a strong reason otherwise +- **Searchable?** Should it appear in global search results? +- **Filterable?** Should it be exposed in the FilterSet? +- **Displayable in list view?** Should it be a column in the object table? +- **Displayable in detail view?** Should it appear in the detail panel? + +## 1. Add the Field to the Model + +**File:** `netbox//models/.py` + +```python +class MyModel(PrimaryModel): + # ... existing fields ... + new_field = models.CharField( + verbose_name=_('new field'), + max_length=100, + blank=True, + ) + # FK example: + related_thing = models.ForeignKey( + to='app.RelatedModel', + on_delete=models.PROTECT, + related_name='my_models', + blank=True, + null=True, + ) +``` + +The `related_name` of a ForeignKey field should generally be the verbose form of the related model's name (e.g. `books` rather than the default `book_set`). + +**Special cases:** + +- **GenericForeignKey**: If this is a non-unique GFK, add a composite index in `Meta`: + ```python + class Meta: + indexes = ( + models.Index(fields=('object_type', 'object_id')), + ) + ``` + +- **`clone_fields`**: If the field should be pre-filled when cloning an object, add it to `clone_fields` on the model class: + ```python + clone_fields = ('existing_field', 'new_field') + ``` + +- **Validation**: If the new field introduces cross-field constraints, add logic to `clean()`: + ```python + def clean(self): + super().clean() + if self.new_field and not self.related_field: + raise ValidationError({'new_field': _('...')}) + ``` + +## 2. Generate the Migration + +**Do NOT write migrations manually.** Tell the user to run: + +```bash +python netbox/manage.py makemigrations -n --no-header +``` + +Set `DEVELOPER = True` in `configuration.py` if the command is blocked. + +For FK fields, also run: +```bash +python netbox/manage.py migrate +``` +before continuing, so the DB is in sync for manual testing. + +## 3. Update the API Serializer + +The serializer lives under `netbox//api/serializers_/` (note the trailing underscore — it's a directory of submodules star-imported by `serializers.py`). Find the submodule that owns the model and edit the serializer there. + +- **Simple field**: just add the field name to `fields` in `Meta`: + ```python + class Meta: + fields = [..., 'new_field', ...] + ``` + +- **FK field**: add a single serializer field with `nested=True`. NetBox does not use a separate `_id` companion field — the framework accepts a primary key (or brief object) when writing: + ```python + related_thing = RelatedThingSerializer( + nested=True, + required=False, + allow_null=True, + ) + # Add 'related_thing' to Meta.fields + ``` + +- **`brief_fields`**: only add to `brief_fields` if the field is truly essential for compact/nested representations. + +## 4. Update Forms + +There are typically up to four forms to update. Find them under `netbox//forms/`. + +### 4a. Model form (create/edit) — `model_forms.py` + +Add the field to the `fieldsets` tuple and to `Meta.fields`: + +```python +class MyModelForm(PrimaryModelForm): + fieldsets = ( + FieldSet('name', 'new_field', 'related_thing', name=_('My Model')), + ... + ) + class Meta: + model = MyModel + fields = ('name', 'new_field', 'related_thing', ...) +``` + +For FK fields, use `DynamicModelChoiceField`: +```python +related_thing = DynamicModelChoiceField( + queryset=RelatedModel.objects.all(), + required=False, +) +``` + +### 4b. Bulk edit form — `bulk_edit.py` + +Add the field as optional (so it can be blanked): +```python +new_field = forms.CharField(required=False) +# or for FK: +related_thing = DynamicModelChoiceField(queryset=..., required=False) +nullable_fields = ('new_field', 'related_thing') # if it can be set to null +``` +Add to `fieldsets` and `Meta.fields` here too. + +### 4c. Bulk import form — `bulk_import.py` + +If the field should be importable via CSV, add it to the import form: +```python +class MyModelImportForm(NetBoxModelImportForm): + new_field = forms.CharField(required=False) + class Meta: + model = MyModel + fields = ('name', 'new_field', ...) +``` + +### 4d. Filter form — `filtersets.py` (the forms version) + +The base class should match the model's base (`PrimaryModelFilterSetForm`, `OrganizationalModelFilterSetForm`, `NestedGroupModelFilterSetForm`, or `NetBoxModelFilterSetForm`). Add the new entries to the existing `fieldsets` and declare the filter field: + +```python +class MyModelFilterForm(PrimaryModelFilterSetForm): + fieldsets = ( + FieldSet('q', 'filter_id', 'tag'), + FieldSet('new_field', 'related_thing_id', name=_('Attributes')), + ) + new_field = forms.CharField(required=False) + related_thing_id = DynamicModelMultipleChoiceField( + queryset=RelatedModel.objects.all(), + required=False, + label=_('Related Thing'), + ) +``` + +## 5. Update the FilterSet + +**File:** `netbox//filtersets.py` + +- **Simple scalar field**: add to `Meta.fields` if a basic exact/contains filter suffices. +- **FK field**: add both `` (name lookup) and `_id` (PK lookup) explicitly — do not rely on `Meta.fields` to generate them: + +```python +class MyModelFilterSet(PrimaryModelFilterSet): + related_thing = django_filters.ModelMultipleChoiceFilter( + field_name='related_thing__name', + queryset=RelatedModel.objects.all(), + to_field_name='name', + label=_('Related thing (name)'), + ) + related_thing_id = django_filters.ModelMultipleChoiceFilter( + queryset=RelatedModel.objects.all(), + label=_('Related thing (ID)'), + ) + + class Meta: + model = MyModel + fields = ('id', 'name', 'new_field', ...) # add new_field here for simple fields +``` + +If the field should be searchable from the search box (`q=`), add it to the `search()` method: +```python +def search(self, queryset, name, value): + return queryset.filter( + Q(name__icontains=value) | + Q(new_field__icontains=value) | # add here + ... + ) +``` + +## 6. Update the Table + +**File:** `netbox//tables/.py` + +- **Simple field**: just add the field name to `Meta.fields`. Add to `default_columns` if it should show by default. +- **FK field** (linking to another object): + ```python + related_thing = tables.Column(linkify=True) + ``` + Add `related_thing` to both `Meta.fields` and `default_columns` if appropriate. +- **Choice field**: display just works if the model uses `get__display()`; no custom column needed. +- **Traversed FK** (field accessed through another relation): + ```python + related_thing = tables.Column( + accessor=tables.A('some_fk__related_thing'), + linkify=True, + ) + ``` + +## 7. Update the Detail View Panel + +The detail view display is controlled by a panel class (not an HTML template), defined under `netbox//ui/panels.py`. + +Find the panel for the model and add a new attribute declaration: + +```python +from netbox.ui import attrs, panels + +class MyModelPanel(panels.ObjectAttributesPanel): + existing_field = attrs.TextAttr('existing_field') + new_field = attrs.TextAttr('new_field') # simple text + related_thing = attrs.RelatedObjectAttr('related_thing', linkify=True) # FK + status = attrs.ChoiceAttr('status') # choice field with badge + is_active = attrs.BooleanAttr('is_active') # boolean + color = attrs.ColorAttr('color') # color swatch +``` + +**Available attr types** (from `netbox.ui.attrs`): + +| Class | Use for | +|---|---| +| `TextAttr` | Plain text / CharField | +| `NumericAttr` | Numbers, optionally with a unit | +| `ChoiceAttr` | Choice fields (renders a colored badge) | +| `BooleanAttr` | Boolean fields | +| `ColorAttr` | Color hex fields | +| `RelatedObjectAttr` | Direct ForeignKey | +| `NestedObjectAttr` | ForeignKey on a nested/hierarchical model (e.g. region.parent) | +| `RelatedObjectListAttr` | ManyToMany or reverse FK list | +| `GenericForeignKeyAttr` | GenericForeignKey | +| `DateTimeAttr` | DateTimeField | +| `TimezoneAttr` | Timezone fields | +| `AddressAttr` | Address text (optionally with map link) | +| `TemplatedAttr` | Custom per-field HTML template | + +If the model uses a legacy HTML template (under `netbox/templates//`) rather than a declarative panel, add a `` row to the relevant `` in that template instead. + +## 8. Update the SearchIndex (if applicable) + +**File:** `netbox//search.py` + +If the new field should be indexed for global search, add it to the model's `SearchIndex`: + +```python +@register_search +class MyModelIndex(SearchIndex): + model = models.MyModel + fields = ( + ('name', 100), + ('new_field', 300), # add here with an appropriate weight + ('description', 500), + ('comments', 5000), + ) +``` + +Weight guide: lower = higher search priority. Name fields ~100, short descriptors ~300–500, long-form comments ~5000. + +## 9. Update GraphQL + +### Filter — `graphql/filters.py` + +Add a filter field to the model's `Filter` class: + +```python +@strawberry_django.filter_type(models.MyModel, lookups=True) +class MyModelFilter(PrimaryModelFilter): + # simple field (lookups=True auto-generates eq/icontains/etc.) + new_field: StrFilterLookup[str] | None = strawberry_django.filter_field() + + # FK field: + related_thing: Annotated['RelatedThingFilter', strawberry.lazy('.graphql.filters')] | None = strawberry_django.filter_field() + related_thing_id: ID | None = strawberry_django.filter_field() +``` + +### Type — `graphql/types.py` + +For simple fields, `fields='__all__'` on the type decorator will pick up the new field automatically. No change needed unless: + +- The field is in an `exclude` list on the type — remove it. +- The field requires a custom type annotation (e.g. a lazy FK reference or a special scalar): + ```python + @strawberry_django.type(models.MyModel, fields='__all__', ...) + class MyModelType(PrimaryObjectType): + related_thing: Annotated['RelatedThingType', strawberry.lazy('.graphql.types')] | None + ``` + +> **Prefetch null failures:** If GraphQL unit tests fail citing null values on a non-nullable field, change the field definition to use `select_related`: +> ```python +> related_thing: ... = strawberry_django.field(select_related=['related_thing']) +> ``` + +## 10. Write Tests + +### FilterSet tests — `tests/test_filtersets.py` + +Add test methods for any new FilterSet fields: + +```python +def test_new_field(self): + params = {'new_field': ['value1', 'value2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), expected) + +def test_related_thing(self): + # Test both name and _id variants + related = RelatedModel.objects.filter(...) + params = {'related_thing_id': [related[0].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), expected) + params = {'related_thing': [related[0].name]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), expected) +``` + +Ensure `setUpTestData` creates test objects with diverse values for the new field. + +### API tests — `tests/test_api.py` + +- Update `setUpTestData` to populate the new field in test instances. +- Update `create_data` and (if applicable) `bulk_update_data` to include the new field. +- If the field is filterable via the API, add a `test_list_objects_by_` test. + +### View tests — `tests/test_views.py` + +- Update `form_data` in `setUpTestData` to include the new field. +- Update `bulk_edit_data` if the field is bulk-editable. +- Update `csv_data` if the field is importable. + +### Model tests — `tests/test_models.py` (if validation was added) + +Add a test for any custom `clean()` logic: + +```python +def test_clean_new_field_validation(self): + instance = MyModel(new_field='invalid_value', ...) + with self.assertRaises(ValidationError): + instance.clean() +``` + +## 11. Update Documentation + +**File:** `docs/models//.md` + +Add the new field to the model's documentation page. Include: +- The field name and description +- Valid values (for choice fields) +- Any constraints or dependencies + +## Summary Checklist + +| # | File(s) | Action | +|---|---|---| +| 1 | `models/.py` | Add field; add to `clone_fields`; add `clean()` validation | +| 2 | (user runs) | `makemigrations -n --no-header` | +| 3 | `api/serializers_/.py` | Add field to `fields`; for FK use a single `Serializer(nested=True)` field (no `_id` companion) | +| 4a | `forms/model_forms.py` | Add to `fieldsets` and `Meta.fields` | +| 4b | `forms/bulk_edit.py` | Add as optional; add to `nullable_fields` if nullable | +| 4c | `forms/bulk_import.py` | Add if CSV-importable | +| 4d | `forms/filtersets.py` | Add filter field and to `fieldsets` | +| 5 | `filtersets.py` | Add to FilterSet; add FK + FK_id pair; update `search()` | +| 6 | `tables/.py` | Add column; add to `Meta.fields`; update `default_columns` | +| 7 | `/ui/panels.py` | Add attr to the model's panel class | +| 8 | `search.py` | Add to SearchIndex `fields` tuple with appropriate weight | +| 9 | `graphql/filters.py`, `types.py` | Add filter field; update type if excluded or needs custom annotation | +| 10 | `tests/test_*.py` | Update filterset, API, view, and model tests | +| 11 | `docs/models//.md` | Document the new field | + +## Common Gotchas + +- **FilterSets need explicit `_id` variants for FK fields** — `Meta.fields` does not auto-generate them. (This is FilterSet-only — API serializers do **not** add a parallel `_id` field; see below.) +- **Serializer FK fields use `nested=True`, not a parallel `_id`.** Older code that defines both `foo = NestedFooSerializer(read_only=True)` and `foo_id = serializers.PrimaryKeyRelatedField(...)` is the legacy pattern; new code uses a single `foo = FooSerializer(nested=True, ...)` field. +- **Migrations must be generated, not written manually.** If `makemigrations` is blocked, ensure `DEVELOPER = True` is set in `configuration.py`. +- **List views and API serializers don't need manual `prefetch_related()`** — this is handled dynamically. Only add explicit prefetches in a viewset if required for a custom endpoint. +- **`clone_fields` must be declared explicitly** on the model. Fields not in this list are not copied when cloning an object. +- **`brief_fields` on serializers is explicit** — just listing a field in `Meta.fields` does not include it in brief/nested representations. +- **Panel attrs, not HTML templates** — new models use `ObjectAttributesPanel` subclasses in `/ui/panels.py`. Only fall back to editing `templates//` HTML files if the model predates the declarative layout system. +- **GraphQL `fields='__all__'`** picks up simple new fields automatically; only explicit overrides needed for FKs, excluded fields, or special scalars. +- **No `ruff format`** on existing files — use `ruff check` only. + +## References + +- Real example (adding FK filter field): `git show 87b17ff26` — adds `profile`/`profile_id` to the Module filterset, filter form, table, template, and tests +- Real example (adding a JSONField): `git show 5f802bb18` — adds `choice_colors` to CustomFieldChoiceSet across model, forms, filterset, serializer, GraphQL, and tests +- Panel attrs reference: `netbox/netbox/ui/attrs.py` +- Panel classes: `netbox//ui/panels.py` +- Base filterset classes: `netbox/netbox/filtersets.py` +- Contributing guide: `docs/development/extending-models.md` diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md new file mode 100644 index 000000000..0fa5783c8 --- /dev/null +++ b/.claude/skills/add-model/SKILL.md @@ -0,0 +1,519 @@ +--- +name: add-model +description: Step-by-step guide for adding a new model to NetBox, including all required components (model, filterset, serializer, views, forms, tables, GraphQL, tests, docs, navigation). Use when the user asks to add a new model or object type to NetBox. +--- + +# Adding a New Model to NetBox + +Adding a model requires wiring up ~12 components. Work through them in order — each builds on the previous. If the user hasn't specified which app to place the model in, ask first. + +## 0. Before You Start + +Decide on: +- **App**: which existing app owns this model (`dcim`, `ipam`, `extras`, etc.) +- **Base class**: see the hierarchy below +- **URL slug**: the kebab-case name used in URLs (e.g. `virtual-chassis`) +- **Model name**: PascalCase (e.g. `VirtualChassis`) +- **Verbose names**: for `Meta.verbose_name` / `verbose_name_plural` + +### Base Class Hierarchy + +| Class | Use when | +|---|-------------------------------------------------------------------------------------| +| `PrimaryModel` | Real infrastructure objects with description, comments, and owner. Most new models. | +| `OrganizationalModel` | Purely organizational/grouping objects (roles, types, categories). | +| `NestedGroupModel` | Hierarchical tree objects (regions, locations). Uses MPTT. | +| `ChangeLoggedModel` | Lightweight ancillary objects; no custom fields, tags, etc. | +| `AdminModel` | Administrative resources (no change-logging in the user-facing changelog). | +| `NetBoxModel` | Direct subclass of the feature set — use only when no other class fits. | + +All of these live in `netbox/netbox/models/__init__.py`. The remainder of this skill assumes `PrimaryModel`; substitute the matching `Organizational…` / `NestedGroup…` / `ChangeLogged…` base classes (filterset, form, table, serializer, GraphQL) where appropriate. + +## 1. Define the Model + +**File:** `netbox//models/.py` (or `models.py` for smaller apps) + +```python +class MyModel(PrimaryModel): + name = models.CharField( + verbose_name=_('name'), + max_length=100, + db_collation='natural_sort', # for alphabetic-aware sorting + ) + some_fk = models.ForeignKey( + to='app.RelatedModel', + on_delete=models.PROTECT, + related_name='my_models', + blank=True, + null=True, + ) + + class Meta: + ordering = ['name'] + verbose_name = _('my model') + verbose_name_plural = _('my models') + + def __str__(self): + return self.name +``` + +- Add the model to `__all__` in the models module's `__init__.py`. +- `db_collation='natural_sort'` on name fields enables natural sort order; omit if not needed. +- Use `models.PROTECT` for FK `on_delete` unless cascade deletion is explicitly desired. +- `PrimaryModel` already provides `description`, `comments`, and `owner` — don't redeclare them. + +**Do NOT run `makemigrations` yourself.** Tell the user to run the following when finished: + +```bash +python netbox/manage.py makemigrations +``` + +## 2. Define Field Choices (if needed) + +**File:** `netbox//choices.py` + +```python +class MyModelStatusChoices(ChoiceSet): + STATUS_ACTIVE = 'active' + STATUS_PLANNED = 'planned' + + CHOICES = [ + (STATUS_ACTIVE, _('Active'), 'blue'), + (STATUS_PLANNED, _('Planned'), 'cyan'), + ] +``` + +Reference with `choices=MyModelStatusChoices` on the model field and `choices=MyModelStatusChoices.CHOICES` in forms. + +## 3. Create the FilterSet + +**File:** `netbox//filtersets.py` + +```python +class MyModelFilterSet(PrimaryModelFilterSet): + some_fk = django_filters.ModelMultipleChoiceFilter( + field_name='some_fk__name', + queryset=RelatedModel.objects.all(), + to_field_name='name', + label=_('Related model (name)'), + ) + some_fk_id = django_filters.ModelMultipleChoiceFilter( + queryset=RelatedModel.objects.all(), + label=_('Related model (ID)'), + ) + + class Meta: + model = MyModel + fields = ('id', 'name', 'description') +``` + +**Critical:** Always add both `` (name/slug lookup) and `_id` (PK lookup) for every FK. Do not rely on `Meta.fields` to auto-generate `_id` variants — it won't work correctly. + +Match the base class to the model: `PrimaryModelFilterSet`, `OrganizationalModelFilterSet`, `NetBoxModelFilterSet`, or `ChangeLoggedModelFilterSet`. + +## 4. Create Forms + +**File:** `netbox//forms/model_forms.py` + +```python +class MyModelForm(PrimaryModelForm): + fieldsets = ( + FieldSet('name', 'some_fk', name=_('My Model')), + FieldSet('description', 'tags', name=_('Other')), + ) + + class Meta: + model = MyModel + fields = ('name', 'some_fk', 'description', 'owner', 'comments', 'tags') +``` + +**File:** `netbox//forms/filtersets.py` (for the filter form) + +```python +class MyModelFilterForm(PrimaryModelFilterSetForm): + model = MyModel + fieldsets = ( + FieldSet('q', 'filter_id', 'tag'), + FieldSet('some_fk_id', name=_('Related')), + ) + some_fk_id = DynamicModelMultipleChoiceField( + queryset=RelatedModel.objects.all(), + required=False, + label=_('Related Model'), + ) + tag = TagFilterField(model) +``` + +Match the form base class to the model's base: `PrimaryModelFilterSetForm`, `OrganizationalModelFilterSetForm`, `NestedGroupModelFilterSetForm`, or `NetBoxModelFilterSetForm` (all in `netbox.forms`). + +### Bulk Edit Form — `netbox//forms/bulk_edit.py` + +```python +class MyModelBulkEditForm(PrimaryModelBulkEditForm): + model = MyModel + description = forms.CharField(max_length=200, required=False) + some_fk = DynamicModelChoiceField(queryset=RelatedModel.objects.all(), required=False) + + fieldsets = ( + FieldSet('some_fk', 'description', name=_('My Model')), + ) + nullable_fields = ('description', 'some_fk') +``` + +### Bulk Import Form — `netbox//forms/bulk_import.py` + +```python +class MyModelImportForm(PrimaryModelImportForm): + some_fk = CSVModelChoiceField( + queryset=RelatedModel.objects.all(), + to_field_name='name', + required=False, + ) + + class Meta: + model = MyModel + fields = ('name', 'some_fk', 'description', 'comments', 'tags') +``` + +Use the matching `Primary…` / `Organizational…` / `NestedGroup…` / `NetBoxModel…` variants of `…ImportForm` and `…BulkEditForm` for non-PrimaryModel bases. + +Export each new form from `netbox//forms/__init__.py`. + +## 5. Create the Table + +**File:** `netbox//tables/.py` + +```python +class MyModelTable(PrimaryModelTable): + name = tables.Column(linkify=True) + some_fk = tables.Column(linkify=True) + tags = columns.TagColumn(url_name=':mymodel_list') + + class Meta(PrimaryModelTable.Meta): + model = MyModel + fields = ('pk', 'id', 'name', 'some_fk', 'description', 'tags', 'created', 'last_updated') + default_columns = ('pk', 'name', 'some_fk', 'description') +``` + +Use custom columns provided by NetBox where appropriate. Otherwise, export from the tables package's `__init__.py`. + +## 6. Add Views + +**File:** `netbox//views.py` + +Common imports: + +```python +from extras.ui.panels import CustomFieldsPanel, TagsPanel +from netbox.ui import layout +from netbox.ui.panels import CommentsPanel +from netbox.views import generic +from utilities.views import register_model_view +``` + +```python +@register_model_view(MyModel, 'list', path='', detail=False) +class MyModelListView(generic.ObjectListView): + queryset = MyModel.objects.all() + table = tables.MyModelTable + filterset = filtersets.MyModelFilterSet + filterset_form = forms.MyModelFilterForm + +@register_model_view(MyModel) +class MyModelView(generic.ObjectView): + queryset = MyModel.objects.all() + template_name = 'generic/object.html' # opt out of model-specific template lookup + layout = layout.SimpleLayout( + left_panels=[panels.MyModelPanel(), TagsPanel(), CustomFieldsPanel()], + right_panels=[CommentsPanel()], + ) + +@register_model_view(MyModel, 'add', detail=False) +@register_model_view(MyModel, 'edit') +class MyModelEditView(generic.ObjectEditView): + queryset = MyModel.objects.all() + form = forms.MyModelForm + +@register_model_view(MyModel, 'delete') +class MyModelDeleteView(generic.ObjectDeleteView): + queryset = MyModel.objects.all() + +@register_model_view(MyModel, 'bulk_import', path='import', detail=False) +class MyModelBulkImportView(generic.BulkImportView): + queryset = MyModel.objects.all() + model_form = forms.MyModelImportForm + +@register_model_view(MyModel, 'bulk_edit', path='edit', detail=False) +class MyModelBulkEditView(generic.BulkEditView): + queryset = MyModel.objects.all() + filterset = filtersets.MyModelFilterSet + table = tables.MyModelTable + form = forms.MyModelBulkEditForm + +@register_model_view(MyModel, 'bulk_delete', path='delete', detail=False) +class MyModelBulkDeleteView(generic.BulkDeleteView): + queryset = MyModel.objects.all() + filterset = filtersets.MyModelFilterSet + table = tables.MyModelTable +``` + +`path='import'`/`'edit'`/`'delete'` keep URLs short and match existing apps. If the model has a `name` field amenable to find/replace, also register a `bulk_rename` view (`generic.BulkRenameView`, `path='rename'`). + +Define `MyModelPanel` as an `ObjectAttributesPanel` subclass in `netbox//ui/panels.py` (see `netbox/dcim/ui/panels.py` for examples and the field summary in `add-model-field`). + +## 7. Add URL Routes + +**File:** `netbox//urls.py` + +```python +from utilities.urls import get_model_urls + +urlpatterns = [ + # ...existing routes... + path('my-models/', include(get_model_urls('', 'mymodel', detail=False))), + path('my-models//', include(get_model_urls('', 'mymodel'))), +] +``` + +`get_model_urls()` auto-generates routes for all registered views. `detail=False` covers the list/create routes; the second `path` covers detail/edit/delete routes. + +## 8. REST API + +### Serializer + +Each app has a `netbox//api/serializers_/` package (note the trailing underscore — it's a directory). Add a new module like `mymodel.py` and re-export from `serializers_/__init__.py` (`netbox//api/serializers.py` star-imports each submodule). + +```python +class MyModelSerializer(PrimaryModelSerializer): + some_fk = RelatedModelSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = MyModel + fields = [ + 'id', 'url', 'display_url', 'display', + 'name', 'some_fk', + 'description', 'owner', 'comments', 'tags', 'custom_fields', + 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') +``` + +NetBox serializers use a single FK field with `nested=True` — no separate `_id` companion. Pass `nested=True` when the related serializer is referenced by another serializer; the framework renders it as a brief representation when reading and accepts a primary key (or brief object) when writing. Match the base class to the model: `PrimaryModelSerializer`, `OrganizationalModelSerializer`, `NestedGroupModelSerializer`, `NetBoxModelSerializer`. + +### ViewSet + +**File:** `netbox//api/views.py` + +```python +class MyModelViewSet(NetBoxModelViewSet): + queryset = MyModel.objects.all() + serializer_class = serializers.MyModelSerializer + filterset_class = filtersets.MyModelFilterSet +``` + +Skip `prefetch_related()` on the queryset — `NetBoxModelViewSet` resolves prefetches dynamically based on the serializer. + +### API URL Route + +**File:** `netbox//api/urls.py` + +```python +router.register('my-models', views.MyModelViewSet) +``` + +## 9. GraphQL + +### Filter + +**File:** `netbox//graphql/filters.py` + +```python +@strawberry_django.filter_type(models.MyModel, lookups=True) +class MyModelFilter(PrimaryModelFilter): + name: StrFilterLookup[str] | None = strawberry_django.filter_field() + some_fk: Annotated['RelatedModelFilter', strawberry.lazy('.graphql.filters')] | None = strawberry_django.filter_field() + some_fk_id: ID | None = strawberry_django.filter_field() +``` + +Add `'MyModelFilter'` to `__all__` at the top of the file. + +### Type + +**File:** `netbox//graphql/types.py` + +```python +@strawberry_django.type( + models.MyModel, + fields='__all__', + filters=MyModelFilter, + pagination=True, +) +class MyModelType(PrimaryObjectType): + some_fk: Annotated['RelatedModelType', strawberry.lazy('.graphql.types')] | None +``` + +Add `'MyModelType'` to `__all__`. + +### Schema + +**File:** `netbox//graphql/schema.py` + +```python +@strawberry.type +class MyAppQuery: + # ...existing fields... + my_model: MyModelType = strawberry_django.field() + my_model_list: list[MyModelType] = strawberry_django.field() +``` + +> **Note:** GraphQL unit tests may fail citing null values on a non-nullable field if related objects are prefetched. Fix by using `= strawberry_django.field(select_related=['some_fk'])` instead. + +## 10. Register in Search + +**File:** `netbox//search.py` + +```python +@register_search +class MyModelIndex(SearchIndex): + model = models.MyModel + fields = ( + ('name', 100), + ('description', 500), + ('comments', 5000), + ) + display_attrs = ('some_fk', 'description') +``` + +Field weights: lower = higher priority in results. Typical: name=100, description=500, comments=5000. + +## 11. Add Navigation Menu Entry + +**File:** `netbox/netbox/navigation/menu.py` + +Find the relevant `MenuGroup` and add: + +```python +get_model_item('', 'mymodel', _('My Models')), +``` + +The model name must be lowercase (not the URL slug). This auto-links to the list view. + +## 12. Add Documentation + +**File:** `docs/models//.md` (filename is the lowercase model name with no separators, e.g. `virtualchassis.md`). + +Include at minimum: +- A description of what the model represents +- A `## Fields` section with a subsection per field (see `docs/models/dcim/site.md` for the canonical structure) + +Then register the page in two indexes: + +- `mkdocs.yml` — add a line under the appropriate `nav:` group (e.g. `- MyModel: 'models//mymodel.md'`) +- `docs/development/models.md` — add to the relevant model-type list under "Models Index" (Primary, Organizational, Nested Group, etc.) + +There is no per-app `index.md` under `docs/models/` — `mkdocs.yml` is the single source of truth for navigation. + +## 13. Write Tests + +### API Tests + +**File:** `netbox//tests/test_api.py` + +```python +class MyModelTest(APIViewTestCases.APIViewTestCase): + model = MyModel + brief_fields = ['description', 'display', 'id', 'name', 'url'] + + @classmethod + def setUpTestData(cls): + # Create 3+ instances for list/bulk tests + my_models = ( + MyModel(name='My Model 1', ...), + MyModel(name='My Model 2', ...), + MyModel(name='My Model 3', ...), + ) + MyModel.objects.bulk_create(my_models) + + cls.create_data = [ + {'name': 'My Model 4', ...}, + {'name': 'My Model 5', ...}, + {'name': 'My Model 6', ...}, + ] +``` + +### View Tests + +**File:** `netbox//tests/test_views.py` + +```python +class MyModelTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = MyModel + + @classmethod + def setUpTestData(cls): + my_models = ( + MyModel(name='My Model 1', ...), + MyModel(name='My Model 2', ...), + MyModel(name='My Model 3', ...), + ) + MyModel.objects.bulk_create(my_models) + + cls.form_data = { + 'name': 'My Model X', + # all required form fields + } + cls.bulk_edit_data = { + 'description': 'New description', + } + cls.csv_data = ( + 'name', + 'My Model 4', + 'My Model 5', + 'My Model 6', + ) +``` + +### FilterSet Tests + +**File:** `netbox//tests/test_filtersets.py` + +```python +from utilities.testing import ChangeLoggedFilterSetTests + +class MyModelFilterSetTestCase(TestCase, ChangeLoggedFilterSetTests): + queryset = MyModel.objects.all() + filterset = MyModelFilterSet + + @classmethod + def setUpTestData(cls): + # Create diverse test data + + def test_name(self): + params = {'name': ['My Model 1', 'My Model 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_some_fk(self): + # Test FK and FK_id filters +``` + +`ChangeLoggedFilterSetTests` provides standard tests for `id`, `created`, `last_updated`, `q` search, etc. Always mix it in. + +## Common Gotchas + +- **Never write migrations manually.** Always run `python netbox/manage.py makemigrations` and let Django generate them. Set `DEVELOPER = True` in `configuration.py` to enable this. +- **FK filters need explicit `_id` variants** in FilterSets. `Meta.fields` does not auto-generate them. +- **`manage.py` lives in `netbox/`**, not the repo root. +- **Brief fields** in API serializers must be declared explicitly via `brief_fields` on the `Meta` class; they are used for nested representations. +- **GraphQL null prefetch failures**: if tests fail on non-nullable fields, add `select_related=[...]` to the `strawberry_django.field()` call. +- **Template**: by default `generic.ObjectView` auto-resolves to `/.html`. If you only define a panel-driven `layout`, set `template_name = 'generic/object.html'` on the view to opt out of that lookup. Add a real per-model template only when you need markup that panels can't express. +- **Serializer FK fields**: write a single field like `some_fk = RelatedModelSerializer(nested=True, ...)` — do **not** add a separate `some_fk_id` companion. The framework accepts a PK or brief object on write. +- **Modern pattern check**: cargo-culting older nested serializer code (`NestedFooSerializer(read_only=True)` plus `_id` field) is wrong for new code — use the `nested=True` form. +- **`PrimaryModel`** already has `description`, `comments`, `owner`. Don't re-add them. +- **No `ruff format`** on existing files. Use ruff check only. + +## References + +- Model base classes: `netbox/netbox/models/__init__.py` +- Concrete example (VirtualChassis): `netbox/dcim/models/devices.py`, `netbox/dcim/filtersets.py`, `netbox/dcim/api/`, `netbox/dcim/graphql/`, `netbox/dcim/tests/` +- Contributing guide: `docs/development/adding-models.md` +- Navigation menu: `netbox/netbox/navigation/menu.py` diff --git a/.claude/skills/remove-config-param/SKILL.md b/.claude/skills/remove-config-param/SKILL.md new file mode 100644 index 000000000..fecbc6494 --- /dev/null +++ b/.claude/skills/remove-config-param/SKILL.md @@ -0,0 +1,168 @@ +--- +name: remove-config-param +description: Step-by-step guide for removing a configuration parameter from NetBox, covering both static parameters (settings.py) and dynamic parameters (database-backed). Use when the user asks to remove, delete, or deprecate a configuration option or setting. +--- + +# Removing a Configuration Parameter from NetBox + +Before touching any files, determine which type of parameter you are removing: + +| Type | Where defined | How to tell | +|---|---|---| +| **Static** | `settings.py` via `getattr(configuration, ...)` | Appears in `settings.py`; not in `config/parameters.py` `PARAMS` | +| **Dynamic** | `config/parameters.py` `PARAMS` tuple | Appears in `PARAMS`; editable via Admin > System > Configuration History | + +Run a broad grep before starting to find all usages: + +```bash +grep -r 'MY_PARAM' netbox/ --include='*.py' -l +grep -r 'MY_PARAM' docs/ -l +``` + +--- + +## Removing a Dynamic Configuration Parameter + +### Step 1 — Find all usages in code + +Before removing the parameter definition, identify every call site: + +```bash +grep -r 'MY_PARAM\|my_param' netbox/ --include='*.py' +``` + +For `get_config().MY_PARAM` and `ConfigItem('MY_PARAM')` patterns specifically: + +```bash +grep -r "get_config()\.MY_PARAM\|ConfigItem('MY_PARAM')" netbox/ --include='*.py' +``` + +Remove or replace every usage. The replacement depends on the reason for removal: +- **Parameter folded into another**: replace with the new parameter access +- **Hard-coded default**: replace `get_config().MY_PARAM` with the literal default value +- **Feature removed**: remove the surrounding code entirely + +### Step 2 — Remove from `PARAMS` + +**File:** `netbox/netbox/config/parameters.py` + +Delete the `ConfigParam(...)` block for the parameter from the `PARAMS` tuple. + +### Step 3 — Remove from the dynamic params index + +**File:** `docs/configuration/index.md` + +Remove the bullet-point entry for `MY_PARAM` from the "Dynamic Configuration Parameters" list. + +### Step 4 — Remove the documentation section + +**File:** `docs/configuration/.md` (whichever file the parameter was documented in) + +Delete the `## MY_PARAM` section and its content, including the trailing `---` separator. + +### Step 5 — Remove from the example config (if present) + +**File:** `netbox/netbox/configuration_example.py` + +If a commented `# MY_PARAM = ...` line was added when the parameter was introduced, remove it. + +### No migration needed + +Dynamic parameters are stored as keys in the `ConfigRevision.data` JSONField. Removing the `ConfigParam` definition from `PARAMS` means the UI no longer shows the field and the `Config` object no longer exposes the attribute — but old `ConfigRevision` rows in the database will silently retain the key in their JSON blob. This is harmless and requires no migration. + +--- + +## Removing a Static Configuration Parameter + +### Step 1 — Find all usages in code + +```bash +grep -r 'MY_PARAM' netbox/ --include='*.py' +``` + +Remove every reference. For Django settings accessed via `settings.MY_PARAM`, also search templates: + +```bash +grep -r 'MY_PARAM' netbox/templates/ +``` + +### Step 2 — Remove from `settings.py` + +**File:** `netbox/netbox/settings.py` + +1. Delete the `MY_PARAM = getattr(configuration, 'MY_PARAM', ...)` line. +2. If the parameter was required (listed in the required-parameter check near the top), remove it from that tuple: + ```python + # Before: + for parameter in ('ALLOWED_HOSTS', 'MY_PARAM', 'SECRET_KEY', 'REDIS'): + # After: + for parameter in ('ALLOWED_HOSTS', 'SECRET_KEY', 'REDIS'): + ``` +3. Remove any validation block that immediately followed the `getattr` line (e.g. `if MY_PARAM not in (...): raise ImproperlyConfigured(...)`). + +### Step 3 — Remove from the example config + +**File:** `netbox/netbox/configuration_example.py` + +Delete the commented `# MY_PARAM = ...` line. + +### Step 4 — Remove the documentation section + +**File:** `docs/configuration/.md` + +Delete the `## MY_PARAM` section and its content, including the trailing `---` separator. + +--- + +## Deprecation vs. Immediate Removal + +If the parameter is used by existing deployments, consider a two-phase removal: + +**Phase 1 (current release) — Deprecate:** +1. Keep the `getattr` / `ConfigParam` definition in place so existing configs don't break. +2. Add a deprecation warning comment in `settings.py` (see how `SENTRY_DSN` is handled with `# TODO: Remove in NetBox vX.Y`). +3. Log a `warnings.warn(...)` or add a startup notice if the parameter is still set. +4. Mark the doc section as deprecated. + +**Phase 2 (future release) — Remove:** +Follow the full removal steps above. + +--- + +## Common Gotchas + +- **Remove all call sites first** — if code still calls `get_config().MY_PARAM` or `settings.MY_PARAM` after the definition is gone, startup or runtime will raise `AttributeError`. +- **Old `ConfigRevision` rows retain the key in their JSON blob** — this is harmless and requires no migration. The risk is code: any remaining call to `get_config().MY_PARAM` or `settings.MY_PARAM` after the definition is gone will raise `AttributeError`. Remove all code references *before* removing the `ConfigParam` definition. +- **`configuration.py` in user deployments** — removing a static parameter may cause a `TypeError` or silent failure if users have `MY_PARAM = ...` in their local `configuration.py`. Document the removal in the release notes. +- **No `ruff format`** on existing files — use `ruff check` only. + +## Summary Checklist + +### Dynamic parameter + +| # | File(s) | Action | +|---|---|---| +| 1 | All `.py` files | Remove all `get_config().MY_PARAM` and `ConfigItem('MY_PARAM')` usages | +| 2 | `netbox/netbox/config/parameters.py` | Remove `ConfigParam(...)` block from `PARAMS` | +| 3 | `docs/configuration/index.md` | Remove bullet-point entry | +| 4 | `docs/configuration/.md` | Remove `## MY_PARAM` section | +| 5 | `netbox/netbox/configuration_example.py` | Remove commented entry (if present) | + +### Static parameter + +| # | File(s) | Action | +|---|---|---| +| 1 | All `.py` and template files | Remove all `settings.MY_PARAM` / `MY_PARAM` usages | +| 2 | `netbox/netbox/settings.py` | Remove `getattr` line; remove from required-params tuple; remove validation block | +| 3 | `netbox/netbox/configuration_example.py` | Remove commented entry | +| 4 | `docs/configuration/.md` | Remove `## MY_PARAM` section | + +## References + +- Dynamic param definitions: `netbox/netbox/config/parameters.py` +- Config loading / `Config` class: `netbox/netbox/config/__init__.py` +- `ConfigRevision` model: `netbox/core/models/config.py` +- Static config loading: `netbox/netbox/settings.py` lines 67–213 +- Example config: `netbox/netbox/configuration_example.py` +- Documentation: `docs/configuration/` +- `add-config-param` skill: `.claude/skills/add-config-param/SKILL.md` (reverse of this skill) diff --git a/.claude/skills/remove-model-field/SKILL.md b/.claude/skills/remove-model-field/SKILL.md new file mode 100644 index 000000000..1e23c1860 --- /dev/null +++ b/.claude/skills/remove-model-field/SKILL.md @@ -0,0 +1,217 @@ +--- +name: remove-model-field +description: Step-by-step checklist for removing a field from an existing NetBox model, covering all required touch points (model, migration, serializer, forms, filterset, table, panel/template, search, GraphQL, tests, docs). Use when the user asks to remove or delete a field or attribute from an existing model. +--- + +# Removing a Field from an Existing NetBox Model + +Removing a field touches many files. Work through the checklist below in order — remove outer consumers first (tests, docs, GraphQL, API, forms) before touching the model definition itself. + +## Before You Start + +Determine upfront: +- **Field name** and which **model/app** owns it +- **Field type**: scalar, FK/M2M, GenericForeignKey, or special (JSONField, etc.) +- **All references** — run a broad grep before touching anything: + +```bash +grep -r 'new_field\|related_thing' netbox/ --include='*.py' -l +grep -r 'new_field\|related_thing' docs/ -l +``` + +For FK/M2M fields, also check for FilterSet `_id` companions and GraphQL lazy annotations referencing this field. + +**Check dependents**: if other models or code use this field (e.g. ordering, constraints, signal handlers), those references must be cleaned up too. + +## 1. Update Tests + +Update test files to remove references to the field being deleted. Specifically: + +- **`tests/test_filtersets.py`** — remove `test_` and `test__id` methods; remove the field from `setUpTestData` test objects. +- **`tests/test_api.py`** — remove the field from `setUpTestData`, `create_data`, and `bulk_update_data`; remove any `test_list_objects_by_` methods. +- **`tests/test_views.py`** — remove the field from `form_data`, `bulk_edit_data`, and `csv_data` in `setUpTestData`. +- **`tests/test_models.py`** — remove any `test_clean_` or constraint tests specific to this field. + +## 2. Update Documentation + +**File:** `docs/models//.md` + +Remove the field's entry from the `## Fields` section. If the field had any cross-references in other doc pages, remove those too. + +## 3. Update GraphQL + +### Filter — `graphql/filters.py` + +Remove the filter field declaration(s) for the deleted field: + +```python +# Remove lines like: +new_field: StrFilterLookup[str] | None = strawberry_django.filter_field() + +# Or for FK: +related_thing: Annotated[...] | None = strawberry_django.filter_field() +related_thing_id: ID | None = strawberry_django.filter_field() +``` + +### Type — `graphql/types.py` + +For simple fields, `fields='__all__'` means no change is needed — the field disappears automatically once removed from the model. + +For FK fields with an explicit annotation, remove the annotation line: + +```python +# Remove: +related_thing: Annotated['RelatedThingType', strawberry.lazy('.graphql.types')] | None +``` + +If the field was in an `exclude` list, remove it from the exclude list (it no longer exists to exclude). + +## 4. Update the API Serializer + +**File:** `netbox//api/serializers_/.py` + +- **Simple field**: remove the field name from `Meta.fields` (and `brief_fields` if present). +- **FK field**: remove the serializer field declaration and its name from `Meta.fields`: + +```python +# Remove: +related_thing = RelatedThingSerializer(nested=True, required=False, allow_null=True) +# And remove 'related_thing' from Meta.fields +``` + +## 5. Update Forms + +There are typically up to four forms to update. Find them under `netbox//forms/`. + +### 5a. Filter form — `forms/filtersets.py` + +- Remove the field from `fieldsets`. +- Remove the filter field declaration (e.g. `new_field = forms.CharField(...)` or the `DynamicModelMultipleChoiceField`). + +### 5b. Bulk edit form — `forms/bulk_edit.py` + +- Remove the field from `fieldsets` and `Meta.fields` (if present). +- Remove the field declaration. +- Remove from `nullable_fields` if listed there. + +### 5c. Bulk import form — `forms/bulk_import.py` + +- Remove from `Meta.fields`. +- Remove any explicit field declaration. + +### 5d. Model form — `model_forms.py` + +- Remove from `fieldsets`. +- Remove from `Meta.fields`. +- Remove any explicit field declaration (e.g. a `DynamicModelChoiceField`). + +## 6. Update the FilterSet + +**File:** `netbox//filtersets.py` + +- **Simple field**: remove from `Meta.fields`. +- **FK field**: remove both the `` and `_id` explicit filter declarations. +- **`search()` method**: if the field was included in the `Q(...)` chain, remove that clause. +- Remove any now-unused imports (e.g. the related model import if it was only used by this filter). + +## 7. Update the Table + +**File:** `netbox//tables/.py` + +- Remove the column declaration (e.g. `related_thing = tables.Column(linkify=True)`). +- Remove the field from `Meta.fields`. +- Remove from `default_columns` if listed there. + +## 8. Update the Detail View Panel + +**File:** `netbox//ui/panels.py` + +Find the panel class for the model and remove the attribute declaration: + +```python +# Remove: +new_field = attrs.TextAttr('new_field') +related_thing = attrs.RelatedObjectAttr('related_thing', linkify=True) +``` + +If the model uses a legacy HTML template (`netbox/templates//`) rather than a declarative panel, remove the corresponding `` row from that template instead. + +## 9. Update the SearchIndex + +**File:** `netbox//search.py` + +If the field was indexed for global search, remove it from the `fields` tuple: + +```python +# Remove: +('new_field', 300), +``` + +## 10. Remove the Field from the Model + +**File:** `netbox//models/.py` + +1. Delete the field declaration. +2. If the field was in `clone_fields`, remove it from that tuple. +3. If `clean()` had validation logic specific to this field, remove those clauses. If `clean()` becomes empty, remove the override entirely. +4. For FK fields: remove the `related_name` on the target model is automatic (Django handles it). If the FK was the only reason a related model was imported, remove that import too. +5. Check `Meta` for references to the field: + - `ordering` — if the field appears in the ordering tuple, remove it (or replace with a remaining field if ordering would otherwise become empty). + - `constraints` — remove any `UniqueConstraint` or `CheckConstraint` whose `fields` list includes this field; if only this field remains, remove the constraint entirely; if other fields remain, remove just this field from the list. + - `indexes` — remove any `models.Index` that includes this field. +6. For GenericForeignKey fields: if this was the only GFK, also remove the `object_type` ContentType FK and `object_id` integer field, and remove the `models.Index(fields=('object_type', 'object_id'))` from `Meta`. + +## 11. Generate the Migration + +**Do NOT write migrations manually.** Tell the user to run: + +```bash +cd netbox/ +python manage.py makemigrations -n remove__from_ --no-header +``` + +Set `DEVELOPER = True` in `configuration.py` if the command is blocked. + +Review the generated migration — it should contain only a `RemoveField` operation (plus any index removal for GFK fields). Apply with: + +```bash +python manage.py migrate +``` + +## Summary Checklist + +| # | File(s) | Action | +|---|---|---| +| 1 | `tests/test_*.py` | Remove field from test data, filter tests, API tests, view tests | +| 2 | `docs/models//.md` | Remove field from `## Fields` section | +| 3 | `graphql/filters.py`, `types.py` | Remove filter field; remove FK annotation if explicit | +| 4 | `api/serializers_/.py` | Remove from `Meta.fields`; remove FK serializer field | +| 5a | `forms/filtersets.py` | Remove from `fieldsets`; remove filter field declaration | +| 5b | `forms/bulk_edit.py` | Remove from `fieldsets`, `Meta.fields`, `nullable_fields` | +| 5c | `forms/bulk_import.py` | Remove from `Meta.fields` and field declaration | +| 5d | `forms/model_forms.py` | Remove from `fieldsets`, `Meta.fields`, and field declaration | +| 6 | `filtersets.py` | Remove from `Meta.fields`; remove FK + FK_id pair; update `search()` | +| 7 | `tables/.py` | Remove column declaration and from `Meta.fields`, `default_columns` | +| 8 | `/ui/panels.py` | Remove attr declaration from panel class | +| 9 | `search.py` | Remove from SearchIndex `fields` tuple | +| 10 | `models/.py` | Remove field; clean up `clone_fields`, `clean()`, `Meta` ordering/constraints/indexes, imports | +| 11 | (user runs) | `makemigrations -n remove__from_ --no-header` then `migrate` | + +## Common Gotchas + +- **Work outside-in** — remove tests, docs, GraphQL, and API references before touching the model, to avoid import errors during the process. +- **FK fields leave no `_id` companion in serializers** — the modern pattern uses a single `field = Serializer(nested=True)`. Grep for the field name and the serializer class name. +- **FilterSets have both `` and `_id`** — both must be removed; they are explicit declarations, not auto-generated. +- **`clone_fields`** must be updated if the field was listed there. +- **`search()` in filtersets** — if the field was in the `Q(...)` chain of the `search()` method, that clause must be removed to avoid a `FieldError` at runtime. +- **`brief_fields` in serializers** — remove explicitly if the field was listed. +- **`makemigrations` must be run**, not written manually. If blocked, set `DEVELOPER = True` in `configuration.py`. +- **No `ruff format`** on existing files — use `ruff check` only. + +## References + +- Panel attrs reference: `netbox/netbox/ui/attrs.py` +- Panel classes: `netbox//ui/panels.py` +- Base filterset classes: `netbox/netbox/filtersets.py` +- `add-model-field` skill: `.claude/skills/add-model-field/SKILL.md` (reverse of this skill) +- Contributing guide: `docs/development/extending-models.md` diff --git a/.claude/skills/remove-model/SKILL.md b/.claude/skills/remove-model/SKILL.md new file mode 100644 index 000000000..a86d6e068 --- /dev/null +++ b/.claude/skills/remove-model/SKILL.md @@ -0,0 +1,194 @@ +--- +name: remove-model +description: Step-by-step guide for removing an existing model from NetBox, covering all required touch points in safe deletion order (tests, docs, nav, search, GraphQL, API, views, URLs, forms, filterset, table, choices, model, migration). Use when the user asks to remove, delete, or deprecate a model or object type from NetBox. +--- + +# Removing a Model from NetBox + +Removing a model requires undoing ~13 components. Work in the order below — remove consumers before providers to avoid import errors during the process. Deleting a model is **irreversible once migrated**; confirm with the user before running `makemigrations`. + +## 0. Before You Start + +Identify: +- **Model name** and **app** — e.g. `MyModel` in `dcim` +- **All references** — run a broad grep before touching anything: + +```bash +grep -r 'MyModel\|mymodel\|my-model\|my_model' netbox/ --include='*.py' -l +grep -r 'MyModel\|mymodel\|my-model\|my_model' docs/ -l +grep -r 'mymodel\|my-model' netbox/netbox/navigation/ --include='*.py' +``` + +Check for: +- Other models with ForeignKey / M2M pointing to this model (they need updating or their own removal first) +- Generic relations via `FeatureQuery` or `ContentType` that reference this model +- Any plugin or external code documented as depending on this model + +**Do not proceed if other retained models have non-nullable FKs to this model** — those FK fields must be removed or made nullable first. + +## 1. Remove Tests + +Delete test methods or entire test classes that exist solely for this model. If the test file contains only this model's tests, delete the file; otherwise remove just the relevant class(es). + +Files to check: +- `netbox//tests/test_api.py` +- `netbox//tests/test_views.py` +- `netbox//tests/test_filtersets.py` +- `netbox//tests/test_models.py` +- `netbox//tests/test_forms.py` +- `netbox//tests/test_tables.py` +- Any app-specific test modules (e.g. `test_cablepaths.py`) + +## 2. Remove Documentation + +1. Delete `docs/models//.md`. +2. Remove the `mkdocs.yml` entry under the relevant `nav:` group. +3. Remove the entry from `docs/development/models.md` (the "Models Index" list). + +## 3. Remove Navigation Menu Entry + +**File:** `netbox/netbox/navigation/menu.py` + +Remove the `get_model_item('', 'mymodel', ...)` line from the relevant `MenuGroup`. + +## 4. Remove from Search Index + +**File:** `netbox//search.py` + +Delete the `@register_search` class for the model. If the file becomes empty (no other indexes), delete the file itself. + +## 5. Remove GraphQL + +Remove in this order (schema depends on types, types depend on filters): + +1. **`netbox//graphql/schema.py`** — remove the `my_model` and `my_model_list` fields from the app's `Query` type. +2. **`netbox//graphql/types.py`** — remove the `MyModelType` class and its `__all__` entry. +3. **`netbox//graphql/filters.py`** — remove the `MyModelFilter` class and its `__all__` entry. + +If any remaining type in `types.py` has a lazy annotation referencing `MyModelType`, remove that annotation too. + +## 6. Remove REST API + +1. **`netbox//api/urls.py`** — remove the `router.register('my-models', ...)` line. +2. **`netbox//api/views.py`** — remove the `MyModelViewSet` class. +3. **`netbox//api/serializers_/.py`** — remove the serializer class. If this was the only serializer in the module, delete the file and remove its `from . import *` line from `serializers_/__init__.py`. + +Also check other serializers that reference this model (e.g. `MyModelSerializer(nested=True)` on related serializers) and remove those fields too. + +## 7. Remove URL Routes + +**File:** `netbox//urls.py` + +Remove the two `path(...)` entries that call `get_model_urls('', 'mymodel', ...)`. + +## 8. Remove Views + +**File:** `netbox//views.py` + +Remove all view classes decorated with `@register_model_view(MyModel, ...)`. There are typically seven: + +- `MyModelListView` +- `MyModelView` +- `MyModelEditView` +- `MyModelDeleteView` +- `MyModelBulkImportView` +- `MyModelBulkEditView` +- `MyModelBulkDeleteView` +- `MyModelBulkRenameView` (if present) + +Also remove the panel class from `netbox//ui/panels.py` and any `layout` references using it. + +If there is a model-specific HTML template (`netbox/templates//mymodel.html` or similar), delete it. + +## 9. Remove Table + +**File:** `netbox//tables/.py` + +Remove the `MyModelTable` class. If it is the sole table in the module, delete the file and clean up the `__init__.py` re-export. + +**File:** `netbox//tables/__init__.py` + +Remove the corresponding `from . import *` or named import. + +## 10. Remove Forms + +Remove in dependency order (bulk forms depend on the model form): + +1. **`netbox//forms/bulk_import.py`** — remove `MyModelImportForm`. +2. **`netbox//forms/bulk_edit.py`** — remove `MyModelBulkEditForm`. +3. **`netbox//forms/filtersets.py`** — remove `MyModelFilterForm`. +4. **`netbox//forms/model_forms.py`** — remove `MyModelForm`. +5. **`netbox//forms/__init__.py`** — remove all re-exports of the deleted form classes. + +## 11. Remove FilterSet + +**File:** `netbox//filtersets.py` + +Remove the `MyModelFilterSet` class. Also remove any imports of `MyModel` or related models that were only used by this filterset. + +## 12. Remove Choices + +**File:** `netbox//choices.py` + +Remove any `ChoiceSet` subclasses that were defined exclusively for this model (e.g. `MyModelStatusChoices`). Leave choices that are shared with other models. + +## 13. Remove the Model + +**File:** `netbox//models/.py` (or `models.py`) + +1. Delete the `MyModel` class. +2. Remove `'MyModel'` from `__all__` in the module. +3. Remove the import line in `netbox//models/__init__.py` if this was the last model in the submodule (or remove just the `MyModel` name from a `from . import ...` line). +4. Remove any now-unused imports in the model file itself. + +## 14. Generate the Migration + +**Do NOT write migrations manually.** Tell the user to run: + +```bash +cd netbox/ +python manage.py makemigrations -n remove_mymodel --no-header +``` + +Set `DEVELOPER = True` in `configuration.py` if the command is blocked. + +Review the generated migration before applying — it should only contain a `DeleteModel` operation (plus any `RemoveField` operations for FKs on other models if Django detected them). Apply with: + +```bash +python manage.py migrate +``` + +## Common Gotchas + +- **Remove consumers before providers** — tests, docs, GraphQL schema, API viewset, URL routes, and views all reference the model; remove them before removing the model itself to avoid import errors. +- **FK cleanup** — Django will detect FKs pointing at the deleted model and auto-add `RemoveField` operations to the migration. Verify the migration is correct before running it. +- **ContentType cleanup** — after migrating, `ContentType` rows for the old model linger in the database. They are harmless but can be cleaned up with `python manage.py remove_stale_contenttypes`. +- **`__all__` entries** — grep all `__init__.py` files for the model name after removing the class; dangling re-exports cause `ImportError` at startup. +- **Serializer references** — other serializers may have a nested `MyModelSerializer(nested=True)` field. Search for the serializer class name as well as the model name. +- **`manage.py` lives in `netbox/`**, not the repo root. +- **No `ruff format`** on existing files — use `ruff check` only. + +## Summary Checklist + +| # | File(s) | Action | +|---|---|---| +| 1 | `tests/test_*.py` | Remove test classes for this model | +| 2 | `docs/models//.md`, `mkdocs.yml`, `docs/development/models.md` | Delete doc page; remove nav entries | +| 3 | `netbox/netbox/navigation/menu.py` | Remove `get_model_item(...)` line | +| 4 | `/search.py` | Remove `SearchIndex` class | +| 5 | `/graphql/schema.py`, `types.py`, `filters.py` | Remove query fields, type, filter | +| 6 | `/api/urls.py`, `views.py`, `serializers_/.py` | Remove router entry, viewset, serializer | +| 7 | `/urls.py` | Remove `get_model_urls(...)` paths | +| 8 | `/views.py`, `/ui/panels.py` | Remove all view classes and panel | +| 9 | `/tables/.py`, `tables/__init__.py` | Remove table class and re-export | +| 10 | `/forms/*.py`, `forms/__init__.py` | Remove all four form classes and re-exports | +| 11 | `/filtersets.py` | Remove `FilterSet` class | +| 12 | `/choices.py` | Remove model-specific `ChoiceSet` subclasses | +| 13 | `/models/.py`, `models/__init__.py` | Remove model class and `__all__` entry | +| 14 | (user runs) | `makemigrations -n remove_mymodel --no-header` then `migrate` | + +## References + +- Model base classes: `netbox/netbox/models/__init__.py` +- Navigation menu: `netbox/netbox/navigation/menu.py` +- `add-model` skill: `.claude/skills/add-model/SKILL.md` (reverse of this skill) diff --git a/.claude/skills/run-tests/SKILL.md b/.claude/skills/run-tests/SKILL.md new file mode 100644 index 000000000..a42c68fb1 --- /dev/null +++ b/.claude/skills/run-tests/SKILL.md @@ -0,0 +1,92 @@ +--- +name: run-tests +description: Run NetBox's Django test suite locally. Use when the user asks to run tests, run a specific test module/class/method, or verify changes pass before opening a PR. +--- + +# Run the NetBox test suite + +NetBox uses `django.test.TestCase` (not pytest). The suite is invoked via `manage.py test` from the repo root. CI runs this exact command in `.github/workflows/ci.yml`. + +## Canonical command + +From the repo root, with the venv active: + +```bash +NETBOX_CONFIGURATION=netbox.configuration_testing python netbox/manage.py test netbox/ --parallel +``` + +`--parallel` runs test processes in parallel and is used in CI. Drop it to debug failures that only appear in parallel mode. + +## Prerequisites + +1. PostgreSQL and Redis reachable on localhost at their default ports (credentials: `netbox`/`netbox`/`netbox`). +2. `configuration.py` in place — copy from the example and fill in DATABASE, REDIS, SECRET_KEY, ALLOWED_HOSTS. This file is gitignored and must never be committed. +3. Dependencies installed: `pip install -r requirements.txt`. +4. `NETBOX_CONFIGURATION` set to `netbox.configuration_testing` — the test config sets `DATABASES`, `REDIS`, and `PLUGINS` appropriately. + +If any of these are missing, surface the gap to the user — do not silently skip. + +## Useful variants + +Run a single app's tests: + +```bash +NETBOX_CONFIGURATION=netbox.configuration_testing python netbox/manage.py test dcim --parallel +``` + +Run a single module, class, or method (Django dotted-path target): + +```bash +NETBOX_CONFIGURATION=netbox.configuration_testing python netbox/manage.py test dcim.tests.test_api +NETBOX_CONFIGURATION=netbox.configuration_testing python netbox/manage.py test dcim.tests.test_api.RackTestCase +NETBOX_CONFIGURATION=netbox.configuration_testing python netbox/manage.py test dcim.tests.test_api.RackTestCase.test_list_objects +``` + +Speed options: + +- `--keepdb` — skip DB rebuild between runs (safe for most iterative work) +- `--parallel` — run tests in parallel across CPU cores (used in CI; don't combine with `--keepdb` without testing first) +- `--failfast` — stop on first failure +- `-v 2` — print each test name as it runs + +## Standard test modules per app + +| Module | Coverage area | +|---|---| +| `test_api.py` | REST API endpoints (CRUD, filtering, bulk operations) | +| `test_filtersets.py` | FilterSet fields and query behavior | +| `test_models.py` | Model methods, validation, constraints | +| `test_views.py` | UI views (list, create, edit, delete, bulk actions) | +| `test_forms.py` | Form validation | +| `test_tables.py` | Table column rendering | + +Specialized modules in some apps: `test_cablepaths.py` (dcim), `test_lookups.py` (ipam). + +## After model changes + +Always generate migrations before running tests; the test DB build will fail if migrations are missing: + +```bash +python netbox/manage.py makemigrations +``` + +Never write migrations manually — let Django generate them. + +## Coverage (matches CI) + +```bash +coverage run --source="netbox/" netbox/manage.py test netbox/ --parallel +coverage report --skip-covered --omit '*/migrations/*,*/tests/*' +``` + +## Why these choices + +- **Don't substitute pytest.** The suite uses `django.test.TestCase`; switching to pytest requires `pytest-django` configured against NetBox's settings, which is not set up. Run via `manage.py test` to match CI. +- **Always set `NETBOX_CONFIGURATION`.** Without it, Django loads `configuration.py` (the production config), which likely has a different database or may not exist in dev environments. +- **`--parallel` for full-suite runs.** CI runs parallel; running without it locally can mask race conditions (rare) and is slower on multi-core machines. + +## References + +- [`AGENTS.md`](../../../AGENTS.md) — Testing and development sections. +- [`.github/workflows/ci.yml`](../../../.github/workflows/ci.yml) — Authoritative CI invocation. +- [`netbox/netbox/configuration_testing.py`](../../../netbox/netbox/configuration_testing.py) — Test configuration used by the runner. diff --git a/.github/ISSUE_TEMPLATE/01-feature_request.yaml b/.github/ISSUE_TEMPLATE/01-feature_request.yaml index 664bea5c6..59a95a5cd 100644 --- a/.github/ISSUE_TEMPLATE/01-feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/01-feature_request.yaml @@ -15,7 +15,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v4.6.0 + placeholder: v4.6.2 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/02-bug_report.yaml b/.github/ISSUE_TEMPLATE/02-bug_report.yaml index 59fd99d0f..c222ea3d2 100644 --- a/.github/ISSUE_TEMPLATE/02-bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/02-bug_report.yaml @@ -27,7 +27,7 @@ body: attributes: label: NetBox Version description: What version of NetBox are you currently running? - placeholder: v4.6.0 + placeholder: v4.6.2 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/03-performance.yaml b/.github/ISSUE_TEMPLATE/03-performance.yaml index 727f39b2f..ca6cec3de 100644 --- a/.github/ISSUE_TEMPLATE/03-performance.yaml +++ b/.github/ISSUE_TEMPLATE/03-performance.yaml @@ -8,7 +8,7 @@ body: attributes: label: NetBox Version description: What version of NetBox are you currently running? - placeholder: v4.6.0 + placeholder: v4.6.2 validations: required: true - type: dropdown diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0bbbe90c7..f45f04103 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,16 +1,14 @@ -### Fixes: #1234 +### Closes: #1234 DeviceType & ModuleType + ModuleTypeProfile -.-> ModuleType DeviceRole & Platform & DeviceType --> Device Device & ModuleType ---> Module Device & Module --> Interface & ConsolePort & PowerPort & ... + Interface --> MACAddress click Device "../../models/dcim/device/" click DeviceRole "../../models/dcim/devicerole/" click DeviceType "../../models/dcim/devicetype/" +click Interface "../../models/dcim/interface/" +click MACAddress "../../models/dcim/macaddress/" click Manufacturer "../../models/dcim/manufacturer/" click Module "../../models/dcim/module/" click ModuleType "../../models/dcim/moduletype/" +click ModuleTypeProfile "../../models/dcim/moduletypeprofile/" click Platform "../../models/dcim/platform/" ``` @@ -69,15 +74,23 @@ Sometimes it is necessary to model a set of physical devices as sharing a single A virtual device context (VDC) is a logical partition within a device. Each VDC operates autonomously but shares a common pool of resources. Each interface can be assigned to one or more VDCs on its device. -## Module Types & Modules +## Module Types, Profiles & Modules Much like device types and devices, module types can instantiate discrete modules, which are hardware components installed within devices. Modules often have their own child components, which become available to the parent device. For example, when modeling a chassis-based switch with multiple line cards in NetBox, the chassis would be created (from a device type) as a device, and each of its line cards would be instantiated from a module type as a module installed in one of the device's module bays. +### Module Type Profiles + +A [module type profile](../models/dcim/moduletypeprofile.md) classifies module types (e.g. `Power Supply`, `Disk`) and may optionally define a [JSON schema](https://json-schema.org/) describing custom attributes that module types of that profile may carry. This is useful for tracking domain-specific specifications such as a power supply's input voltage, a CPU's clock speed, or a disk's capacity, without needing to add a custom field to every module type in NetBox. + !!! tip "Device Bays vs. Module Bays" What's the difference between device bays and module bays? Device bays are appropriate when the installed hardware has its own management plane, isolated from the parent device. A common example is a blade server chassis in which the blades share power but operate independently. In contrast, a module bay holds a module which does _not_ operate independently of its parent device, as with the chassis switch line card example mentioned above. One especially nice feature of modules is that templated components can be automatically renamed according to the module bay into which the parent module is installed. For example, if we create a module type with interfaces named `Gi{module}/0/1-48` and install a module of this type into module bay 7 of a device, NetBox will create interfaces named `Gi7/0/1-48`. +## MAC Addresses + +[MAC addresses](../models/dcim/macaddress.md) are modeled as first-class objects in NetBox so that an interface may have multiple MAC addresses assigned to it, with one optionally designated as the interface's primary MAC. This accommodates virtual interfaces and modular hardware where the link-layer address is not necessarily fixed at the factory. MAC addresses can be assigned to both [device interfaces](../models/dcim/interface.md) and [virtual machine interfaces](../models/virtualization/vminterface.md). + ## Cables NetBox models cables as connections among certain types of device components and other objects. Each cable can be assigned a type, color, length, and label. NetBox will enforce basic sanity checks to prevent invalid connections. (For example, a network interface cannot be connected to a power outlet.) @@ -89,3 +102,7 @@ flowchart LR Interface --> Cable Cable --> fp1[Front Port] & fp2[Front Port] ``` + +### Cable Bundles + +Related cables can optionally be grouped into a [cable bundle](../models/dcim/cablebundle.md), representing a logical collection such as a conduit, trunk, or wiring harness. Bundle membership is purely organizational: it does not affect cable tracing or connectivity. Deleting a cable removes it from its bundle but does not delete the bundle itself, allowing bundles to outlive any specific member cable. diff --git a/docs/features/facilities.md b/docs/features/facilities.md index 1421281eb..32f8402b5 100644 --- a/docs/features/facilities.md +++ b/docs/features/facilities.md @@ -13,10 +13,12 @@ flowchart TD Rack --> Device Site --> Rack RackRole --> Rack + RackGroup --> Rack click Device "../../models/dcim/device/" click Location "../../models/dcim/location/" click Rack "../../models/dcim/rack/" +click RackGroup "../../models/dcim/rackgroup/" click RackRole "../../models/dcim/rackrole/" click Region "../../models/dcim/region/" click Site "../../models/dcim/site/" @@ -60,11 +62,15 @@ A location can be any logical subdivision within a building, such as a floor or A rack type represents a unique specification of a rack which exists in the real world. Each rack type can be setup with weight, height, and unit ordering. New racks of this type can then be created in NetBox, and any associated specifications will be automatically replicated from the device type. +## Rack Groups + +In addition to being assigned to a [location](#locations), racks may optionally be assigned to a [rack group](../models/dcim/rackgroup.md). Rack groups are flat (non-hierarchical) and exist alongside locations as a secondary axis of grouping — particularly handy for organizing racks by row, aisle, or pod within a single location, or for scoping [VLAN groups](../models/ipam/vlangroup.md) to a subset of racks. + ## Racks Finally, NetBox models each equipment rack as a discrete object within a site and location. These are physical objects into which devices are installed. Each rack can be assigned an operational status, type, facility ID, and other attributes related to inventory tracking. Each rack also must define a height (in rack units) and width, and may optionally specify its physical dimensions. -Each rack must be associated to a site, but the assignment to a location within that site is optional. Users can also create custom roles to which racks can be assigned. NetBox supports tracking rack space in half-unit increments, so it's possible to mount devices at e.g. position 2.5 within a rack. +Each rack must be associated to a site, but the assignment to a location or rack group within that site is optional. Users can also create custom roles to which racks can be assigned. NetBox supports tracking rack space in half-unit increments, so it's possible to mount devices at e.g. position 2.5 within a rack. !!! tip "Devices" You'll notice in the diagram above that a device can be installed within a site, location, or rack. This approach affords plenty of flexibility as not all sites need to define child locations, and not all devices reside in racks. diff --git a/docs/installation/1-postgresql.md b/docs/installation/1-postgresql.md index 7dac7cf46..8c5a538ad 100644 --- a/docs/installation/1-postgresql.md +++ b/docs/installation/1-postgresql.md @@ -5,6 +5,9 @@ This section entails the installation and configuration of a local PostgreSQL da !!! warning "PostgreSQL 15 or later required" NetBox requires PostgreSQL 15 or later. Please note that MySQL and other relational databases are **not** supported. +!!! warning "PostgreSQL 14 deprecation notice" + Support for PostgreSQL 14 is deprecated as of NetBox v4.6 and will be removed in NetBox v4.7. Please plan to upgrade to PostgreSQL 15 or later. + ## Installation ```no-highlight diff --git a/docs/installation/index.md b/docs/installation/index.md index b514df448..154b88aa3 100644 --- a/docs/installation/index.md +++ b/docs/installation/index.md @@ -33,7 +33,34 @@ The following sections detail how to set up a new instance of NetBox: Below is a simplified overview of the NetBox application stack for reference: -![NetBox UI as seen by a non-authenticated user](../media/installation/netbox_application_stack.png) +```mermaid +flowchart TB + nginx["nginx / Apache
HTTP reverse proxy
"]:::red + gunicorn["gunicorn
WSGI HTTP server
"]:::orange + rqworker["rqworker
Background worker
"]:::pink + netbox["NetBox
Django application
"]:::blue + django["Django
Python application framework
"]:::green + storage["Storage Driver
Static asset storage
"]:::gray + postgres["PostgreSQL
Relational database
"]:::teal + redis["Redis
In-memory store
"]:::purple + + nginx --> gunicorn + nginx --> storage + gunicorn --> netbox + rqworker --> netbox + netbox --> django + django --> postgres + django --> redis + + classDef red fill:#b91c1c,stroke:#7f1d1d,color:#fff + classDef orange fill:#c2410c,stroke:#7c2d12,color:#fff + classDef pink fill:#a21caf,stroke:#701a75,color:#fff + classDef blue fill:#1d4ed8,stroke:#1e3a8a,color:#fff + classDef green fill:#15803d,stroke:#14532d,color:#fff + classDef gray fill:#4b5563,stroke:#1f2937,color:#fff + classDef teal fill:#0f766e,stroke:#134e4a,color:#fff + classDef purple fill:#6d28d9,stroke:#4c1d95,color:#fff +``` ## Upgrading diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index e6c60c80c..9aefd6c55 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -4,7 +4,20 @@ Upgrading NetBox to a new version is pretty simple, however users are cautioned NetBox can generally be upgraded directly to any newer release with no interim steps, with the one exception being incrementing major versions. This can be done only from the most recent _minor_ release of the major version. For example, NetBox v2.11.8 can be upgraded to version 3.3.2 following the steps below. However, a deployment of NetBox v2.10.10 or earlier must first be upgraded to any v2.11 release, and then to any v3.x release. (This is to accommodate the consolidation of database schema migrations effected by a major version change). -[![Upgrade paths](../media/installation/upgrade_paths.png)](../media/installation/upgrade_paths.png) +```mermaid +block-beta + columns 10 + v29["v2.9"] v210["v2.10"] v211["v2.11"] v30["v3.0"] v31["v3.1"] dots["..."] v36["v3.6"] v37["v3.7"] v40["v4.0"] v41["v4.1"] + v2arrow["To any v2.x release ➜"]:3 space:7 + space:2 v3arrow["To any v3.x release ➜"]:6 space:2 + space:7 v4arrow["To any v4.x release ➜"]:3 + classDef orange fill:#b45309,stroke:#78350f,color:#fff + classDef green fill:#0f766e,stroke:#134e4a,color:#fff + classDef blue fill:#1d4ed8,stroke:#1e3a8a,color:#fff + class v2arrow orange + class v3arrow green + class v4arrow blue +``` !!! warning "Perform a Backup" Always be sure to save a backup of your current NetBox deployment prior to starting the upgrade process. @@ -27,6 +40,7 @@ NetBox requires the following dependencies: | NetBox Version | Python min | Python max | PostgreSQL min | Redis min | Documentation | |:--------------:|:----------:|:----------:|:--------------:|:---------:|:-----------------------------------------------------------------------------------------:| +| 4.6 | 3.12 | 3.14 | 14 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.6.0/docs/installation/index.md) | | 4.5 | 3.12 | 3.14 | 14 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.5.0/docs/installation/index.md) | | 4.4 | 3.10 | 3.12 | 14 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.4.0/docs/installation/index.md) | | 4.3 | 3.10 | 3.12 | 14 | 4.0 | [Link](https://github.com/netbox-community/netbox/blob/v4.3.0/docs/installation/index.md) | diff --git a/docs/integrations/rest-api.md b/docs/integrations/rest-api.md index 74b421456..d55471168 100644 --- a/docs/integrations/rest-api.md +++ b/docs/integrations/rest-api.md @@ -663,6 +663,51 @@ Note that there is no requirement for the attributes to be identical among objec !!! note The bulk update of objects is an all-or-none operation, meaning that if NetBox fails to successfully update any of the specified objects (e.g. due a validation error), the entire operation will be aborted and none of the objects will be updated. +### Concurrent Update Protection + +!!! info "This feature was introduced in NetBox v4.6." + +To guard against the lost-update problem when multiple clients modify the same object, NetBox returns a weak `ETag` response header on detail-view responses (`GET`, `POST`, `PATCH`, `PUT`) for individual objects. Clients may supply this value back on a subsequent `PATCH` or `PUT` request via the `If-Match` request header. If the object's current ETag does not match any of the values supplied, the server rejects the request with a `412 Precondition Failed` response and includes the current ETag in the response so the client can retry. + +```no-highlight +# Capture the ETag returned with the object +$ curl -s -i -H "Authorization: Bearer $TOKEN" http://netbox/api/dcim/sites/1/ | grep -i ^etag +ETag: W/"2026-05-01T17:42:11.123456+00:00" + +# Submit an update with If-Match referencing that ETag +$ curl -s -X PATCH \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -H 'If-Match: W/"2026-05-01T17:42:11.123456+00:00"' \ + http://netbox/api/dcim/sites/1/ \ + --data '{"status": "decommissioning"}' +``` + +A literal `If-Match: *` value matches any current ETag and may be used to assert simply that the object exists. Submitting `If-Match` is optional; requests without the header retain prior (last-write-wins) behavior. + +### Adding and Removing Tags + +!!! info "This feature was introduced in NetBox v4.6." + +In addition to replacing an object's tag set wholesale via the `tags` field, taggable models accept two write-only fields, `add_tags` and `remove_tags`, which apply only the specified additions or removals without disturbing existing tags. This is convenient when concurrent clients each manage a distinct subset of an object's tags. + +```no-highlight +curl -s -X PATCH \ +-H "Authorization: Bearer $TOKEN" \ +-H "Content-Type: application/json" \ +http://netbox/api/dcim/sites/1/ \ +--data '{ + "add_tags": [{"name": "production"}], + "remove_tags": [{"name": "staging"}] +}' +``` + +Constraints: + +* `tags` may not be combined with `add_tags` or `remove_tags` in the same request. +* `remove_tags` is only valid on updates; it cannot be used when creating a new object. +* The same tag may not appear in both `add_tags` and `remove_tags`. + ### Deleting an Object To delete an object from NetBox, make a `DELETE` request to the model's _detail_ endpoint specifying its unique numeric ID. The `Authorization` header must be included to specify an authorization token, however this type of request does not support passing any data in the body. @@ -747,7 +792,10 @@ Additionally, a token can be set to expire at a specific time. This can be usefu #### v1 and v2 Tokens -Beginning with NetBox v4.5, two versions of API token are supported, denoted as v1 and v2. Users are strongly encouraged to create only v2 tokens and to discontinue the use of v1 tokens. Support for v1 tokens will be removed in a future NetBox release. +!!! warning "v1 Tokens Are Deprecated" + v1 API tokens are deprecated as of NetBox v4.6 and will be removed in NetBox v5.0. All users should migrate to v2 tokens. + +Beginning with NetBox v4.5, two versions of API token are supported, denoted as v1 and v2. Users are strongly encouraged to create only v2 tokens and to discontinue the use of v1 tokens. v2 API tokens offer much stronger security. The token plaintext given at creation time is hashed together with a configured [cryptographic pepper](../configuration/required-parameters.md#api_token_peppers) to generate a unique checksum. This checksum is irreversible; the token plaintext is never stored on the server and thus cannot be retrieved even with database-level access. @@ -871,3 +919,11 @@ GET /api/dcim/sites/?created_by_request=e39c84bc-f169-4d5f-bc1c-94487a1b18b5 !!! note This header is included with _all_ NetBox responses, although it is most practical when working with an API. + +### `ETag` + +A weak entity tag (e.g. `W/"2026-05-01T17:42:11.123456+00:00"`) returned on detail-view responses for individual objects. The value is derived from the object's `last_updated` timestamp (or `created`, if the object has no `last_updated`). Clients may supply this value on a subsequent write request via the `If-Match` header to perform a conditional update. See [Concurrent Update Protection](#concurrent-update-protection) for details. + +### `If-Match` + +A request header which may be supplied on `PATCH` or `PUT` requests targeting a single object. If the object's current ETag does not match any value supplied, the request is rejected with a `412 Precondition Failed` response. A literal value of `*` matches any existing object. See [Concurrent Update Protection](#concurrent-update-protection) for details. diff --git a/docs/media/installation/netbox_application_stack.png b/docs/media/installation/netbox_application_stack.png deleted file mode 100644 index c860b964b..000000000 Binary files a/docs/media/installation/netbox_application_stack.png and /dev/null differ diff --git a/docs/media/installation/upgrade_paths.png b/docs/media/installation/upgrade_paths.png deleted file mode 100644 index 576c6d190..000000000 Binary files a/docs/media/installation/upgrade_paths.png and /dev/null differ diff --git a/docs/models/core/objectchange.md b/docs/models/core/objectchange.md new file mode 100644 index 000000000..57c9e1e88 --- /dev/null +++ b/docs/models/core/objectchange.md @@ -0,0 +1,43 @@ +# Object Changes + +An object change is a record of a single create, update, or delete operation against an object whose model supports [change logging](../../features/change-logging.md). Object changes form a complete audit trail: each one captures the user that initiated the change, the request that caused it, the action performed, and a JSON snapshot of the object before and after. + +For component objects (e.g. an interface on a device), an object change can also reference a related parent object so that the change appears in the parent's changelog as well as the component's own. + +## Fields + +### Time + +The date and time at which the change was recorded. + +### User & User Name + +The [user](../users/user.md) who initiated the change. The user's username is also stored as a static string (`user_name`) so that change records remain readable even after the user account is deleted. + +### Request ID + +A UUID identifying the request that produced the change. All changes resulting from a single request share the same request ID, which makes it easy to correlate related modifications. The same value is returned on REST API responses via the `X-Request-ID` header. + +### Action + +The type of operation performed: `create`, `update`, or `delete`. + +### Changed Object + +A generic foreign key (`changed_object_type` + `changed_object_id`) identifying the object that was modified. + +### Related Object + +An optional generic foreign key referencing a related object (e.g. the parent device for a changed interface). When set, the change is also surfaced in the related object's changelog. + +### Object Representation + +A static text representation of the changed object, captured at the time of the change. Preserved so that the change record is meaningful even after the underlying object is deleted. + +### Message + +An optional free-form message attached to the change. May be supplied via the UI (in eligible forms) or via the [REST API](../../integrations/rest-api.md#changelog-messages) using the `changelog_message` field. + +### Pre-Change Data & Post-Change Data + +JSON snapshots of the object's serialized state immediately before and immediately after the change. For `create` actions, only post-change data is recorded; for `delete` actions, only pre-change data. The diff displayed in the UI is computed from these snapshots. diff --git a/docs/models/core/objecttype.md b/docs/models/core/objecttype.md new file mode 100644 index 000000000..73b2f129e --- /dev/null +++ b/docs/models/core/objecttype.md @@ -0,0 +1,26 @@ +# Object Types + +An object type identifies a NetBox model by its app label and model name (e.g. `dcim.device`). Object types are used wherever NetBox needs to refer to a model dynamically — most commonly in [custom fields](../extras/customfield.md), [object permissions](../users/objectpermission.md), [export templates](../extras/exporttemplate.md), [event rules](../extras/eventrule.md), and generic relations such as the assignment of an [IP address](../ipam/ipaddress.md) to either a device or VM interface. + +Object types extend Django's stock `ContentType` model with two additional attributes that NetBox uses to reason about model capabilities: `public` (whether the model is intended for direct reference) and `features` (the set of NetBox model features the model supports, such as change logging or custom fields). + +!!! note "For plugin authors" + NetBox code should generally use `ObjectType.objects.get_for_model()` rather than Django's `ContentType.objects.get_for_model()`, so that the resulting object exposes NetBox's `public` and `features` attributes. The two managers are otherwise interchangeable. + +## Fields + +### App Label + +The Django application label to which the model belongs (e.g. `dcim`, `ipam`, or a plugin's app label). + +### Model + +The lowercase model name (e.g. `device`, `prefix`). + +### Public + +Indicates whether the model is part of NetBox's public data model. Public models are those intended to be referenced from other objects (e.g. via custom fields or generic relations). Internal models — those backing implementation details — are non-public and are excluded from interfaces that expose model selection to end users. + +### Features + +The list of NetBox model features the underlying model supports (for example: `change_logging`, `custom_fields`, `tags`, `webhooks`). This list is consulted when filtering object types for a particular feature, e.g. when populating the model selector for an event rule. diff --git a/docs/models/dcim/cablebundle.md b/docs/models/dcim/cablebundle.md index cf585b378..15d6ba4da 100644 --- a/docs/models/dcim/cablebundle.md +++ b/docs/models/dcim/cablebundle.md @@ -1,8 +1,8 @@ # Cable Bundles -A cable bundle is a logical grouping of individual [cables](./cable.md). Bundles can be used to organize cables that share a common purpose, route, or physical grouping (such as a conduit or harness). +A cable bundle is a logical grouping of individual [cables](./cable.md). Bundles are useful for organizing cables that share a common purpose, route, or physical grouping such as a conduit, trunk, or wiring harness. -Assigning cables to a bundle is optional and does not affect cable tracing or connectivity. Bundles persist independently of their member cables: deleting a cable clears its bundle assignment but does not delete the bundle itself. +Assigning cables to a bundle is optional and does not affect cable tracing or connectivity. Bundles persist independently of their member cables: deleting a cable clears its bundle assignment but does not delete the bundle itself, allowing the bundle to be reused for replacement cables. ## Fields @@ -12,4 +12,4 @@ A unique name for the cable bundle. ### Description -A brief description of the bundle's purpose or contents. +An optional short description of the bundle's purpose or contents. diff --git a/docs/models/dcim/macaddress.md b/docs/models/dcim/macaddress.md index fe3d1f0e3..4931f5eb7 100644 --- a/docs/models/dcim/macaddress.md +++ b/docs/models/dcim/macaddress.md @@ -1,11 +1,25 @@ # MAC Addresses -A MAC address object in NetBox comprises a single Ethernet link layer address, and represents a MAC address as reported by or assigned to a network interface. MAC addresses can be assigned to [device](../dcim/device.md) and [virtual machine](../virtualization/virtualmachine.md) interfaces. A MAC address can be specified as the primary MAC address for a given device or VM interface. +A MAC address object in NetBox represents a single Ethernet link-layer address as reported by or assigned to a network interface. MAC addresses can be assigned to [device interfaces](./interface.md) and [virtual machine interfaces](../virtualization/vminterface.md), and any one of an interface's assigned MAC addresses may be designated as its **primary** MAC address. -Most interfaces have only a single MAC address, hard-coded at the factory. However, on some devices (particularly virtual interfaces) it is possible to assign additional MAC addresses or change existing ones. For this reason NetBox allows multiple MACAddress objects to be assigned to a single interface. +Most physical interfaces have only a single MAC address, hard-coded at the factory. However, some interfaces (particularly virtual interfaces and modular hardware) support multiple or reassignable MAC addresses. To accommodate this, NetBox models MAC addresses as first-class objects which may be created, modified, and reassigned independently of any specific interface. ## Fields ### MAC Address -The 48-bit MAC address, in colon-hexadecimal notation (e.g. `aa:bb:cc:11:22:33`). +The 48-bit MAC address, expressed in colon-hexadecimal notation (for example, `aa:bb:cc:11:22:33`). + +### Assigned Object + +A generic reference to the [device interface](./interface.md) or [virtual machine interface](../virtualization/vminterface.md) to which this MAC address is assigned. A MAC address may exist without being assigned to any interface. + +A MAC address that is currently designated as the primary MAC of its parent interface cannot be reassigned to (or unassigned from) another interface without first clearing the primary designation. + +### Description + +An optional human-readable description of the MAC address. + +### Comments + +Free-form Markdown-supported notes regarding the MAC address. diff --git a/docs/models/dcim/moduletype.md b/docs/models/dcim/moduletype.md index dca2b4af4..e2e42620d 100644 --- a/docs/models/dcim/moduletype.md +++ b/docs/models/dcim/moduletype.md @@ -11,8 +11,9 @@ Similar to [device types](./devicetype.md), each module type can have any of the * Power Outlets * Front pass-through ports * Rear pass-through ports +* Module bays -Note that device bays and module bays may _not_ be added to modules. +Note that device bays may _not_ be added to modules. ## Automatic Component Renaming diff --git a/docs/models/dcim/moduletypeprofile.md b/docs/models/dcim/moduletypeprofile.md index 8e9879398..3ab8a51cc 100644 --- a/docs/models/dcim/moduletypeprofile.md +++ b/docs/models/dcim/moduletypeprofile.md @@ -1,8 +1,8 @@ # Module Type Profiles -Each [module type](./moduletype.md) may optionally be assigned a profile according to its classification. A profile can extend module types with user-configured attributes. For example, you might want to specify the input current and voltage of a power supply, or the clock speed and number of cores for a processor. +Each [module type](./moduletype.md) may optionally be assigned a profile according to its classification. A profile can extend module types with user-configured attributes — for example, the input current and voltage of a power supply, or the clock speed and number of cores for a processor. -Module type attributes are managed via the configuration of a [JSON schema](https://json-schema.org/) on the profile. For example, the following schema introduces three module type attributes, two of which are designated as required attributes. +Module type attributes are managed by configuring a [JSON schema](https://json-schema.org/) on the profile. The schema below introduces three module type attributes, two of which are designated as required: ```json { @@ -29,10 +29,22 @@ Module type attributes are managed via the configuration of a [JSON schema](http } ``` -The assignment of module types to a profile is optional. The designation of a schema for a profile is also optional: A profile can be used simply as a mechanism for classifying module types if the addition of custom attributes is not needed. +Both the assignment of module types to a profile and the designation of a schema for a profile are optional: a profile can be used purely as a classification mechanism if the addition of custom attributes is not needed. ## Fields +### Name + +A unique name for the profile (for example, `Power Supply` or `Disk`). + +### Description + +An optional description of the profile. + ### Schema -This field holds the [JSON schema](https://json-schema.org/) for the profile. The configured JSON schema must be valid (or the field must be null). +An optional [JSON schema](https://json-schema.org/) defining the attributes that may (or must) be set on each assigned module type. The schema must be valid JSON Schema, or else null. + +### Comments + +Free-form Markdown-supported notes about the profile. diff --git a/docs/models/extras/configcontextprofile.md b/docs/models/extras/configcontextprofile.md index 289cce20a..118329330 100644 --- a/docs/models/extras/configcontextprofile.md +++ b/docs/models/extras/configcontextprofile.md @@ -1,6 +1,6 @@ # Config Context Profiles -Profiles can be used to organize [configuration contexts](./configcontext.md) and to enforce a desired structure for their data. The later is achieved by defining a [JSON schema](https://json-schema.org/) to which all config context with this profile assigned must comply. +Profiles can be used to organize [configuration contexts](./configcontext.md) and to enforce a desired structure for their data. The latter is achieved by defining a [JSON schema](https://json-schema.org/) to which all config contexts assigned to the profile must comply. Any context whose data fails validation against the profile's schema cannot be saved. For example, the following schema defines two keys, `size` and `priority`, of which the former is required: @@ -22,12 +22,26 @@ For example, the following schema defines two keys, `size` and `priority`, of wh } ``` +A profile's schema may also be populated from a [data source](../core/datasource.md), enabling the schema to be maintained externally (for example, in a git repository) and synchronized into NetBox. + ## Fields ### Name -A unique human-friendly name. +A unique, human-friendly name for the profile. + +### Description + +An optional description of the profile's purpose. ### Schema -The JSON schema to be enforced for all assigned config contexts (optional). +An optional [JSON schema](https://json-schema.org/) document. When set, the schema is enforced for every config context assigned to this profile. Leaving the schema blank allows the profile to be used purely as an organizational grouping. + +### Data Source / Data File / Data Path + +Optional pointers to a remote [data source](../core/datasource.md) and file from which the schema is populated. When configured, the schema field is overwritten on synchronization. + +### Auto Sync Enabled + +When set, the profile's schema is automatically refreshed whenever the upstream data file is updated. diff --git a/docs/models/ipam/iprange.md b/docs/models/ipam/iprange.md index fd439998a..760dde699 100644 --- a/docs/models/ipam/iprange.md +++ b/docs/models/ipam/iprange.md @@ -21,6 +21,13 @@ The [Virtual Routing and Forwarding](./vrf.md) instance in which this IP range e The beginning and ending IP addresses (inclusive) which define the boundaries of the range. Both IP addresses must specify the correct mask. +A range may contain a single IP address by setting the start and end address to the same value, including the same mask length. This is useful when a pool-like construct, such as a DHCP or NAT pool, contains only one usable address. + +!!! note + A single-address IP range is not a replacement for an [IP address](./ipaddress.md). Use an IP address object when modeling an address configured on an interface, assigned as a primary IP, or otherwise participating in IP-address-specific relationships. Use an IP range only when the address is being modeled as a pool, reservation range, or similar range-oriented construct. + + The end address is still required. To create a single-address range, enter the same address and mask for both the start and end address. + !!! note The maximum supported size of an IP range is 2^32 - 1. diff --git a/docs/models/ipam/vlangroup.md b/docs/models/ipam/vlangroup.md index 6a4e7bf15..303dda83e 100644 --- a/docs/models/ipam/vlangroup.md +++ b/docs/models/ipam/vlangroup.md @@ -18,6 +18,16 @@ A unique URL-friendly identifier. (This value can be used for filtering.) The set of VLAN IDs which are encompassed by the group. By default, this will be the entire range of valid IEEE 802.1Q VLAN IDs (1 to 4094, inclusive). VLANs created within a group must have a VID that falls within one of these ranges. Ranges may not overlap. +Internally, each range is stored in PostgreSQL as a canonical half-open `[start, end)` interval. The `start` value is the first VID included in the range; the `end` value sits one above the last VID included. + +The REST API and UI both present ranges using inclusive bounds, so most users never see the half-open form. Users working with the stored value directly, such as through psql, raw SQL, Django ORM access in Custom Scripts or plugins, or third-party tools, should expect this canonical representation. + +For the range covering VLANs 100 through 200: + +* UI input: `100-200` +* REST API range item: `[100, 200]` +* Database row: `[100, 201)` + ### Total VLAN IDs A read-only integer indicating the total count of VLAN IDs available within the group, calculated from the configured VLAN ID Ranges. For example, a group with ranges `100-199` and `300-399` would have a total of 200 VLAN IDs. This value is automatically computed and updated whenever the VLAN ID ranges are modified. diff --git a/docs/models/users/group.md b/docs/models/users/group.md new file mode 100644 index 000000000..a7d0df697 --- /dev/null +++ b/docs/models/users/group.md @@ -0,0 +1,19 @@ +# Groups + +A group is a collection of [users](./user.md) which share a common set of permissions. Assigning [object permissions](./objectpermission.md) to a group, rather than to individual users, simplifies the administration of permissions for related users (e.g. members of a particular team). + +A user inherits the union of all permissions assigned to each group of which they are a member, in addition to any permissions assigned directly to the user. + +## Fields + +### Name + +A unique name for the group. + +### Description + +A short description of the group's role or membership. + +### Object Permissions + +The set of [object permissions](./objectpermission.md) granted to all members of this group. diff --git a/docs/models/users/objectpermission.md b/docs/models/users/objectpermission.md new file mode 100644 index 000000000..67d7a20e0 --- /dev/null +++ b/docs/models/users/objectpermission.md @@ -0,0 +1,50 @@ +# Object Permissions + +An object permission grants the ability to perform one or more actions (e.g. view, add, change, delete) against a defined set of object types, and may be restricted to a subset of objects matching a configured filter. Permissions are assigned to [users](./user.md) and/or [groups](./group.md); a user's effective permissions are the union of those assigned directly and those inherited via group membership. + +See the [permissions documentation](../../administration/permissions.md) for a detailed walkthrough of how permissions are evaluated. + +## Fields + +### Name + +A short, human-readable name for the permission. + +### Description + +An optional longer description of what the permission grants. + +### Enabled + +When unset, the permission is effectively disabled: it remains assigned to its users and groups, but is ignored during permission checks. This is useful for temporarily revoking access without altering assignments. + +### Object Types + +The list of NetBox model types to which this permission applies (e.g. `dcim.device`, `ipam.prefix`). + +### Actions + +The list of actions granted by the permission. The standard CRUD actions are `view`, `add`, `change`, and `delete`. Models may also register custom actions (e.g. `napalm` on `dcim.device`); custom actions appear here when supported by the selected object types. + +### Constraints + +An optional [Django ORM-style filter](https://docs.djangoproject.com/en/stable/topics/db/queries/#field-lookups) expressed as JSON. When set, the permission applies only to objects matching the filter. Multiple constraint sets may be supplied as a JSON list; an object matches if it satisfies any of the sets (logical OR). + +For example, to grant a permission only over devices in a specific site: + +```json +{"site__slug": "ny-dc1"} +``` + +Or, to apply the permission to devices in either of two sites: + +```json +[ + {"site__slug": "ny-dc1"}, + {"site__slug": "sj-dc2"} +] +``` + +### Users & Groups + +The [users](./user.md) and [groups](./group.md) to which this permission is assigned. diff --git a/docs/models/users/ownergroup.md b/docs/models/users/ownergroup.md index 61c279438..db6420325 100644 --- a/docs/models/users/ownergroup.md +++ b/docs/models/users/ownergroup.md @@ -1,9 +1,13 @@ # Owner Groups -Groups are used to correlate and organize [owners](./owner.md). The assignment of an owner to a group has no bearing on the relationship of owned objects to their owners. +Groups are used to correlate and organize [owners](./owner.md). The assignment of an owner to a group has no bearing on the relationship of owned objects to their owners; groups exist purely as an organizational convenience for administrators. ## Fields ### Name -The name of the group. +A unique name for the group. + +### Description + +An optional description of the group's role or membership. diff --git a/docs/models/users/token.md b/docs/models/users/token.md new file mode 100644 index 000000000..0d8481760 --- /dev/null +++ b/docs/models/users/token.md @@ -0,0 +1,51 @@ +# Tokens + +A token is a secret credential associated with a [user](./user.md) which authenticates requests to NetBox's REST and GraphQL APIs. A user may hold multiple tokens; each can be independently expired, restricted, or revoked. + +Beginning with NetBox v4.5, two token versions are supported. v2 tokens (the default for newly-created tokens) are stored only as a salted HMAC digest, and the plaintext is shown to the user only once at creation time. Legacy v1 tokens store the plaintext directly; **their use is deprecated and support will be removed in NetBox v5.0.** See the [REST API authentication](../../integrations/rest-api.md#authentication) documentation for the request header formats used by each version. + +## Fields + +### Version + +Indicates whether this is a v1 (legacy) or v2 token. v2 is the default and is strongly preferred. **v1 tokens are deprecated and will be removed in NetBox v5.0.** + +### User + +The [user](./user.md) which owns the token. All requests authenticated with the token are performed as this user. + +### Description + +A free-form description of the token (e.g. naming the application or automation that uses it). + +### Created + +The date and time at which the token was created. + +### Expires + +An optional date and time after which the token will no longer be valid. Tokens without an expiration never expire. + +### Last Used + +The date and time at which the token was most recently used to authenticate a request. This value is updated at most once per minute to limit database write overhead. + +### Enabled + +When unset, the token is temporarily revoked. Disabled tokens cannot be used to authenticate requests but are not deleted, allowing them to be re-enabled later. + +### Write Enabled + +When unset, the token may only be used for read operations (e.g. `GET`). All write operations (`POST`, `PATCH`, `PUT`, `DELETE`) made with the token will be rejected. + +### Allowed IPs + +An optional list of IPv4 and/or IPv6 prefixes from which the token may be used. If set, requests originating from any other source address will be rejected. + +### Key (v2 only) + +A short, randomly-generated identifier transmitted in plaintext alongside each request. The key allows the server to locate the matching token record before validating the secret portion. + +### Plaintext (v1 only) + +The full plaintext value of a v1 token. Stored as-is in the database, which is one of the reasons v2 tokens are preferred. diff --git a/docs/models/users/user.md b/docs/models/users/user.md new file mode 100644 index 000000000..0bbfeeed0 --- /dev/null +++ b/docs/models/users/user.md @@ -0,0 +1,47 @@ +# Users + +A user represents an individual account in NetBox. Users authenticate to access the application, and may be granted permissions either directly or through their assigned [groups](./group.md). Each user can hold one or more API [tokens](./token.md) for use with the REST and GraphQL APIs. + +NetBox extends Django's stock user model to support multiple API tokens per user, configurable [object permissions](./objectpermission.md), and integration with [remote authentication backends](../../administration/authentication/overview.md). + +## Fields + +### Username + +A unique identifier used to log in. May contain letters, digits, and the characters `@ . + - _`. Username comparison is case-insensitive: a new user cannot be created whose username differs from an existing one only in letter case. + +### First Name + +The user's given name. Optional. + +### Last Name + +The user's family name. Optional. + +### Email Address + +The user's email address. Used by NetBox to send notifications (e.g. error reports) when configured to do so. + +### Active + +When unset, the user is treated as inactive and may not log in. Disabling a user is generally preferable to deletion, as it preserves the user's history in change records and other related objects. + +### Staff Status + +Designates whether the user can log into the (legacy) Django admin site. Most NetBox functionality is exposed via the standard UI; staff status is rarely needed. + +### Superuser Status + +Designates that the user is granted all permissions implicitly, bypassing all permission checks. Use sparingly. + +### Date Joined + +The date and time at which the user account was created. + +### Groups + +The set of [groups](./group.md) to which the user belongs. A user inherits all permissions assigned to each of their groups. + +### Object Permissions + +The set of [object permissions](./objectpermission.md) assigned directly to the user, in addition to those granted via group membership. diff --git a/docs/plugins/development/index.md b/docs/plugins/development/index.md index 25f8b68df..b94639300 100644 --- a/docs/plugins/development/index.md +++ b/docs/plugins/development/index.md @@ -116,9 +116,10 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i | `middleware` | A list of middleware classes to append after NetBox's build-in middleware | | `queues` | A list of custom background task queues to create | | `events_pipeline` | A list of handlers to add to [`EVENTS_PIPELINE`](../../configuration/miscellaneous.md#events_pipeline), identified by dotted paths | -| `search_extensions` | The dotted path to the list of search index classes (default: `search.indexes`) | +| `search_indexes` | The dotted path to the list of search index classes (default: `search.indexes`) | | `data_backends` | The dotted path to the list of data source backend classes (default: `data_backends.backends`) | | `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) | +| `menu` | The dotted path to a top-level navigation menu provided by the plugin (default: `navigation.menu`) | | `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) | | `graphql_schema` | The dotted path to the plugin's GraphQL schema class, if any (default: `graphql.schema`) | | `user_preferences` | The dotted path to the dictionary mapping of user preferences defined by the plugin (default: `preferences.preferences`) | diff --git a/docs/plugins/development/permissions.md b/docs/plugins/development/permissions.md index 62a1ede1b..0fb3b0618 100644 --- a/docs/plugins/development/permissions.md +++ b/docs/plugins/development/permissions.md @@ -1,24 +1,71 @@ # Custom Model Actions -Plugins can register custom permission actions for their models. These actions appear as checkboxes in the ObjectPermission form, making it easy for administrators to grant or restrict access to plugin-specific functionality without manually entering action names. +Plugins can register custom permission actions for their models. These actions appear as checkboxes in the [ObjectPermission](../../models/users/objectpermission.md) form alongside the standard view/add/change/delete actions, making it easy for administrators to grant or restrict access to plugin-specific functionality without manually entering action names. -For example, a plugin might define a "sync" action for a model that syncs data from an external source, or a "bypass" action that allows users to bypass certain restrictions. +For example, a plugin might define a `sync` action for a model that fetches data from an external source, or a `bypass` action that allows users to skip certain restrictions. ## Registering Model Actions -The preferred way to register custom actions is via Django's `Meta.permissions` on the model class. NetBox will automatically register these as model actions when the app is loaded: +The preferred way to register custom actions is via Django's `Meta.permissions` on the model class. NetBox automatically registers these as model actions when the app is loaded: -```python +```python title="models.py" from netbox.models import NetBoxModel -class MyModel(NetBoxModel): - # ... +class WidgetSync(NetBoxModel): + # ... fields ... class Meta: permissions = [ - ('sync', 'Synchronize data from external source'), - ('export', 'Export data to external system'), + ('sync', 'Synchronize widgets from external source'), + ('export', 'Export widgets to external system'), ] ``` -Once registered, these actions appear as checkboxes in a flat list when creating or editing an ObjectPermission. +Once registered, these actions appear as checkboxes in a flat list when creating or editing an ObjectPermission. The first element of each tuple is the action's identifier (used in code) and the second is the help text shown to administrators in the UI. + +!!! note "Reserved action names" + Action names that conflict with NetBox's built-in CRUD verbs (`view`, `add`, `change`, `delete`) are reserved and cannot be reused as custom actions. + +## Granting an Action + +Custom actions are granted just like any standard permission: + +1. Open **Admin → Object Permissions** and create a new permission. +2. Select the relevant object type(s) (e.g. `my_plugin | widget sync`). +3. Tick the custom action's checkbox (e.g. `sync`). +4. Assign the permission to the desired users and/or groups. + +Optional [constraints](../../models/users/objectpermission.md#constraints) may be added to limit the permission to a subset of objects. + +## Checking an Action at Runtime + +Custom actions follow Django's standard permission naming convention `._`. To check whether the current user is authorized to perform a custom action against a model, call `user.has_perm()`: + +```python +if request.user.has_perm('my_plugin.sync_widgetsync'): + # User is permitted to invoke the sync action + ... +``` + +Per-object permission checks (which respect any [constraints](../../models/users/objectpermission.md#constraints) on the granting permission) work the same way: + +```python +if request.user.has_perm('my_plugin.sync_widgetsync', obj=widget): + ... +``` + +For class-based views, NetBox provides `ObjectPermissionRequiredMixin` from `utilities.views`, which integrates cleanly with these custom actions: + +```python title="views.py" +from utilities.views import ObjectPermissionRequiredMixin +from django.views.generic import View + +class SyncWidgetView(ObjectPermissionRequiredMixin, View): + queryset = WidgetSync.objects.all() + permission_required = 'my_plugin.sync_widgetsync' + + def post(self, request, pk): + widget = self.get_object() + widget.sync() + return redirect(widget.get_absolute_url()) +``` diff --git a/docs/plugins/installation.md b/docs/plugins/installation.md index ffea5d42d..5d48037b2 100644 --- a/docs/plugins/installation.md +++ b/docs/plugins/installation.md @@ -7,12 +7,32 @@ Download and install the plugin's Python package per its installation instructions. Plugins published via PyPI are typically installed using the [`pip`](https://packaging.python.org/en/latest/tutorials/installing-packages/) command line utility. Be sure to install the plugin within NetBox's virtual environment. +For production installations, the recommended method is to add the package to `/opt/netbox/local_requirements.txt` and then run NetBox's upgrade script. This installs the plugin as part of the standard NetBox installation and upgrade process and ensures that it will be reinstalled if the virtual environment is rebuilt. + ```no-highlight -$ source /opt/netbox/venv/bin/activate -(venv) $ pip install +$ sudo sh -c "echo '' >> /opt/netbox/local_requirements.txt" +$ sudo /opt/netbox/upgrade.sh ``` -Alternatively, you may wish to install the plugin manually by running `python setup.py install`. If you are developing a plugin and want to install it only temporarily, run `python setup.py develop` instead. +Installing packages into NetBox's virtual environment requires write permissions to that directory. For installations under `/opt/netbox`, a regular user typically does not have write permissions. Activating the virtual environment does not change file permissions, so a direct `pip install` command may result in a `Permission denied` error. + +If you must install a package manually, use one of the following methods. You can switch to a root shell before activating the virtual environment: + +```no-highlight +$ sudo -i +# source /opt/netbox/venv/bin/activate +(venv) # pip install +``` + +Or, run `pip` by invoking the Python executable within NetBox's virtual environment: + +```no-highlight +$ sudo /opt/netbox/venv/bin/python3 -m pip install +``` + +In the examples above, `$` indicates a regular user shell and `#` indicates a root shell. + +Packages that are not published to PyPI may need to be installed from a local source tree. From the package directory, use one of the methods above to run `pip install .`; for editable development installs, run `pip install --editable .` instead. ## Enable the Plugin @@ -38,13 +58,16 @@ PLUGINS_CONFIG = { } ``` +!!! note + If you ran `/opt/netbox/upgrade.sh` after enabling and configuring the plugin, the script has already applied database migrations and collected static files. If you ran it only to install the package before enabling the plugin, continue with the migration and static file steps below. + ## Run Database Migrations If the plugin introduces new database models, run the provided schema migrations: ```no-highlight -(venv) $ cd /opt/netbox/netbox/ -(venv) $ python3 manage.py migrate +(venv) $ cd /opt/netbox/ +(venv) $ python3 netbox/manage.py migrate ``` !!! tip @@ -55,14 +78,14 @@ If the plugin introduces new database models, run the provided schema migrations Plugins may package static resources like images or scripts to be served directly by the HTTP front end. Ensure that these are copied to the static root directory with the `collectstatic` management command: ```no-highlight -(venv) $ cd /opt/netbox/netbox/ -(venv) $ python3 manage.py collectstatic +(venv) $ cd /opt/netbox/ +(venv) $ python3 netbox/manage.py collectstatic ``` -### Restart WSGI Service +## Restart WSGI Service Finally, restart the WSGI service and RQ workers to load the new plugin: ```no-highlight -# sudo systemctl restart netbox netbox-rq +$ sudo systemctl restart netbox netbox-rq ``` diff --git a/docs/plugins/removal.md b/docs/plugins/removal.md index 039dcec83..b30892d6a 100644 --- a/docs/plugins/removal.md +++ b/docs/plugins/removal.md @@ -19,18 +19,19 @@ Delete the plugin's entry (if any) in the `PLUGINS_CONFIG` dictionary in `config Run the `reindex` management command to reindex the global search engine. This will remove any stale entries pertaining to objects provided by the plugin. ```no-highlight -$ cd /opt/netbox/netbox/ +$ cd /opt/netbox/ $ source /opt/netbox/venv/bin/activate -(venv) $ python3 manage.py reindex +(venv) $ python3 netbox/manage.py reindex ``` ## Uninstall its Python Package -Use `pip` to remove the installed plugin: +If the plugin was installed by adding it to `/opt/netbox/local_requirements.txt`, remove its package entry from that file first. Otherwise, the package will be reinstalled the next time NetBox's upgrade script is run. + +Use `pip` to remove the installed plugin from NetBox's virtual environment: ```no-highlight -$ source /opt/netbox/venv/bin/activate -(venv) $ pip uninstall +$ sudo /opt/netbox/venv/bin/python3 -m pip uninstall ``` ## Restart WSGI Service @@ -38,7 +39,7 @@ $ source /opt/netbox/venv/bin/activate Restart the WSGI service: ```no-highlight -# sudo systemctl restart netbox +$ sudo systemctl restart netbox ``` ## Drop Database Tables @@ -150,5 +151,5 @@ stale_types.delete() After making these changes, restart the NetBox service to ensure all changes are reflected. ```no-highlight -sudo systemctl restart netbox +$ sudo systemctl restart netbox ``` diff --git a/docs/release-notes/version-4.6.md b/docs/release-notes/version-4.6.md index ab1363f4f..f4ea93052 100644 --- a/docs/release-notes/version-4.6.md +++ b/docs/release-notes/version-4.6.md @@ -1,5 +1,88 @@ # NetBox v4.6 +## v4.6.2 (2026-06-02) + +### Enhancements + +* [#17127](https://github.com/netbox-community/netbox/issues/17127) - Add a user preference for selecting metric or imperial units of measurement +* [#19336](https://github.com/netbox-community/netbox/issues/19336) - Convert the filtering of tabbed list views from JavaScript to HTMX +* [#19460](https://github.com/netbox-community/netbox/issues/19460) - Support additional template variables for greater flexibility when constructing map URLs +* [#20804](https://github.com/netbox-community/netbox/issues/20804) - Support bulk renaming of the `label` field on device components +* [#21261](https://github.com/netbox-community/netbox/issues/21261) - Allow setting `quick_add` on an `ObjectVar` in custom scripts +* [#21952](https://github.com/netbox-community/netbox/issues/21952) - Improve robustness of the RQ worker liveness check +* [#22109](https://github.com/netbox-community/netbox/issues/22109) - Include child dependency counts in the module type REST API representation +* [#22212](https://github.com/netbox-community/netbox/issues/22212) - Make designated environment parameters available within Jinja2 templates via the new `env()` filter +* [#22239](https://github.com/netbox-community/netbox/issues/22239) - Rename the "Save" button on the table configuration form to "Apply" for clarity +* [#22255](https://github.com/netbox-community/netbox/issues/22255) - Allow plugins to register custom serializer resolvers for `get_serializer_for_model()` + +### Bug Fixes + +* [#21091](https://github.com/netbox-community/netbox/issues/21091) - Declare proper request & response schema types for the device/VM config rendering API endpoints +* [#22158](https://github.com/netbox-community/netbox/issues/22158) - Cache empty config revision state to avoid per-request queries polluting database connections +* [#22163](https://github.com/netbox-community/netbox/issues/22163) - Fix `ValueError` raised by CircuitTerminationForm when a termination type is set but the target object is blank +* [#22180](https://github.com/netbox-community/netbox/issues/22180) - Ensure custom scripts added via a remote data source are validated +* [#22187](https://github.com/netbox-community/netbox/issues/22187) - Fix erroneous cable path retracing when using a cable profile +* [#22219](https://github.com/netbox-community/netbox/issues/22219) - Add missing required form field indicator to InlineFields rows +* [#22228](https://github.com/netbox-community/netbox/issues/22228) - Validate `vid_ranges` bounds metadata in `VLANGroup.save()` to avoid miscounts and a crash on singleton ranges +* [#22232](https://github.com/netbox-community/netbox/issues/22232) - Prevent duplicate scheduled background jobs from being created +* [#22233](https://github.com/netbox-community/netbox/issues/22233) - Fix `site_id` filter on the cables REST API returning no results when both endpoints are circuit terminations +* [#22247](https://github.com/netbox-community/netbox/issues/22247) - Display the verbose name instead of the internal model name for the related object type on the custom field detail page +* [#22270](https://github.com/netbox-community/netbox/issues/22270) - Avoid recording a spurious UPDATE change record after DELETE for objects with reverse SET_NULL relations +* [#22282](https://github.com/netbox-community/netbox/issues/22282) - Fix `fetch()` on S3Backend to reliably resolve object keys +* [#22283](https://github.com/netbox-community/netbox/issues/22283) - Restrict the Job queryset in ScriptResultView to authorized objects +* [#22286](https://github.com/netbox-community/netbox/issues/22286) - Mark the `name` and `description` fields on the GraphQL ConfigContextProfileFilter as optional +* [#22287](https://github.com/netbox-community/netbox/issues/22287) - Fix GraphQL `EventRuleFilter.action_object_type` being typed as a string lookup against a ContentType foreign key +* [#22301](https://github.com/netbox-community/netbox/issues/22301) - Avoid name conflict when multiple plugins introduce taggable models of the same name +* [#22307](https://github.com/netbox-community/netbox/issues/22307) - Fix inconsistent enforcement of `grant_token` permissions between the UI and REST API +* [#22325](https://github.com/netbox-community/netbox/issues/22325) - Fix `AttributeError` when creating a custom field choice set with base choices +* [#22328](https://github.com/netbox-community/netbox/issues/22328) - Avoid out-of-memory crash in DynamicMultipleChoiceField with large choice sets + +--- + +## v4.6.1 (2026-05-19) + +### Enhancements + +* [#16851](https://github.com/netbox-community/netbox/issues/16851) - Correct errant and missing ARIA labels throughout the UI +* [#20776](https://github.com/netbox-community/netbox/issues/20776) - Add changelog message support for bulk rename operations +* [#20808](https://github.com/netbox-community/netbox/issues/20808) - Display the names of installed devices when selecting a rack position +* [#21938](https://github.com/netbox-community/netbox/issues/21938) - Display geographic hierarchy for circuit terminations assigned to sites, locations, or regions +* [#21993](https://github.com/netbox-community/netbox/issues/21993) - Allow IP ranges comprising a single IP address +* [#22057](https://github.com/netbox-community/netbox/issues/22057) - Add filter support for notifications and subscriptions to GraphQL API +* [#22192](https://github.com/netbox-community/netbox/issues/22192) - Introduce `HTTP_CLIENT_IP_HEADERS` configuration parameter to customize HTTP headers used to determine client IP address + +### Performance Improvements + +* [#22060](https://github.com/netbox-community/netbox/issues/22060) - Implement GraphQL query depth limiting (via `GRAPHQL_MAX_QUERY_DEPTH`) to guard against excessively complex queries +* [#22061](https://github.com/netbox-community/netbox/issues/22061) - Add prefetch hints to various GraphQL type mixins to improve query efficiency +* [#22102](https://github.com/netbox-community/netbox/issues/22102) - Add GIN index on CablePath to optimize filtering of cable paths by node +* [#22104](https://github.com/netbox-community/netbox/issues/22104) - Avoid retracing cable paths during cable deletion +* [#22146](https://github.com/netbox-community/netbox/issues/22146) - Avoid renumbering MPTT trees when creating module bays + +### Bug Fixes + +* [#21934](https://github.com/netbox-community/netbox/issues/21934) - Fix striped table rows overriding conditional row color highlighting for virtual/LAG interfaces +* [#22055](https://github.com/netbox-community/netbox/issues/22055) - Fix API exceptions being silently consumed by middleware without reporting to Sentry +* [#22079](https://github.com/netbox-community/netbox/issues/22079) - Fix security vulnerability allowing arbitrary code execution via ExportTemplate `environment_params` (CVE-2026-29514) +* [#22081](https://github.com/netbox-community/netbox/issues/22081) - REST API should return plaintext for new v2 tokens upon creation +* [#22183](https://github.com/netbox-community/netbox/issues/22183) - Fix spurious changelog entries for `interface_b` generated when saving an unchanged wireless link +* [#22190](https://github.com/netbox-community/netbox/issues/22190) - Restore tenant and tenant group column options for circuits group table configuration +* [#22198](https://github.com/netbox-community/netbox/issues/22198) - Restrict export template queryset to authorized objects in REST API and list views +* [#22202](https://github.com/netbox-community/netbox/issues/22202) - Fix crash in system housekeeping job when no stable releases are available +* [#22206](https://github.com/netbox-community/netbox/issues/22206) - Fix `TypeError` exception raised by table config validation when `ordering` attribute is null +* [#22207](https://github.com/netbox-community/netbox/issues/22207) - Fix missing explicit `object_type` field annotation on TableConfigType GraphQL type +* [#22208](https://github.com/netbox-community/netbox/issues/22208) - Add missing `user_id` FK filter on job filterset +* [#22209](https://github.com/netbox-community/netbox/issues/22209) - Add missing `cable_id` FK filter on cable termination filterset +* [#22227](https://github.com/netbox-community/netbox/issues/22227) - Fix display of IP address detail view when multiple NAT assignments exist +* [#22236](https://github.com/netbox-community/netbox/issues/22236) - Fix support for user changelog message when saving table configurations via the REST API + +### Deprecations + +* [#22128](https://github.com/netbox-community/netbox/issues/22128) - Deprecate support for v1 API tokens (to be removed in v5.0) +* [#22141](https://github.com/netbox-community/netbox/issues/22141) - Deprecate support for PostgreSQL 14 (to be removed in v4.7) + +--- + ## v4.6.0 (2026-05-05) ### New Features diff --git a/mkdocs.yml b/mkdocs.yml index 60bc2c643..36a1abcaf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -189,6 +189,8 @@ nav: - DataFile: 'models/core/datafile.md' - DataSource: 'models/core/datasource.md' - Job: 'models/core/job.md' + - ObjectChange: 'models/core/objectchange.md' + - ObjectType: 'models/core/objecttype.md' - DCIM: - Cable: 'models/dcim/cable.md' - CableBundle: 'models/dcim/cablebundle.md' @@ -280,8 +282,12 @@ nav: - Tenant: 'models/tenancy/tenant.md' - TenantGroup: 'models/tenancy/tenantgroup.md' - Users: + - Group: 'models/users/group.md' + - ObjectPermission: 'models/users/objectpermission.md' - Owner: 'models/users/owner.md' - OwnerGroup: 'models/users/ownergroup.md' + - Token: 'models/users/token.md' + - User: 'models/users/user.md' - Virtualization: - Cluster: 'models/virtualization/cluster.md' - ClusterGroup: 'models/virtualization/clustergroup.md' diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index 446ec0eed..94e43f0ce 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -1,6 +1,6 @@ from django import forms from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.utils.translation import gettext_lazy as _ from circuits.choices import ( @@ -24,7 +24,7 @@ from utilities.forms.fields import ( from utilities.forms.mixins import DistanceValidationMixin from utilities.forms.rendering import FieldSet, InlineFields, M2MAddRemoveFields from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions -from utilities.templatetags.builtins.filters import bettertitle +from utilities.string import title __all__ = ( 'CircuitForm', @@ -195,13 +195,11 @@ class CircuitTerminationForm(NetBoxModelForm): termination_type = ContentTypeChoiceField( queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES), widget=HTMXSelect(), - required=False, label=_('Termination type') ) termination = DynamicModelChoiceField( label=_('Termination'), queryset=Site.objects.none(), # Initial queryset - required=False, disabled=True, selector=True ) @@ -247,16 +245,28 @@ class CircuitTerminationForm(NetBoxModelForm): self.fields['termination'].queryset = model.objects.all() self.fields['termination'].widget.attrs['selector'] = model._meta.label_lower self.fields['termination'].disabled = False - self.fields['termination'].label = _(bettertitle(model._meta.verbose_name)) + self.fields['termination'].label = _(title(model._meta.verbose_name)) except ObjectDoesNotExist: pass if self.instance and termination_type_id != self.instance.termination_type_id: self.initial['termination'] = None + else: + # Clear the initial termination value if termination_type is not set + self.initial['termination'] = None def clean(self): super().clean() + termination = self.cleaned_data.get('termination') + termination_type = self.cleaned_data.get('termination_type') + if termination_type and not termination: + raise ValidationError({ + 'termination': _('Please select a {termination_type}.').format( + termination_type=_(title(termination_type.model_class()._meta.verbose_name)) + ) + }) + # Assign the selected termination (if any) self.instance.termination = self.cleaned_data.get('termination') @@ -319,7 +329,7 @@ class CircuitGroupAssignmentForm(NetBoxModelForm): self.fields['member'].queryset = model.objects.all() self.fields['member'].widget.attrs['selector'] = model._meta.label_lower self.fields['member'].disabled = False - self.fields['member'].label = _(bettertitle(model._meta.verbose_name)) + self.fields['member'].label = _(title(model._meta.verbose_name)) except ObjectDoesNotExist: pass diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index 29e7f8de3..d150d2b5c 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -74,7 +74,7 @@ class ProviderNetworkType(PrimaryObjectType): class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType): circuit: Annotated['CircuitType', strawberry.lazy('circuits.graphql.types')] - @strawberry_django.field + @strawberry_django.field(prefetch_related='termination') def termination(self) -> Annotated[ Annotated['LocationType', strawberry.lazy('dcim.graphql.types')] | Annotated['RegionType', strawberry.lazy('dcim.graphql.types')] @@ -133,7 +133,7 @@ class CircuitGroupType(OrganizationalObjectType): class CircuitGroupAssignmentType(TagsMixin, BaseObjectType): group: Annotated['CircuitGroupType', strawberry.lazy('circuits.graphql.types')] - @strawberry_django.field + @strawberry_django.field(prefetch_related='member') def member(self) -> Annotated[ Annotated['CircuitType', strawberry.lazy('circuits.graphql.types')] | Annotated['VirtualCircuitType', strawberry.lazy('circuits.graphql.types')], diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index 0e2d808c4..10d621092 100644 --- a/netbox/circuits/models/circuits.py +++ b/netbox/circuits/models/circuits.py @@ -17,6 +17,7 @@ from netbox.models.features import ( TagsMixin, ) from netbox.models.mixins import DistanceMixin +from utilities.string import title from .base import BaseCircuitType @@ -367,6 +368,13 @@ class CircuitTermination( return reverse('circuits:circuittermination', args=[self.pk]) def clean(self): + if self.termination_type and not (self.termination or self.termination_id): + termination_type = self.termination_type.model_class() + raise ValidationError( + _("Please select a {termination_type}.").format( + termination_type=_(title(termination_type._meta.verbose_name)) + ) + ) super().clean() if self.termination is None: diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py index 9ee9bd37b..b2d013086 100644 --- a/netbox/circuits/tables/circuits.py +++ b/netbox/circuits/tables/circuits.py @@ -159,7 +159,7 @@ class CircuitTerminationTable(NetBoxTable): ) -class CircuitGroupTable(OrganizationalModelTable): +class CircuitGroupTable(TenancyColumnsMixin, OrganizationalModelTable): name = tables.Column( verbose_name=_('Name'), linkify=True @@ -176,8 +176,8 @@ class CircuitGroupTable(OrganizationalModelTable): class Meta(OrganizationalModelTable.Meta): model = CircuitGroup fields = ( - 'pk', 'name', 'description', 'circuit_group_assignment_count', 'comments', 'tags', - 'created', 'last_updated', 'actions', + 'pk', 'name', 'description', 'circuit_group_assignment_count', 'tenant', 'tenant_group', 'comments', + 'tags', 'created', 'last_updated', 'actions', ) default_columns = ('pk', 'name', 'description', 'circuit_group_assignment_count') diff --git a/netbox/circuits/tests/query_counts.json b/netbox/circuits/tests/query_counts.json new file mode 100644 index 000000000..408dfdca0 --- /dev/null +++ b/netbox/circuits/tests/query_counts.json @@ -0,0 +1,24 @@ +{ + "circuit:api_list_objects": 16, + "circuit:list_objects_with_permission": 22, + "circuitgroup:api_list_objects": 13, + "circuitgroup:list_objects_with_permission": 20, + "circuitgroupassignment:api_list_objects": 17, + "circuitgroupassignment:list_objects_with_permission": 26, + "circuittermination:api_list_objects": 18, + "circuittermination:list_objects_with_permission": 24, + "circuittype:api_list_objects": 13, + "circuittype:list_objects_with_permission": 20, + "provider:api_list_objects": 15, + "provider:list_objects_with_permission": 20, + "provideraccount:api_list_objects": 14, + "provideraccount:list_objects_with_permission": 21, + "providernetwork:api_list_objects": 14, + "providernetwork:list_objects_with_permission": 21, + "virtualcircuit:api_list_objects": 16, + "virtualcircuit:list_objects_with_permission": 24, + "virtualcircuittermination:api_list_objects": 17, + "virtualcircuittermination:list_objects_with_permission": 23, + "virtualcircuittype:api_list_objects": 13, + "virtualcircuittype:list_objects_with_permission": 20 +} diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index 3b8280236..81d609847 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -8,7 +8,7 @@ from ipam.models import ASN, RIR from utilities.testing import APITestCase, APIViewTestCases -class AppTest(APITestCase): +class AppTestCase(APITestCase): def test_root(self): url = reverse('circuits-api:api-root') @@ -17,7 +17,7 @@ class AppTest(APITestCase): self.assertEqual(response.status_code, 200) -class ProviderTest(APIViewTestCases.APIViewTestCase): +class ProviderTestCase(APIViewTestCases.APIViewTestCase): model = Provider brief_fields = ['circuit_count', 'description', 'display', 'id', 'name', 'slug', 'url'] bulk_update_data = { @@ -59,7 +59,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase): ] -class CircuitTypeTest(APIViewTestCases.APIViewTestCase): +class CircuitTypeTestCase(APIViewTestCases.APIViewTestCase): model = CircuitType brief_fields = ['circuit_count', 'description', 'display', 'id', 'name', 'slug', 'url'] create_data = ( @@ -91,7 +91,7 @@ class CircuitTypeTest(APIViewTestCases.APIViewTestCase): CircuitType.objects.bulk_create(circuit_types) -class CircuitTest(APIViewTestCases.APIViewTestCase): +class CircuitTestCase(APIViewTestCases.APIViewTestCase): model = Circuit brief_fields = ['cid', 'description', 'display', 'id', 'provider', 'url'] bulk_update_data = { @@ -155,7 +155,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase): ] -class CircuitTerminationTest(APIViewTestCases.APIViewTestCase): +class CircuitTerminationTestCase(APIViewTestCases.APIViewTestCase): model = CircuitTermination brief_fields = ['_occupied', 'cable', 'circuit', 'description', 'display', 'id', 'term_side', 'url'] user_permissions = ('circuits.view_circuit', ) @@ -217,7 +217,7 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase): } -class CircuitGroupTest(APIViewTestCases.APIViewTestCase): +class CircuitGroupTestCase(APIViewTestCases.APIViewTestCase): model = CircuitGroup brief_fields = ['display', 'id', 'name', 'url'] bulk_update_data = { @@ -249,7 +249,7 @@ class CircuitGroupTest(APIViewTestCases.APIViewTestCase): ] -class ProviderAccountTest(APIViewTestCases.APIViewTestCase): +class ProviderAccountTestCase(APIViewTestCases.APIViewTestCase): model = ProviderAccount brief_fields = ['account', 'description', 'display', 'id', 'name', 'url'] user_permissions = ('circuits.view_provider',) @@ -293,7 +293,7 @@ class ProviderAccountTest(APIViewTestCases.APIViewTestCase): } -class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase): +class CircuitGroupAssignmentTestCase(APIViewTestCases.APIViewTestCase): model = CircuitGroupAssignment brief_fields = ['display', 'group', 'id', 'member', 'member_id', 'member_type', 'priority', 'url'] bulk_update_data = { @@ -368,7 +368,7 @@ class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase): ] -class ProviderNetworkTest(APIViewTestCases.APIViewTestCase): +class ProviderNetworkTestCase(APIViewTestCases.APIViewTestCase): model = ProviderNetwork brief_fields = ['description', 'display', 'id', 'name', 'url'] user_permissions = ('circuits.view_provider', ) @@ -409,7 +409,7 @@ class ProviderNetworkTest(APIViewTestCases.APIViewTestCase): } -class VirtualCircuitTypeTest(APIViewTestCases.APIViewTestCase): +class VirtualCircuitTypeTestCase(APIViewTestCases.APIViewTestCase): model = VirtualCircuitType brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url', 'virtual_circuit_count'] create_data = ( @@ -441,7 +441,7 @@ class VirtualCircuitTypeTest(APIViewTestCases.APIViewTestCase): VirtualCircuitType.objects.bulk_create(virtual_circuit_types) -class VirtualCircuitTest(APIViewTestCases.APIViewTestCase): +class VirtualCircuitTestCase(APIViewTestCases.APIViewTestCase): model = VirtualCircuit brief_fields = ['cid', 'description', 'display', 'id', 'provider_network', 'url'] bulk_update_data = { @@ -505,7 +505,7 @@ class VirtualCircuitTest(APIViewTestCases.APIViewTestCase): ] -class VirtualCircuitTerminationTest(APIViewTestCases.APIViewTestCase): +class VirtualCircuitTerminationTestCase(APIViewTestCases.APIViewTestCase): model = VirtualCircuitTermination brief_fields = ['description', 'display', 'id', 'interface', 'role', 'url', 'virtual_circuit'] bulk_update_data = { diff --git a/netbox/circuits/tests/test_forms.py b/netbox/circuits/tests/test_forms.py new file mode 100644 index 000000000..7745526fb --- /dev/null +++ b/netbox/circuits/tests/test_forms.py @@ -0,0 +1,43 @@ +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from circuits.forms import CircuitTerminationForm +from circuits.models import Circuit, CircuitType, Provider, ProviderNetwork + + +class CircuitTerminationFormTestCase(TestCase): + @classmethod + def setUpTestData(cls): + provider = Provider.objects.create(name='Provider 1', slug='provider-1') + circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') + + cls.circuit = Circuit.objects.create( + cid='Circuit 1', + provider=provider, + type=circuit_type, + ) + cls.provider_network = ProviderNetwork.objects.create( + name='Provider Network 1', + provider=provider, + ) + + def test_termination_required_when_termination_type_is_selected(self): + """ + Selecting a termination type without a target object should report a + validation error against the visible form field. + """ + provider_network_type = ContentType.objects.get_for_model(ProviderNetwork) + + form = CircuitTerminationForm( + data={ + 'circuit': self.circuit.pk, + 'term_side': 'A', + 'termination_type': provider_network_type.pk, + 'termination': '', + } + ) + + self.assertFalse(form.is_valid()) + self.assertIn('termination', form.errors) + self.assertIn('Please select a Provider Network.', form.errors['termination']) + self.assertNotIn('termination_id', form.errors) diff --git a/netbox/circuits/tests/test_models.py b/netbox/circuits/tests/test_models.py index f837cab65..a3c4a54ca 100644 --- a/netbox/circuits/tests/test_models.py +++ b/netbox/circuits/tests/test_models.py @@ -1,3 +1,5 @@ +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import NON_FIELD_ERRORS, ValidationError from django.test import TestCase from circuits.models import Circuit, CircuitTermination, CircuitType, Provider, ProviderNetwork @@ -146,3 +148,21 @@ class CircuitTerminationTestCase(TestCase): # Cache should be cleared (SET_NULL behavior) self.assertIsNone(self.circuits[0].termination_a) + + def test_termination_required_when_termination_type_is_selected(self): + """Model rejects type-without-target before generic GFK validation hits termination_id.""" + provider_network_type = ContentType.objects.get_for_model(ProviderNetwork) + + termination = CircuitTermination( + circuit=self.circuits[0], + term_side='A', + termination_type=provider_network_type, + ) + + with self.assertRaises(ValidationError) as cm: + termination.full_clean() + + errors = cm.exception.message_dict + self.assertIn(NON_FIELD_ERRORS, errors) + self.assertIn('Please select a Provider Network.', errors[NON_FIELD_ERRORS]) + self.assertNotIn('termination_id', errors) diff --git a/netbox/circuits/tests/test_signals.py b/netbox/circuits/tests/test_signals.py new file mode 100644 index 000000000..e553bcf92 --- /dev/null +++ b/netbox/circuits/tests/test_signals.py @@ -0,0 +1,92 @@ +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +from django.test import SimpleTestCase, TestCase + +from circuits import signals +from circuits.models import Circuit, CircuitTermination, CircuitType, Provider +from dcim.models import ( + Cable, + CablePath, + Device, + DeviceRole, + DeviceType, + Interface, + Manufacturer, + Site, +) + + +class RebuildCablepathsSignalTestCase(TestCase): + """ + Verify circuits.signals.rebuild_cablepaths retraces paths that cross the peer termination + when a CircuitTermination is saved or deleted. + """ + + @classmethod + def setUpTestData(cls): + cls.site = Site.objects.create(name='Site', slug='site') + manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer') + device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type') + device_role = DeviceRole.objects.create(name='Device Role', slug='device-role') + cls.device = Device.objects.create(site=cls.site, device_type=device_type, role=device_role, name='Device 1') + provider = Provider.objects.create(name='Provider', slug='provider') + circuit_type = CircuitType.objects.create(name='Circuit Type', slug='circuit-type') + cls.circuit = Circuit.objects.create(provider=provider, type=circuit_type, cid='Circuit 1') + + def test_saving_termination_rebuilds_peer_path(self): + interface = Interface.objects.create(device=self.device, name='Interface 1') + site_z = Site.objects.create(name='Site Z', slug='site-z') + termination_a = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A') + termination_z = CircuitTermination.objects.create(circuit=self.circuit, termination=site_z, term_side='Z') + Cable(a_terminations=[interface], b_terminations=[termination_a]).save() + original_path = CablePath.objects.get() + + # Saving the Z (peer) termination should cause rebuild_paths to run for the A peer. + with patch('circuits.signals.rebuild_paths') as rebuild_paths: + termination_z.save() + + rebuild_paths.assert_called_once() + rebuilt_for = rebuild_paths.call_args.args[0] + self.assertEqual([t.pk for t in rebuilt_for], [termination_a.pk]) + + # Without patching, the real signal should retrace the path successfully. + termination_z.save() + self.assertEqual(CablePath.objects.count(), 1) + self.assertNotEqual(CablePath.objects.get().pk, original_path.pk) + + def test_deleting_termination_rebuilds_peer_path(self): + site_z = Site.objects.create(name='Site Z', slug='site-z') + termination_a = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A') + termination_z = CircuitTermination.objects.create(circuit=self.circuit, termination=site_z, term_side='Z') + + with patch('circuits.signals.rebuild_paths') as rebuild_paths: + termination_z.delete() + + rebuild_paths.assert_called_once() + rebuilt_for = rebuild_paths.call_args.args[0] + self.assertEqual([t.pk for t in rebuilt_for], [termination_a.pk]) + + def test_saving_termination_without_peer_does_not_rebuild(self): + termination = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A') + + with patch('circuits.signals.rebuild_paths') as rebuild_paths: + termination.save() + + rebuild_paths.assert_not_called() + + +class RebuildCablepathsDirectHandlerTestCase(SimpleTestCase): + """ + Direct-call tests for rebuild_cablepaths branches that are not reachable through + normal model operations (e.g. raw=True is only set by Django's loaddata pathway). + """ + + def test_raw_import_skips_peer_lookup_and_rebuild(self): + instance = SimpleNamespace(get_peer_termination=MagicMock()) + + with patch.object(signals, 'rebuild_paths') as rebuild_paths: + signals.rebuild_cablepaths(instance=instance, raw=True) + + instance.get_peer_termination.assert_not_called() + rebuild_paths.assert_not_called() diff --git a/netbox/circuits/tests/test_tables.py b/netbox/circuits/tests/test_tables.py index 29c104d03..40b09a0d9 100644 --- a/netbox/circuits/tests/test_tables.py +++ b/netbox/circuits/tests/test_tables.py @@ -2,45 +2,45 @@ from circuits.tables import * from utilities.testing import TableTestCases -class CircuitTypeTableTest(TableTestCases.StandardTableTestCase): +class CircuitTypeTableTestCase(TableTestCases.StandardTableTestCase): table = CircuitTypeTable -class CircuitTableTest(TableTestCases.StandardTableTestCase): +class CircuitTableTestCase(TableTestCases.StandardTableTestCase): table = CircuitTable -class CircuitTerminationTableTest(TableTestCases.StandardTableTestCase): +class CircuitTerminationTableTestCase(TableTestCases.StandardTableTestCase): table = CircuitTerminationTable -class CircuitGroupTableTest(TableTestCases.StandardTableTestCase): +class CircuitGroupTableTestCase(TableTestCases.StandardTableTestCase): table = CircuitGroupTable -class CircuitGroupAssignmentTableTest(TableTestCases.StandardTableTestCase): +class CircuitGroupAssignmentTableTestCase(TableTestCases.StandardTableTestCase): table = CircuitGroupAssignmentTable -class ProviderTableTest(TableTestCases.StandardTableTestCase): +class ProviderTableTestCase(TableTestCases.StandardTableTestCase): table = ProviderTable -class ProviderAccountTableTest(TableTestCases.StandardTableTestCase): +class ProviderAccountTableTestCase(TableTestCases.StandardTableTestCase): table = ProviderAccountTable -class ProviderNetworkTableTest(TableTestCases.StandardTableTestCase): +class ProviderNetworkTableTestCase(TableTestCases.StandardTableTestCase): table = ProviderNetworkTable -class VirtualCircuitTypeTableTest(TableTestCases.StandardTableTestCase): +class VirtualCircuitTypeTableTestCase(TableTestCases.StandardTableTestCase): table = VirtualCircuitTypeTable -class VirtualCircuitTableTest(TableTestCases.StandardTableTestCase): +class VirtualCircuitTableTestCase(TableTestCases.StandardTableTestCase): table = VirtualCircuitTable -class VirtualCircuitTerminationTableTest(TableTestCases.StandardTableTestCase): +class VirtualCircuitTerminationTableTestCase(TableTestCases.StandardTableTestCase): table = VirtualCircuitTerminationTable diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py index b3593b927..a9de4663e 100644 --- a/netbox/circuits/tests/test_views.py +++ b/netbox/circuits/tests/test_views.py @@ -1,7 +1,6 @@ import datetime from django.contrib.contenttypes.models import ContentType -from django.test import override_settings from django.urls import reverse from circuits.choices import * @@ -210,8 +209,13 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase): self.assertContains(response, circuit_type.name) self.assertContains(response, 'background-color: #12ab34') - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[]) def test_bulk_import_objects_with_terminations(self): + self.add_permissions( + 'circuits.view_circuit', + 'circuits.view_provider', + 'circuits.view_circuittype', + 'dcim.view_site', + ) site = Site.objects.first() json_data = f""" [ @@ -420,8 +424,13 @@ class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'description': 'New description', } - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): + self.add_permissions( + 'circuits.view_circuittermination', + 'dcim.view_cable', + 'dcim.view_interface', + 'dcim.view_device', + ) device = create_test_device('Device 1') circuittermination = CircuitTermination.objects.first() @@ -711,8 +720,13 @@ class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'comments': 'New comments', } - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[]) def test_bulk_import_objects_with_terminations(self): + self.add_permissions( + 'circuits.view_virtualcircuit', + 'circuits.view_providernetwork', + 'circuits.view_virtualcircuittype', + 'dcim.view_interface', + ) interfaces = Interface.objects.filter(type=InterfaceTypeChoices.TYPE_VIRTUAL) json_data = f""" [ diff --git a/netbox/circuits/ui/panels.py b/netbox/circuits/ui/panels.py index 3572479cd..8111f61b0 100644 --- a/netbox/circuits/ui/panels.py +++ b/netbox/circuits/ui/panels.py @@ -12,16 +12,43 @@ class CircuitCircuitTerminationPanel(panels.ObjectPanel): template_name = 'circuits/panels/circuit_circuit_termination.html' title = _('Termination') + termination_ancestor_max_depth = 3 def __init__(self, side, accessor=None, **kwargs): super().__init__(accessor=accessor, **kwargs) self.side = side + def _get_termination_nodes(self, termination): + """ + Return the termination target's ancestors, including itself, when the + target is tree-like. + + Non-tree GFK targets return None, so the template preserves the current + single-object rendering. + """ + target = getattr(termination, 'termination', None) + if target is None: + return None + + get_ancestors = getattr(target, 'get_ancestors', None) + if not callable(get_ancestors): + return None + + nodes = list(get_ancestors(include_self=True)) + + if self.termination_ancestor_max_depth is not None: + nodes = nodes[-self.termination_ancestor_max_depth:] + + return nodes + def get_context(self, context): + termination = resolve_attr_path(context, f'{self.accessor}.termination_{self.side.lower()}') + return { **super().get_context(context), 'side': self.side, - 'termination': resolve_attr_path(context, f'{self.accessor}.termination_{self.side.lower()}'), + 'termination': termination, + 'termination_nodes': self._get_termination_nodes(termination), } @@ -58,7 +85,9 @@ class CircuitTerminationPanel(panels.ObjectAttributesPanel): title = _('Circuit Termination') circuit = attrs.RelatedObjectAttr('circuit', linkify=True) provider = attrs.RelatedObjectAttr('circuit.provider', linkify=True) - termination = attrs.GenericForeignKeyAttr('termination', linkify=True, label=_('Termination point')) + termination = attrs.GenericForeignKeyAttr( + 'termination', linkify=True, nested=True, max_depth=3, label=_('Termination point') + ) connection = attrs.TemplatedAttr( 'pk', template_name='circuits/circuit_termination/attrs/connection.html', @@ -91,7 +120,7 @@ class CircuitPanel(panels.ObjectAttributesPanel): cid = attrs.TextAttr('cid', label=_('Circuit ID'), style='font-monospace', copy_button=True) type = attrs.RelatedObjectAttr('type', linkify=True, colored=True) status = attrs.ChoiceAttr('status') - distance = attrs.NumericAttr('distance', unit_accessor='get_distance_unit_display') + distance = attrs.DistanceAttr('distance') tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group') install_date = attrs.DateTimeAttr('install_date', spec='date') termination_date = attrs.DateTimeAttr('termination_date', spec='date') diff --git a/netbox/core/apps.py b/netbox/core/apps.py index 68b65f58c..86ed763b5 100644 --- a/netbox/core/apps.py +++ b/netbox/core/apps.py @@ -22,7 +22,7 @@ class CoreConfig(AppConfig): def ready(self): from core.api import schema # noqa: F401 - from core.checks import check_duplicate_indexes # noqa: F401 + from core.checks import check_duplicate_indexes, check_postgresql_version # noqa: F401 from netbox import context_managers # noqa: F401 from netbox.models.features import register_models diff --git a/netbox/core/checks.py b/netbox/core/checks.py index 430eacbec..90300bdd3 100644 --- a/netbox/core/checks.py +++ b/netbox/core/checks.py @@ -1,9 +1,11 @@ from django.apps import apps -from django.core.checks import Error, Tags, register +from django.core.checks import Error, Tags, Warning, register +from django.db import connection from django.db.models import Index, UniqueConstraint __all__ = ( 'check_duplicate_indexes', + 'check_postgresql_version', ) @@ -39,3 +41,29 @@ def check_duplicate_indexes(app_configs, **kwargs): ) return errors + + +@register(Tags.database) +def check_postgresql_version(app_configs, **kwargs): + """ + Warn if the PostgreSQL version is less than 15, as support for PostgreSQL 14 + will be removed in NetBox v4.7. + """ + warnings = [] + try: + with connection.cursor() as cursor: + cursor.execute('SHOW server_version_num') + row = cursor.fetchone() + pg_version = int(row[0]) + if pg_version < 150000: + major_version = pg_version // 10000 + warnings.append( + Warning( + f'Support for PostgreSQL {major_version} is deprecated and will be removed in NetBox v4.7.', + hint='Please upgrade to PostgreSQL 15 or later.', + id='netbox.W001', + ) + ) + except Exception: + pass + return warnings diff --git a/netbox/core/data_backends.py b/netbox/core/data_backends.py index 11f9900b5..c85d81715 100644 --- a/netbox/core/data_backends.py +++ b/netbox/core/data_backends.py @@ -1,9 +1,8 @@ import logging -import os import re import tempfile from contextlib import contextmanager -from pathlib import Path +from pathlib import Path, PurePosixPath from urllib.parse import urlparse from django import forms @@ -171,6 +170,7 @@ class S3Backend(DataBackend): import boto3 local_path = tempfile.TemporaryDirectory() + local_root = Path(local_path.name).resolve() # Initialize the S3 resource and bucket aws_access_key_id = self.params.get('aws_access_key_id') @@ -185,16 +185,31 @@ class S3Backend(DataBackend): ) bucket = s3.Bucket(self._bucket_name) - # Download all files within the specified path - for obj in bucket.objects.filter(Prefix=self._remote_path): - local_filename = os.path.join(local_path.name, obj.key) - # Build local path - Path(os.path.dirname(local_filename)).mkdir(parents=True, exist_ok=True) - bucket.download_file(obj.key, local_filename) + try: + # Download all files within the specified path + for obj in bucket.objects.filter(Prefix=self._remote_path): + local_filename = self._resolve_local_path(local_root, obj.key) + # Build local path + local_filename.parent.mkdir(parents=True, exist_ok=True) + bucket.download_file(obj.key, str(local_filename)) - yield local_path.name + yield local_path.name + finally: + local_path.cleanup() - local_path.cleanup() + @staticmethod + def _resolve_local_path(local_root, key): + # S3 object keys are POSIX-style paths. Strip any leading separator so the key + # joins onto the temp directory rather than replacing it, then ensure the + # resolved destination remains within the temp directory to prevent path + # traversal via crafted object keys. + key_parts = PurePosixPath(key.lstrip('/')).parts + local_filename = local_root.joinpath(*key_parts).resolve() + if not local_filename.is_relative_to(local_root) or local_filename == local_root: + raise SyncError( + _("Invalid S3 object key '{key}': resolves outside of the local data directory").format(key=key) + ) + return local_filename @property def _region_name(self): diff --git a/netbox/core/filtersets.py b/netbox/core/filtersets.py index 2539b0c8a..7416dd7de 100644 --- a/netbox/core/filtersets.py +++ b/netbox/core/filtersets.py @@ -137,7 +137,24 @@ class JobFilterSet(BaseFilterSet): distinct=False, null_value=None ) + notifications = django_filters.MultipleChoiceFilter( + choices=JobNotificationChoices, + distinct=False, + null_value=None + ) queue_name = django_filters.CharFilter() + user_id = django_filters.ModelMultipleChoiceFilter( + queryset=User.objects.all(), + distinct=False, + label=_('User (ID)'), + ) + user = django_filters.ModelMultipleChoiceFilter( + field_name='user__username', + queryset=User.objects.all(), + distinct=False, + to_field_name='username', + label=_('User name'), + ) class Meta: model = Job diff --git a/netbox/core/jobs.py b/netbox/core/jobs.py index 46dc10d1c..e55973c0f 100644 --- a/netbox/core/jobs.py +++ b/netbox/core/jobs.py @@ -222,8 +222,11 @@ class SystemHousekeepingJob(JobRunner): if 'tag_name' not in release or release.get('devrelease') or release.get('prerelease'): continue releases.append((version.parse(release['tag_name']), release.get('html_url'))) - latest_release = max(releases) self.logger.debug(f"Found {len(response.json())} releases; {len(releases)} usable") + if not releases: + self.logger.info("No usable releases found; skipping") + return + latest_release = max(releases) self.logger.info(f"Latest release: {latest_release[0]}") # Cache the most recent release diff --git a/netbox/core/management/commands/nbshell.py b/netbox/core/management/commands/nbshell.py index 637719a61..800f7b401 100644 --- a/netbox/core/management/commands/nbshell.py +++ b/netbox/core/management/commands/nbshell.py @@ -150,7 +150,7 @@ class Command(BaseCommand): try: import readline import rlcompleter - except ModuleNotFoundError: + except ModuleNotFoundError: # pragma: no cover pass else: readline.set_completer(rlcompleter.Completer(namespace).complete) diff --git a/netbox/core/signals.py b/netbox/core/signals.py index cdb787667..cfd10c988 100644 --- a/netbox/core/signals.py +++ b/netbox/core/signals.py @@ -225,7 +225,23 @@ def handle_deleted_object(sender, instance, **kwargs): # We only care about triggering the m2m_changed signal for models which support # change logging continue + related_object_type = ContentType.objects.get_for_model(related_model) for obj in related_model.objects.filter(**{related_field_name: instance.pk}): + # Skip any related object that is itself being deleted as part of this same + # operation (e.g. a sibling caught up in the same cascade). Its deletion has + # already been recorded, so nulling the FK and saving here would record an + # UPDATE ObjectChange *after* the object's DELETE, corrupting the changelog and + # breaking branch replay. (Ref: #22270) + # + # Note this is order-dependent: it only fires once the related object's own + # pre_delete has run (adding it to the set). If the cascade happens to delete + # this instance *before* the related object, the guard won't trigger and the + # related object still gets an UPDATE — but in the harmless UPDATE-then-DELETE + # order, not the corrupting DELETE-then-UPDATE order. Fully suppressing it in + # every ordering would require the complete deletion set, which isn't available + # from a pre_delete signal. + if (related_object_type, obj.pk) in _signals_received.pre_delete: + continue obj.snapshot() # Ensure the change record includes the "before" state if type(relation) is ManyToManyRel: getattr(obj, related_field_name).remove(instance) diff --git a/netbox/core/tests/query_counts.json b/netbox/core/tests/query_counts.json new file mode 100644 index 000000000..c6a476cd7 --- /dev/null +++ b/netbox/core/tests/query_counts.json @@ -0,0 +1,8 @@ +{ + "datafile:api_list_objects": 10, + "datafile:list_objects_with_permission": 18, + "datasource:api_list_objects": 12, + "datasource:list_objects_with_permission": 20, + "job:api_list_objects": 12, + "job:list_objects_with_permission": 19 +} diff --git a/netbox/core/tests/test_api.py b/netbox/core/tests/test_api.py index 3d1857481..2a159f6bd 100644 --- a/netbox/core/tests/test_api.py +++ b/netbox/core/tests/test_api.py @@ -1,5 +1,6 @@ import uuid +from django.contrib.contenttypes.models import ContentType from django.urls import reverse from django.utils import timezone from django_rq import get_queue @@ -10,14 +11,15 @@ from rq.job import JobStatus from rq.registry import FailedJobRegistry, StartedJobRegistry from users.constants import TOKEN_PREFIX -from users.models import Token, User +from users.models import Token from utilities.testing import APITestCase, APIViewTestCases, TestCase +from utilities.testing.mixins import RQQueueTestMixin from utilities.testing.utils import disable_logging from ..models import * -class AppTest(APITestCase): +class AppTestCase(APITestCase): def test_root(self): url = reverse('core-api:api-root') @@ -26,7 +28,7 @@ class AppTest(APITestCase): self.assertEqual(response.status_code, 200) -class DataSourceTest(APIViewTestCases.APIViewTestCase): +class DataSourceTestCase(APIViewTestCases.APIViewTestCase): model = DataSource brief_fields = ['description', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -62,7 +64,7 @@ class DataSourceTest(APIViewTestCases.APIViewTestCase): ] -class DataFileTest( +class DataFileTestCase( APIViewTestCases.GetObjectViewTestCase, APIViewTestCases.ListObjectsViewTestCase, APIViewTestCases.GraphQLTestCase @@ -105,7 +107,8 @@ class DataFileTest( DataFile.objects.bulk_create(data_files) -class ObjectTypeTest(APITestCase): +class ObjectTypeTestCase(APITestCase): + model = ObjectType def test_list_objects(self): object_type_count = ObjectType.objects.count() @@ -121,7 +124,52 @@ class ObjectTypeTest(APITestCase): self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_200_OK) -class BackgroundTaskTestCase(TestCase): +class JobTestCase( + APIViewTestCases.GetObjectViewTestCase, + APIViewTestCases.ListObjectsViewTestCase, +): + model = Job + brief_fields = ['completed', 'created', 'status', 'url', 'user'] + + @classmethod + def setUpTestData(cls): + datasource = DataSource.objects.create( + name='Data Source 1', + type='local', + source_url='file:///var/tmp/source1/', + ) + ct = ContentType.objects.get_for_model(DataSource) + Job.objects.bulk_create( + [ + Job( + name='Job 1', + object_type=ct, + object_id=datasource.pk, + status='pending', + queue_name='default', + job_id=uuid.uuid4(), + ), + Job( + name='Job 2', + object_type=ct, + object_id=datasource.pk, + status='running', + queue_name='default', + job_id=uuid.uuid4(), + ), + Job( + name='Job 3', + object_type=ct, + object_id=datasource.pk, + status='completed', + queue_name='default', + job_id=uuid.uuid4(), + ), + ] + ) + + +class BackgroundTaskTestCase(RQQueueTestMixin, TestCase): user_permissions = () @staticmethod @@ -133,19 +181,14 @@ class BackgroundTaskTestCase(TestCase): raise Exception("Job failed") def setUp(self): - """ - Create a user and token for API calls. - """ - # Create the test user and assign permissions - self.user = User.objects.create_user(username='testuser', is_active=True) + super().setUp() + + # The base TestCase creates self.user; make it active and create a token for API calls. + self.user.is_active = True + self.user.save() self.token = Token.objects.create(user=self.user) self.header = {'HTTP_AUTHORIZATION': f'Bearer {TOKEN_PREFIX}{self.token.key}.{self.token.token}'} - # Clear all queues prior to running each test - get_queue('default').connection.flushall() - get_queue('high').connection.flushall() - get_queue('low').connection.flushall() - def test_background_queue_list(self): url = reverse('core-api:rqqueue-list') diff --git a/netbox/core/tests/test_changelog.py b/netbox/core/tests/test_changelog.py index dc9dbb2dc..0d00972b9 100644 --- a/netbox/core/tests/test_changelog.py +++ b/netbox/core/tests/test_changelog.py @@ -33,7 +33,7 @@ from utilities.testing.utils import create_tags, create_test_device, post_data from utilities.testing.views import ModelViewTestCase -class ChangeLogViewTest(ModelViewTestCase): +class ChangeLogViewTestCase(ModelViewTestCase): model = Site @classmethod @@ -397,7 +397,7 @@ class ChangeLogViewTest(ModelViewTestCase): self.assertEqual(objectchanges.count(), 2) -class ChangeLogAPITest(APITestCase): +class ChangeLogAPITestCase(APITestCase): @classmethod def setUpTestData(cls): @@ -703,7 +703,7 @@ class ChangeLogAPITest(APITestCase): self.assertEqual(changes[3].action, ObjectChangeActionChoices.ACTION_DELETE) -class ChangelogPruneRetentionTest(TestCase): +class ChangelogPruneRetentionTestCase(TestCase): """Test suite for Changelog pruning retention settings.""" @staticmethod diff --git a/netbox/core/tests/test_data_backends.py b/netbox/core/tests/test_data_backends.py index 8020eb76c..9f96a0cb3 100644 --- a/netbox/core/tests/test_data_backends.py +++ b/netbox/core/tests/test_data_backends.py @@ -1,9 +1,12 @@ +import tempfile +from pathlib import Path from unittest import skipIf from unittest.mock import patch from django.test import TestCase -from core.data_backends import url_has_embedded_credentials +from core.data_backends import S3Backend, url_has_embedded_credentials +from core.exceptions import SyncError try: import dulwich # noqa: F401 @@ -12,7 +15,7 @@ except ImportError: DULWICH_AVAILABLE = False -class URLEmbeddedCredentialsTests(TestCase): +class URLEmbeddedCredentialsTestCase(TestCase): def test_url_with_embedded_username(self): self.assertTrue(url_has_embedded_credentials('https://myuser@bitbucket.org/workspace/repo.git')) @@ -54,7 +57,7 @@ class URLEmbeddedCredentialsTests(TestCase): @skipIf(not DULWICH_AVAILABLE, "dulwich is not installed") -class GitBackendCredentialIntegrationTests(TestCase): +class GitBackendCredentialIntegrationTestCase(TestCase): """ Integration tests that verify GitBackend correctly applies credential logic. @@ -114,3 +117,49 @@ class GitBackendCredentialIntegrationTests(TestCase): self.assertEqual(kwargs.get('username'), None) self.assertEqual(kwargs.get('password'), None) + + +class S3BackendKeyResolutionTestCase(TestCase): + """ + Verify that S3Backend resolves object keys to local paths safely, rejecting any + key that would write outside of the local temporary directory. + """ + + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.addCleanup(self._tmp.cleanup) + self.local_root = Path(self._tmp.name).resolve() + + def test_simple_key_resolves_under_root(self): + result = S3Backend._resolve_local_path(self.local_root, 'data/file.yaml') + self.assertEqual(result, self.local_root / 'data' / 'file.yaml') + + def test_nested_key_resolves_under_root(self): + result = S3Backend._resolve_local_path(self.local_root, 'a/b/c/d.txt') + self.assertEqual(result, self.local_root / 'a' / 'b' / 'c' / 'd.txt') + + def test_absolute_key_is_contained(self): + # A key beginning with '/' must not escape the local root + result = S3Backend._resolve_local_path(self.local_root, '/etc/passwd') + self.assertEqual(result, self.local_root / 'etc' / 'passwd') + self.assertTrue(result.is_relative_to(self.local_root)) + + def test_parent_traversal_is_rejected(self): + with self.assertRaises(SyncError): + S3Backend._resolve_local_path(self.local_root, '../escape.txt') + + def test_nested_parent_traversal_is_rejected(self): + with self.assertRaises(SyncError): + S3Backend._resolve_local_path(self.local_root, 'foo/../../escape.txt') + + def test_absolute_parent_traversal_is_rejected(self): + with self.assertRaises(SyncError): + S3Backend._resolve_local_path(self.local_root, '/../escape.txt') + + def test_empty_key_is_rejected(self): + with self.assertRaises(SyncError): + S3Backend._resolve_local_path(self.local_root, '') + + def test_root_key_is_rejected(self): + with self.assertRaises(SyncError): + S3Backend._resolve_local_path(self.local_root, '/') diff --git a/netbox/core/tests/test_filtersets.py b/netbox/core/tests/test_filtersets.py index e9045832a..d8513d90a 100644 --- a/netbox/core/tests/test_filtersets.py +++ b/netbox/core/tests/test_filtersets.py @@ -244,6 +244,56 @@ class ObjectChangeTestCase(TestCase, BaseFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) +class JobTestCase(TestCase, BaseFilterSetTests): + queryset = Job.objects.all() + filterset = JobFilterSet + ignore_fields = ('data', 'error', 'log_entries') + + @classmethod + def setUpTestData(cls): + users = ( + User(username='user1'), + User(username='user2'), + User(username='user3'), + ) + User.objects.bulk_create(users) + + jobs = ( + Job( + name='Job 1', job_id=uuid.uuid4(), user=users[0], + notifications=JobNotificationChoices.NOTIFICATION_ALWAYS, + ), + Job( + name='Job 2', job_id=uuid.uuid4(), user=users[0], + notifications=JobNotificationChoices.NOTIFICATION_ALWAYS, + ), + Job( + name='Job 3', job_id=uuid.uuid4(), user=users[1], + notifications=JobNotificationChoices.NOTIFICATION_ON_FAILURE, + ), + Job( + name='Job 4', job_id=uuid.uuid4(), user=users[2], + notifications=JobNotificationChoices.NOTIFICATION_NEVER, + ), + ) + Job.objects.bulk_create(jobs) + + def test_user(self): + """Filter Jobs by user (ID and username).""" + params = {'user_id': User.objects.filter(username__in=['user1', 'user2']).values_list('pk', flat=True)} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + params = {'user': ['user1', 'user2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + + def test_notifications(self): + """Filter Jobs by notification policy.""" + params = {'notifications': [ + JobNotificationChoices.NOTIFICATION_ALWAYS, + JobNotificationChoices.NOTIFICATION_ON_FAILURE, + ]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + + class ObjectTypeTestCase(TestCase, BaseFilterSetTests): queryset = ObjectType.objects.all() filterset = ObjectTypeFilterSet diff --git a/netbox/core/tests/test_jobs.py b/netbox/core/tests/test_jobs.py new file mode 100644 index 000000000..650d843a9 --- /dev/null +++ b/netbox/core/tests/test_jobs.py @@ -0,0 +1,387 @@ +import uuid +from datetime import timedelta +from unittest.mock import MagicMock, patch + +import requests +from django.conf import settings +from django.test import TestCase, override_settings +from django.utils import timezone + +from core.choices import DataSourceStatusChoices +from core.jobs import SyncDataSourceJob, SystemHousekeepingJob +from core.models import DataFile, DataSource, Job + + +def _make_runner(cls, **job_attrs): + """ + Build a JobRunner without going through ``__init__``. + + ``JobRunner.__init__`` attaches a ``JobLogHandler`` to a module-level + singleton logger, so calling it once per test would accumulate handlers + across the suite. Bypass it and stub the logger directly. + """ + runner = cls.__new__(cls) + runner.job = MagicMock(**job_attrs) + runner.logger = MagicMock() + return runner + + +class HousekeepingRunnerMixin: + """Provides a `_runner()` helper that builds a SystemHousekeepingJob with a mock job.""" + + @staticmethod + def _runner(): + return _make_runner(SystemHousekeepingJob) + + +class SyncDataSourceJobTestCase(TestCase): + @classmethod + def setUpTestData(cls): + cls.datasource = DataSource.objects.create( + name='Test Source', + type='local', + source_url='/tmp/test', + ) + + def test_enqueue_sets_datasource_status_to_queued(self): + job = MagicMock() + job.object = self.datasource + + with patch('core.models.Job.enqueue', return_value=job): + result = SyncDataSourceJob.enqueue(instance=self.datasource) + + self.assertIs(result, job) + # Verify both the in-memory assignment (`datasource.status = ...`) and the + # persisted update (`DataSource.objects.filter(pk=...).update(...)`). + self.assertEqual(self.datasource.status, DataSourceStatusChoices.QUEUED) + self.datasource.refresh_from_db() + self.assertEqual(self.datasource.status, DataSourceStatusChoices.QUEUED) + + def test_enqueue_without_object_is_noop(self): + # Baseline: the datasource starts at NEW. (Captures setUpTestData drift.) + self.assertEqual(self.datasource.status, DataSourceStatusChoices.NEW) + + job = MagicMock() + job.object = None + + with ( + patch('core.models.Job.enqueue', return_value=job), + patch( + 'core.jobs.DataSource.objects.filter', + wraps=DataSource.objects.filter, + ) as filter_, + ): + result = SyncDataSourceJob.enqueue() + + self.assertIs(result, job) + # Intent: the `if datasource := job.object` branch was skipped, so no + # filter().update() call was even attempted. + filter_.assert_not_called() + # Outcome: no side effect on any existing DataSource. + self.datasource.refresh_from_db() + self.assertEqual(self.datasource.status, DataSourceStatusChoices.NEW) + + def test_run_syncs_datasource_and_updates_search_cache(self): + datafile = DataFile.objects.create( + source=self.datasource, + path='test.txt', + last_updated=timezone.now(), + size=4, + hash='0' * 64, + data=b'test', + ) + runner = _make_runner(SyncDataSourceJob, object_id=self.datasource.pk) + + with ( + patch('core.models.DataSource.sync') as sync, + patch('core.jobs.search_backend.cache') as cache, + ): + runner.run() + + sync.assert_called_once_with() + cache.assert_called_once() + # The cache argument should iterate over the datasource's data files. + cache_arg = cache.call_args.args[0] + self.assertEqual(list(cache_arg), [datafile]) + + def test_run_marks_datasource_failed_and_reraises_on_sync_error(self): + runner = _make_runner(SyncDataSourceJob, object_id=self.datasource.pk) + + with ( + patch('core.models.DataSource.sync', side_effect=RuntimeError('boom')), + patch('core.jobs.search_backend.cache') as cache, + ): + with self.assertRaisesMessage(RuntimeError, 'boom'): + runner.run() + + self.datasource.refresh_from_db() + self.assertEqual(self.datasource.status, DataSourceStatusChoices.FAILED) + cache.assert_not_called() + + def test_run_raises_when_datasource_no_longer_exists(self): + # If the DataSource was deleted between enqueue and run, the initial lookup + # raises DoesNotExist; the framework will surface it as a job error. + runner = _make_runner(SyncDataSourceJob, object_id=99_999_999) + + with self.assertRaises(DataSource.DoesNotExist): + runner.run() + + +class SystemHousekeepingRunTestCase(HousekeepingRunnerMixin, TestCase): + SUBMETHODS = ( + 'send_census_report', + 'clear_expired_sessions', + 'prune_changelog', + 'delete_expired_jobs', + 'check_for_new_releases', + ) + + @override_settings(DEBUG=True) + def test_run_skips_when_debug_is_enabled(self): + # DEBUG is checked before sys.argv; sys.argv is irrelevant here. + runner = self._runner() + patches = {name: patch.object(SystemHousekeepingJob, name) for name in self.SUBMETHODS} + mocks = {name: p.start() for name, p in patches.items()} + self.addCleanup(lambda: [p.stop() for p in patches.values()]) + + runner.run() + + for mock in mocks.values(): + mock.assert_not_called() + + @override_settings(DEBUG=False) + def test_run_skips_during_test_invocation(self): + runner = self._runner() + patches = {name: patch.object(SystemHousekeepingJob, name) for name in self.SUBMETHODS} + mocks = {name: p.start() for name, p in patches.items()} + self.addCleanup(lambda: [p.stop() for p in patches.values()]) + + with patch('core.jobs.sys.argv', ['manage.py', 'test']): + runner.run() + + for mock in mocks.values(): + mock.assert_not_called() + + @override_settings(DEBUG=False) + def test_run_executes_all_housekeeping_tasks(self): + runner = self._runner() + patches = {name: patch.object(SystemHousekeepingJob, name) for name in self.SUBMETHODS} + mocks = {name: p.start() for name, p in patches.items()} + self.addCleanup(lambda: [p.stop() for p in patches.values()]) + + with patch('core.jobs.sys.argv', ['netbox']): + runner.run() + + for mock in mocks.values(): + mock.assert_called_once_with() + + +class SendCensusReportTestCase(HousekeepingRunnerMixin, TestCase): + @override_settings(ISOLATED_DEPLOYMENT=True) + def test_send_census_report_skips_when_isolated_deployment(self): + with patch('core.jobs.requests.get') as get: + self._runner().send_census_report() + get.assert_not_called() + + @override_settings(ISOLATED_DEPLOYMENT=False, CENSUS_REPORTING_ENABLED=False) + def test_send_census_report_skips_when_reporting_disabled(self): + with patch('core.jobs.requests.get') as get: + self._runner().send_census_report() + get.assert_not_called() + + @override_settings( + ISOLATED_DEPLOYMENT=False, + CENSUS_REPORTING_ENABLED=True, + CENSUS_URL='https://census.example/', + DEPLOYMENT_ID='abc123', + ) + def test_send_census_report_sends_expected_payload(self): + with ( + patch('core.jobs.requests.get') as get, + patch('core.jobs.resolve_proxies', return_value={'https': 'proxy'}) as resolve, + ): + self._runner().send_census_report() + + resolve.assert_called_once_with(url='https://census.example/') + get.assert_called_once() + kwargs = get.call_args.kwargs + self.assertEqual(kwargs['url'], 'https://census.example/') + self.assertEqual(kwargs['timeout'], 3) + self.assertEqual(kwargs['proxies'], {'https': 'proxy'}) + self.assertEqual(kwargs['params']['deployment_id'], 'abc123') + self.assertIn('version', kwargs['params']) + self.assertIn('python_version', kwargs['params']) + + @override_settings( + ISOLATED_DEPLOYMENT=False, + CENSUS_REPORTING_ENABLED=True, + CENSUS_URL='https://census.example/', + ) + def test_send_census_report_swallows_request_exception(self): + with ( + patch('core.jobs.requests.get', side_effect=requests.RequestException('down')), + patch('core.jobs.resolve_proxies', return_value={}), + ): + self._runner().send_census_report() # must not raise + + +class ClearExpiredSessionsTestCase(HousekeepingRunnerMixin, TestCase): + def test_clear_expired_sessions_calls_session_store(self): + engine = MagicMock() + with patch('core.jobs.import_module', return_value=engine) as import_module: + self._runner().clear_expired_sessions() + + import_module.assert_called_once_with(settings.SESSION_ENGINE) + engine.SessionStore.clear_expired.assert_called_once_with() + + def test_clear_expired_sessions_handles_not_implemented(self): + engine = MagicMock() + engine.SessionStore.clear_expired.side_effect = NotImplementedError + runner = self._runner() + + with patch('core.jobs.import_module', return_value=engine): + runner.clear_expired_sessions() # must not raise + + runner.logger.warning.assert_called_once() + self.assertIn( + 'does not support', + runner.logger.warning.call_args.args[0], + ) + + +class DeleteExpiredJobsTestCase(HousekeepingRunnerMixin, TestCase): + def test_delete_expired_jobs_skips_when_retention_unset(self): + old_job = Job.objects.create(name='old', job_id=uuid.uuid4()) + Job.objects.filter(pk=old_job.pk).update(created=timezone.now() - timedelta(days=365)) + + with patch('core.jobs.Config') as config_cls: + config_cls.return_value.JOB_RETENTION = 0 + self._runner().delete_expired_jobs() + + self.assertTrue(Job.objects.filter(pk=old_job.pk).exists()) + + def test_delete_expired_jobs_deletes_only_jobs_older_than_retention(self): + old_job = Job.objects.create(name='old', job_id=uuid.uuid4()) + recent_job = Job.objects.create(name='recent', job_id=uuid.uuid4()) + Job.objects.filter(pk=old_job.pk).update(created=timezone.now() - timedelta(days=30)) + Job.objects.filter(pk=recent_job.pk).update(created=timezone.now() - timedelta(hours=1)) + + with patch('core.jobs.Config') as config_cls: + config_cls.return_value.JOB_RETENTION = 7 + self._runner().delete_expired_jobs() + + self.assertFalse(Job.objects.filter(pk=old_job.pk).exists()) + self.assertTrue(Job.objects.filter(pk=recent_job.pk).exists()) + + +class CheckForNewReleasesTestCase(HousekeepingRunnerMixin, TestCase): + @override_settings(ISOLATED_DEPLOYMENT=True) + def test_check_for_new_releases_skips_when_isolated(self): + with patch('core.jobs.requests.get') as get: + self._runner().check_for_new_releases() + get.assert_not_called() + + @override_settings(ISOLATED_DEPLOYMENT=False, RELEASE_CHECK_URL=None) + def test_check_for_new_releases_skips_when_url_unset(self): + with patch('core.jobs.requests.get') as get: + self._runner().check_for_new_releases() + get.assert_not_called() + + @override_settings(ISOLATED_DEPLOYMENT=False, RELEASE_CHECK_URL='https://api.example/') + def test_check_for_new_releases_handles_request_exception(self): + with ( + patch('core.jobs.requests.get', side_effect=requests.RequestException('down')), + patch('core.jobs.cache') as cache, + patch('core.jobs.resolve_proxies', return_value={}), + ): + self._runner().check_for_new_releases() + + cache.set.assert_not_called() + + @override_settings(ISOLATED_DEPLOYMENT=False, RELEASE_CHECK_URL='https://api.example/') + def test_check_for_new_releases_handles_no_stable_releases(self): + # All entries are filtered out (prereleases, devreleases, or missing tag_name); + # check_for_new_releases() must not crash on the resulting empty list. + response = MagicMock() + response.json.return_value = [ + {'tag_name': 'v4.7.0-rc1', 'html_url': 'https://example/rc', 'prerelease': True}, + {'tag_name': 'v4.7.0-dev', 'html_url': 'https://example/dev', 'devrelease': True}, + {'html_url': 'https://example/no-tag'}, + ] + + with ( + patch('core.jobs.requests.get', return_value=response), + patch('core.jobs.cache.set') as cache_set, + patch('core.jobs.resolve_proxies', return_value={}), + ): + self._runner().check_for_new_releases() + + cache_set.assert_not_called() + + @override_settings(ISOLATED_DEPLOYMENT=False, RELEASE_CHECK_URL='https://api.example/') + def test_check_for_new_releases_caches_latest_stable_release(self): + response = MagicMock() + response.json.return_value = [ + {'tag_name': 'v4.5.0', 'html_url': 'https://example/4.5.0'}, + {'tag_name': 'v4.6.0', 'html_url': 'https://example/4.6.0'}, + {'tag_name': 'v4.7.0-rc1', 'html_url': 'https://example/rc', 'prerelease': True}, + {'tag_name': 'v4.7.0-dev', 'html_url': 'https://example/dev', 'devrelease': True}, + {'html_url': 'https://example/no-tag'}, + ] + + with ( + patch('core.jobs.requests.get', return_value=response) as get, + patch('core.jobs.cache.set') as cache_set, + patch('core.jobs.resolve_proxies', return_value={'http': 'proxy'}) as resolve, + ): + self._runner().check_for_new_releases() + + # HTTP request: URL, GitHub API Accept header, resolved proxies. + resolve.assert_called_once_with(url='https://api.example/') + get.assert_called_once_with( + url='https://api.example/', + headers={'Accept': 'application/vnd.github.v3+json'}, + proxies={'http': 'proxy'}, + ) + response.raise_for_status.assert_called_once_with() + + cache_set.assert_called_once() + # Accept either positional or keyword form for cache.set(key, value, ttl). + call = cache_set.call_args + bound = {**dict(zip(('key', 'value', 'timeout'), call.args)), **call.kwargs} + self.assertEqual(bound['key'], 'latest_release') + self.assertIsNone(bound['timeout']) + latest_version, latest_url = bound['value'] + self.assertEqual(str(latest_version), '4.6.0') + self.assertEqual(latest_url, 'https://example/4.6.0') + + +class PruneChangelogTestCase(HousekeepingRunnerMixin, TestCase): + def test_prune_changelog_skips_when_retention_unset(self): + with ( + patch('core.jobs.Config') as config_cls, + patch('core.jobs.ObjectChange') as object_change, + ): + config_cls.return_value.CHANGELOG_RETENTION = None + self._runner().prune_changelog() + + object_change.objects.filter.assert_not_called() + + def test_prune_changelog_uses_strict_cutoff_filter(self): + # Implementation pin: prune_changelog must use time__lt (strict less-than) so a + # record exactly at the cutoff is retained. End-to-end behavior of the prune is + # covered by ChangelogPruneRetentionTestCase in core/tests/test_changelog.py. + with ( + patch('core.jobs.Config') as config_cls, + patch('core.jobs.ObjectChange') as object_change, + patch('core.jobs.timezone') as tz, + ): + config_cls.return_value.CHANGELOG_RETENTION = 7 + config_cls.return_value.CHANGELOG_RETAIN_CREATE_LAST_UPDATE = False + tz.now.return_value = timezone.datetime(2026, 1, 8, tzinfo=timezone.get_current_timezone()) + object_change.objects.filter.return_value.delete.return_value = (0, {}) + + self._runner().prune_changelog() + + expected_cutoff = timezone.datetime(2026, 1, 1, tzinfo=timezone.get_current_timezone()) + object_change.objects.filter.assert_called_once_with(time__lt=expected_cutoff) diff --git a/netbox/core/tests/test_management_command_coverage.py b/netbox/core/tests/test_management_command_coverage.py new file mode 100644 index 000000000..7f964fcb6 --- /dev/null +++ b/netbox/core/tests/test_management_command_coverage.py @@ -0,0 +1,79 @@ +import ast +from pathlib import Path + +from django.apps import apps +from django.conf import settings +from django.test import SimpleTestCase + +EXCLUDED_CUSTOM_COMMANDS = { + # Deprecated; excluded from management command test coverage by #22124. + 'housekeeping', +} + + +class ManagementCommandCoverageTestCase(SimpleTestCase): + def test_all_custom_management_commands_have_tests(self): + custom_commands = self._get_custom_management_commands() + tested_commands = self._get_tested_management_commands() + + self.assertTrue( + custom_commands, + 'No custom management commands were discovered; check command discovery logic.', + ) + + missing_commands = sorted(custom_commands - tested_commands - EXCLUDED_CUSTOM_COMMANDS) + + self.assertEqual( + missing_commands, + [], + msg=(f'Tests are missing for custom management commands: {", ".join(missing_commands)}'), + ) + + @staticmethod + def _get_custom_management_commands(): + base_dir = Path(settings.BASE_DIR).resolve() + commands = set() + + for app_config in apps.get_app_configs(): + app_path = Path(app_config.path).resolve() + if not app_path.is_relative_to(base_dir): + continue + + commands_path = app_path / 'management' / 'commands' + if not commands_path.exists(): + continue + + commands.update(path.stem for path in commands_path.glob('*.py') if not path.name.startswith('_')) + + return commands + + @staticmethod + def _get_tested_management_commands(): + base_dir = Path(settings.BASE_DIR).resolve() + commands = set() + + for test_file in base_dir.glob('*/tests/test_management_commands.py'): + tree = ast.parse(test_file.read_text(encoding='utf-8')) + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + if not _is_call_command(node.func): + continue + if not node.args: + continue + + command_name = node.args[0] + if isinstance(command_name, ast.Constant) and isinstance(command_name.value, str): + commands.add(command_name.value) + + return commands + + +def _is_call_command(func): + if isinstance(func, ast.Name): + return func.id == 'call_command' + + if isinstance(func, ast.Attribute): + return func.attr == 'call_command' + + return False diff --git a/netbox/core/tests/test_management_commands.py b/netbox/core/tests/test_management_commands.py new file mode 100644 index 000000000..7aa970a23 --- /dev/null +++ b/netbox/core/tests/test_management_commands.py @@ -0,0 +1,317 @@ +from io import StringIO +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +from django.core.management import call_command +from django.core.management.base import CommandError +from django.test import TestCase, override_settings + +from core.choices import DataSourceStatusChoices +from core.management.commands import nbshell +from core.management.commands.rqworker import DEFAULT_QUEUES + + +class MakeMigrationsTestCase(TestCase): + @override_settings(DEVELOPER=False) + def test_blocked_in_non_developer_mode(self): + with self.assertRaisesMessage(CommandError, 'development purposes only'): + call_command('makemigrations', stdout=StringIO(), stderr=StringIO()) + + @override_settings(DEVELOPER=False) + def test_check_flag_allowed_in_non_developer_mode(self): + with patch('core.management.commands.makemigrations._Command.handle') as super_handle: + call_command( + 'makemigrations', + check_changes=True, + stdout=StringIO(), + stderr=StringIO(), + ) + + super_handle.assert_called_once() + self.assertTrue(super_handle.call_args.kwargs['check_changes']) + + +class NbShellTestCase(TestCase): + def test_color_helpers_wrap_text(self): + self.assertIn('message', nbshell.color('green', 'message')) + self.assertIn('message', nbshell.bright('message')) + + def test_get_models_excludes_private_models(self): + public_model = type('PublicModel', (), {}) + private_model = type('PrivateModel', (), {'_netbox_private': True}) + app_config = SimpleNamespace(get_models=lambda: [public_model, private_model]) + + self.assertEqual(nbshell.get_models(app_config), [public_model]) + + def test_get_constants_returns_module_attributes(self): + constants = SimpleNamespace(FOO='bar', ANSWER=42) + + with patch('core.management.commands.nbshell.import_string', return_value=constants): + self.assertEqual( + nbshell.get_constants(SimpleNamespace(name='testapp')), + {'FOO': 'bar', 'ANSWER': 42}, + ) + + def test_get_constants_handles_missing_constants_module(self): + with patch('core.management.commands.nbshell.import_string', side_effect=ImportError): + self.assertEqual(nbshell.get_constants(SimpleNamespace(name='testapp')), {}) + + def test_executes_inline_command(self): + namespace = {} + + with patch( + 'core.management.commands.nbshell.Command.get_namespace', + return_value=namespace, + ): + call_command('nbshell', command='answer = 42') + + self.assertEqual(namespace['answer'], 42) + + def test_starts_interactive_shell_without_inline_command(self): + namespace = {'answer': 42} + + with ( + patch('core.management.commands.nbshell.Command.get_namespace', return_value=namespace), + patch('core.management.commands.nbshell.Command.get_banner_text', return_value='banner'), + patch('core.management.commands.nbshell.code.interact', return_value=None) as interact, + ): + call_command('nbshell', stdout=StringIO()) + + interact.assert_called_once_with(banner='banner', local=namespace) + + def test_get_namespace_includes_models_constants_and_helpers(self): + class DummyModel: + pass + + app_config = SimpleNamespace( + name='dummyapp', + get_models=lambda: [DummyModel], + ) + command = nbshell.Command() + command.django_models = {} + + with ( + patch('core.management.commands.nbshell.CORE_APPS', ('dummyapp',)), + patch('core.management.commands.nbshell.get_installed_plugins', return_value={}), + patch('core.management.commands.nbshell.apps.get_app_config', return_value=app_config), + patch('core.management.commands.nbshell.get_constants', return_value={'CONSTANT': 'value'}), + ): + namespace = command.get_namespace() + + self.assertIs(namespace['dummyapp'].DummyModel, DummyModel) + self.assertEqual(namespace['dummyapp'].CONSTANT, 'value') + self.assertEqual(command.django_models['dummyapp'], ['DummyModel']) + self.assertEqual(namespace['lsapps'], command._lsapps) + self.assertEqual(namespace['lsmodels'], command._lsmodels) + + def test_list_apps_and_models_helpers(self): + command = nbshell.Command() + command.django_models = {'dcim': ['Device', 'Site']} + app_config = SimpleNamespace(verbose_name='DCIM') + + with ( + patch('core.management.commands.nbshell.apps.get_app_config', return_value=app_config), + patch('builtins.print') as print_, + ): + command._lsapps() + command._lsmodels('dcim') + + self.assertIn(('dcim - DCIM',), [call.args for call in print_.call_args_list]) + self.assertIn(('DCIM:',), [call.args for call in print_.call_args_list]) + self.assertIn((' dcim.Device',), [call.args for call in print_.call_args_list]) + self.assertIn((' dcim.Site',), [call.args for call in print_.call_args_list]) + + def test_list_models_reports_unknown_app(self): + command = nbshell.Command() + command.django_models = {} + + with patch('builtins.print') as print_: + command._lsmodels('unknown') + + print_.assert_called_once_with('No models listed for unknown') + + def test_list_models_lists_all_apps_when_no_app_label_given(self): + command = nbshell.Command() + command.django_models = {'dcim': ['Device'], 'ipam': ['IPAddress']} + app_configs = { + 'dcim': SimpleNamespace(verbose_name='DCIM'), + 'ipam': SimpleNamespace(verbose_name='IPAM'), + } + + with ( + patch( + 'core.management.commands.nbshell.apps.get_app_config', + side_effect=lambda label: app_configs[label], + ), + patch('builtins.print') as print_, + ): + command._lsmodels() + + printed = [call.args for call in print_.call_args_list] + self.assertIn(('DCIM:',), printed) + self.assertIn(('IPAM:',), printed) + self.assertIn((' dcim.Device',), printed) + self.assertIn((' ipam.IPAddress',), printed) + + def test_banner_includes_installed_plugins(self): + with ( + patch('core.management.commands.nbshell.platform.node', return_value='netbox'), + patch('core.management.commands.nbshell.platform.python_version', return_value='3.12.0'), + patch('core.management.commands.nbshell.get_version', return_value='5.2.0'), + patch('core.management.commands.nbshell.get_installed_plugins', return_value={'plugin': '1.2.3'}), + ): + banner = nbshell.Command.get_banner_text() + + self.assertIn('NetBox interactive shell', banner) + self.assertIn('Plugins:', banner) + self.assertIn('plugin', banner) + + +class RQWorkerTestCase(TestCase): + def test_defaults_to_all_queues_and_enables_scheduler(self): + with ( + patch('core.management.commands.rqworker.registry', {'system_jobs': {}}), + patch('core.management.commands.rqworker._Command.handle') as super_handle, + self.assertLogs('netbox.rqworker', level='WARNING') as logs, + ): + call_command('rqworker', stdout=StringIO(), stderr=StringIO()) + + super_handle.assert_called_once() + args, kwargs = super_handle.call_args + self.assertEqual(args, DEFAULT_QUEUES) + self.assertTrue(kwargs['with_scheduler']) + self.assertEqual(len(logs.output), 1) + self.assertIn('No queues have been specified', logs.output[0]) + + def test_schedules_registered_system_jobs(self): + job = MagicMock() + job.name = 'TestJob' + + with ( + patch('core.management.commands.rqworker.registry', {'system_jobs': {job: {'interval': 5}}}), + patch('core.management.commands.rqworker._Command.handle') as super_handle, + ): + call_command('rqworker', 'high', stdout=StringIO(), stderr=StringIO()) + + job.enqueue_once.assert_called_once_with(interval=5) + super_handle.assert_called_once() + args, kwargs = super_handle.call_args + self.assertEqual(args, ('high',)) + self.assertTrue(kwargs['with_scheduler']) + + def test_system_jobs_must_specify_interval(self): + job = MagicMock() + job.name = 'TestJob' + + with patch('core.management.commands.rqworker.registry', {'system_jobs': {job: {}}}): + with self.assertRaisesMessage(TypeError, 'System job must specify an interval'): + call_command('rqworker', stdout=StringIO(), stderr=StringIO()) + + +class SyncDataSourceTestCase(TestCase): + class FakeDataSource: + def __init__(self, name): + self.name = name + self.pk = name + self.sync = MagicMock() + + def __str__(self): + return self.name + + def get_status_display(self): + return 'completed' + + class FakeQuerySet(list): + def values(self, *fields): + return [{field: getattr(item, field) for field in fields} for item in self] + + def test_requires_name_or_all(self): + with self.assertRaisesMessage(CommandError, 'Must specify at least one data source'): + call_command('syncdatasource', stdout=StringIO()) + + def test_invalid_name(self): + with patch('core.management.commands.syncdatasource.DataSource') as data_source_model: + data_source_model.objects.filter.return_value = self.FakeQuerySet() + with self.assertRaisesMessage(CommandError, 'Invalid data source names: nonexistent-source'): + call_command('syncdatasource', 'nonexistent-source', stdout=StringIO()) + + data_source_model.objects.filter.assert_called_once() + self.assertEqual( + set(data_source_model.objects.filter.call_args.kwargs['name__in']), + {'nonexistent-source'}, + ) + + def test_all_syncs_datasource(self): + datasource = MagicMock() + datasource.__str__.return_value = 'Test Data Source' + datasource.get_status_display.return_value = 'completed' + + out = StringIO() + + with patch('core.management.commands.syncdatasource.DataSource') as data_source_model: + data_source_model.objects.all.return_value = [datasource] + call_command('syncdatasource', sync_all=True, stdout=out) + + data_source_model.objects.all.assert_called_once_with() + datasource.sync.assert_called_once_with() + self.assertIn('Syncing Test Data Source', out.getvalue()) + self.assertIn('completed', out.getvalue()) + + def test_named_datasource_syncs_matching_datasource(self): + datasource = self.FakeDataSource('source-a') + datasources = self.FakeQuerySet([datasource]) + out = StringIO() + + with patch('core.management.commands.syncdatasource.DataSource') as data_source_model: + data_source_model.objects.filter.return_value = datasources + call_command('syncdatasource', 'source-a', stdout=out) + + data_source_model.objects.filter.assert_called_once() + self.assertEqual( + set(data_source_model.objects.filter.call_args.kwargs['name__in']), + {'source-a'}, + ) + datasource.sync.assert_called_once_with() + self.assertIn('[1] Syncing source-a', out.getvalue()) + self.assertIn('completed', out.getvalue()) + self.assertNotIn('Syncing 1 data sources.', out.getvalue()) + self.assertNotIn('Finished.', out.getvalue()) + + def test_sync_failure_marks_datasource_failed_and_reraises(self): + datasource = MagicMock() + datasource.__str__.return_value = 'source-a' + datasource.pk = 1 + datasource.sync.side_effect = RuntimeError('boom') + + with patch('core.management.commands.syncdatasource.DataSource') as data_source_model: + data_source_model.objects.all.return_value = [datasource] + + with self.assertRaisesMessage(RuntimeError, 'boom'): + call_command('syncdatasource', sync_all=True, stdout=StringIO()) + + data_source_model.objects.filter.assert_called_once_with(pk=1) + data_source_model.objects.filter.return_value.update.assert_called_once_with( + status=DataSourceStatusChoices.FAILED, + ) + + def test_multiple_names_prints_summary_and_syncs_datasources(self): + datasource_a = self.FakeDataSource('source-a') + datasource_b = self.FakeDataSource('source-b') + datasources = self.FakeQuerySet([datasource_a, datasource_b]) + out = StringIO() + + with patch('core.management.commands.syncdatasource.DataSource') as data_source_model: + data_source_model.objects.filter.return_value = datasources + call_command('syncdatasource', 'source-a', 'source-b', stdout=out) + + data_source_model.objects.filter.assert_called_once() + self.assertEqual( + set(data_source_model.objects.filter.call_args.kwargs['name__in']), + {'source-a', 'source-b'}, + ) + datasource_a.sync.assert_called_once_with() + datasource_b.sync.assert_called_once_with() + self.assertIn('Syncing 2 data sources.', out.getvalue()) + self.assertIn('[1] Syncing source-a', out.getvalue()) + self.assertIn('[2] Syncing source-b', out.getvalue()) + self.assertIn('Finished.', out.getvalue()) diff --git a/netbox/core/tests/test_models.py b/netbox/core/tests/test_models.py index af342324f..0b6de44c5 100644 --- a/netbox/core/tests/test_models.py +++ b/netbox/core/tests/test_models.py @@ -150,7 +150,7 @@ class DataSourceChangeLoggingTestCase(TestCase): self.assertEqual(objectchange.postchange_data['parameters']['password'], CENSOR_TOKEN) -class ObjectTypeTest(TestCase): +class ObjectTypeTestCase(TestCase): def test_create(self): """ @@ -227,7 +227,7 @@ class ObjectTypeTest(TestCase): self.assertNotIn(ObjectType.objects.get_by_natural_key('dcim', 'cabletermination'), bookmarks_ots) -class JobTest(TestCase): +class JobTestCase(TestCase): def _make_job(self, user, notifications): """ diff --git a/netbox/core/tests/test_signals.py b/netbox/core/tests/test_signals.py new file mode 100644 index 000000000..d691e72e2 --- /dev/null +++ b/netbox/core/tests/test_signals.py @@ -0,0 +1,547 @@ +import uuid +from types import SimpleNamespace +from unittest.mock import MagicMock, Mock, patch + +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError +from django.core.signals import request_finished +from django.db import transaction +from django.test import RequestFactory, SimpleTestCase, TestCase, override_settings + +from core import signals +from core.choices import DataSourceStatusChoices, JobStatusChoices, ObjectChangeActionChoices +from core.models import ConfigRevision, DataSource, ObjectChange, ObjectType +from core.signals import _signals_received, clear_events, post_sync +from dcim.choices import InterfaceTypeChoices +from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site, SiteGroup +from extras.models import Tag +from extras.validators import CustomValidator +from netbox.context import events_queue +from netbox.context_managers import event_tracking +from users.models import User +from utilities.exceptions import AbortRequest + + +def _build_request(user): + request = RequestFactory().get('/') + request.id = uuid.uuid4() + request.user = user + return request + + +class UpdateObjectTypesSignalTestCase(TestCase): + """ + Verify core.signals.update_object_types has registered an ObjectType for known + models, with the expected public flag and feature set. + """ + + def test_public_model_object_type_is_registered(self): + ot = ObjectType.objects.get(app_label='dcim', model='site') + self.assertTrue(ot.public) + # Site supports several features — verify a couple representative ones. + self.assertIn('custom_fields', ot.features) + self.assertIn('tags', ot.features) + + def test_private_model_object_type_is_registered_as_non_public(self): + ot = ObjectType.objects.get(app_label='dcim', model='cablepath') + self.assertFalse(ot.public) + + +class HandleChangedObjectSignalTestCase(TestCase): + """ + Verify core.signals.handle_changed_object writes an ObjectChange and increments + metric counters whenever a tracked object is created or updated within a request. + """ + + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user(username='alice', password='pw') + + def test_create_records_an_objectchange(self): + request = _build_request(self.user) + with event_tracking(request): + site = Site.objects.create(name='Site 1', slug='site-1') + + oc = ObjectChange.objects.get( + changed_object_type=ContentType.objects.get_for_model(Site), + changed_object_id=site.pk, + ) + self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_CREATE) + self.assertEqual(oc.user, self.user) + self.assertEqual(oc.request_id, request.id) + + def test_update_records_an_objectchange(self): + site = Site.objects.create(name='Site 1', slug='site-1') + request = _build_request(self.user) + + with event_tracking(request): + site.description = 'updated' + site.save() + + ocs = ObjectChange.objects.filter( + changed_object_type=ContentType.objects.get_for_model(Site), + changed_object_id=site.pk, + ).order_by('-pk') + self.assertEqual(ocs.first().action, ObjectChangeActionChoices.ACTION_UPDATE) + + def test_no_request_skips_objectchange(self): + # Saving outside a request context (no event_tracking) should not record any + # ObjectChange entries. + Site.objects.create(name='Site 1', slug='site-1') + self.assertEqual(ObjectChange.objects.count(), 0) + + def test_m2m_tag_change_records_objectchange_with_postchange_tags(self): + site = Site.objects.create(name='Site 1', slug='site-1') + tag = Tag.objects.create(name='Important', slug='important') + request = _build_request(self.user) + + with event_tracking(request): + site.tags.add(tag) + + oc = ObjectChange.objects.filter( + changed_object_type=ContentType.objects.get_for_model(Site), + changed_object_id=site.pk, + ).first() + self.assertEqual(oc.postchange_data['tags'], ['Important']) + + +class HandleDeletedObjectSignalTestCase(TestCase): + """ + Verify core.signals.handle_deleted_object writes a delete-type ObjectChange and + respects PROTECTION_RULES. + """ + + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user(username='alice', password='pw') + + def setUp(self): + # Reset the in-memory pre_delete bookkeeping; the signal's de-dup set lives in a + # threading.local that is not rolled back between TestCase methods. + _signals_received.pre_delete = set() + + def test_delete_records_an_objectchange(self): + site = Site.objects.create(name='Site 1', slug='site-1') + site_pk = site.pk + site_type = ContentType.objects.get_for_model(Site) + request = _build_request(self.user) + + with event_tracking(request): + site.delete() + + oc = ObjectChange.objects.get(changed_object_type=site_type, changed_object_id=site_pk) + self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_DELETE) + self.assertIsNone(oc.postchange_data) + + @override_settings(PROTECTION_RULES={'dcim.site': [CustomValidator({'name': {'neq': 'protected'}})]}) + def test_protection_rule_violation_aborts_deletion(self): + site = Site.objects.create(name='protected', slug='protected') + request = _build_request(self.user) + + with event_tracking(request): + # The signal raises AbortRequest from a pre_delete handler, which can poison + # the surrounding transaction; isolate the delete in its own atomic block. + with self.assertRaises((AbortRequest, ValidationError)): + with transaction.atomic(): + site.delete() + + self.assertTrue(Site.objects.filter(pk=site.pk).exists()) + + def test_delete_records_single_objectchange(self): + # A delete should record exactly one ObjectChange for the deleted object. + site = Site.objects.create(name='Site 1', slug='site-1') + site_pk = site.pk + site_type = ContentType.objects.get_for_model(Site) + request = _build_request(self.user) + + with event_tracking(request): + site.delete() + + ocs = ObjectChange.objects.filter(changed_object_type=site_type, changed_object_id=site_pk) + self.assertEqual(ocs.count(), 1) + + def test_duplicate_pre_delete_for_same_instance_is_ignored(self): + # Exercise the dedup short-circuit by invoking the handler twice for the + # same instance, mirroring what happens when a parent and its child are + # deleted simultaneously and the same pre_delete fires more than once. + # Only the first invocation should record an ObjectChange. + site = Site.objects.create(name='Site 1', slug='site-1') + site_pk = site.pk + site_type = ContentType.objects.get_for_model(Site) + request = _build_request(self.user) + + with event_tracking(request): + signals.handle_deleted_object(sender=Site, instance=site) + signals.handle_deleted_object(sender=Site, instance=site) + + ocs = ObjectChange.objects.filter( + changed_object_type=site_type, + changed_object_id=site_pk, + action=ObjectChangeActionChoices.ACTION_DELETE, + ) + self.assertEqual(ocs.count(), 1) + + def test_delete_records_change_for_objects_with_nulled_fk(self): + # When a parent is deleted, related objects with on_delete=SET_NULL have + # their FK cleared by the signal *and* receive a change-log entry via + # snapshot()+save(). Without the signal, Django's SET_NULL would clear + # the FK silently with no ObjectChange. + group = SiteGroup.objects.create(name='Group', slug='group') + group_pk = group.pk + site = Site.objects.create(name='Site', slug='site', group=group) + site_type = ContentType.objects.get_for_model(Site) + request = _build_request(self.user) + + with event_tracking(request): + group.delete() + + site.refresh_from_db() + self.assertIsNone(site.group) + oc = ObjectChange.objects.get( + changed_object_type=site_type, + changed_object_id=site.pk, + action=ObjectChangeActionChoices.ACTION_UPDATE, + ) + self.assertEqual(oc.prechange_data['group'], group_pk) + self.assertIsNone(oc.postchange_data['group']) + + def test_cascade_delete_does_not_record_update_after_delete(self): + # Regression test for #22270. Deleting a device cascades to all of its interfaces. + # When the LAG interface's pre_delete fires, it clears the `lag` FK (SET_NULL) on its + # member interfaces and records a change. If a member is itself being deleted in the + # same cascade, that update must not be written *after* the member's delete record — + # an UPDATE-after-DELETE corrupts the changelog and breaks branch replay. + manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer') + device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type', slug='device-type') + role = DeviceRole.objects.create(name='Role', slug='role') + site = Site.objects.create(name='Site', slug='site') + device = Device.objects.create(name='Device', site=site, device_type=device_type, role=role) + # Members are created before the LAG so their PKs are lower; the cascade fires their + # pre_delete (and records their DELETE) before the LAG's pre_delete runs. + member1 = Interface.objects.create(device=device, name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED) + member2 = Interface.objects.create(device=device, name='eth1', type=InterfaceTypeChoices.TYPE_1GE_FIXED) + lag = Interface.objects.create(device=device, name='lag0', type=InterfaceTypeChoices.TYPE_LAG) + member1.lag = lag + member1.save() + member2.lag = lag + member2.save() + member_pks = (member1.pk, member2.pk) + interface_type = ContentType.objects.get_for_model(Interface) + + request = _build_request(self.user) + with event_tracking(request): + device.delete() + + for member_pk in member_pks: + actions = list( + ObjectChange.objects.filter( + request_id=request.id, + changed_object_type=interface_type, + changed_object_id=member_pk, + ).order_by('time', 'pk').values_list('action', flat=True) + ) + # In this ordering the member's pre_delete fires before the LAG's, so the LAG + # skips it: the member's only change record for the request is its own deletion. + # Pre-fix, a spurious UPDATE was appended *after* this DELETE — assert the exact + # sequence so any trailing (or leading) update fails the test. + self.assertEqual(actions, [ObjectChangeActionChoices.ACTION_DELETE]) + + +class ClearSignalHistorySignalTestCase(TestCase): + """ + Verify core.signals.clear_signal_history resets the pre_delete bookkeeping at the + end of every request. + """ + + def test_request_finished_clears_history(self): + _signals_received.pre_delete = {('a', 1), ('b', 2)} + + request_finished.send(sender=self.__class__) + + self.assertEqual(_signals_received.pre_delete, set()) + + +class ClearEventsQueueSignalTestCase(TestCase): + """ + Verify core.signals.clear_events_queue empties the in-flight events queue when the + clear_events signal fires (e.g. during a rolled-back bulk transaction). + """ + + def test_clear_events_signal_empties_the_queue(self): + events_queue.set({'event-1': object(), 'event-2': object()}) + + clear_events.send(sender='test-suite') + + self.assertEqual(events_queue.get(), {}) + + +class EnqueueSyncJobSignalTestCase(TestCase): + """ + Verify core.signals.enqueue_sync_job schedules a recurring sync job when a + DataSource is saved with a sync_interval, and removes any existing schedule + otherwise. + """ + + def test_saving_datasource_with_interval_enqueues_sync_job(self): + with patch('core.jobs.SyncDataSourceJob') as sync_job: + DataSource.objects.create( + name='DS 1', + type='local', + source_url='/tmp/ds1', + enabled=True, + sync_interval=60, + ) + + sync_job.enqueue_once.assert_called_once() + _, kwargs = sync_job.enqueue_once.call_args + self.assertEqual(kwargs.get('interval'), 60) + + def test_disabled_datasource_clears_scheduled_jobs(self): + class ScheduledJobQueryset: + def __init__(self, jobs): + self.jobs = jobs + self.defer = Mock(return_value=self) + self.filter = Mock(return_value=self) + + def __iter__(self): + return iter(self.jobs) + + with patch('core.jobs.SyncDataSourceJob') as sync_job: + ds = DataSource.objects.create( + name='DS 1', + type='local', + source_url='/tmp/ds1', + enabled=True, + sync_interval=60, + ) + sync_job.reset_mock() + + scheduled_job = Mock() + scheduled_jobs = ScheduledJobQueryset([scheduled_job]) + sync_job.get_jobs.return_value = scheduled_jobs + ds.enabled = False + ds.sync_interval = None + ds.save() + + sync_job.get_jobs.assert_called_once_with(ds) + scheduled_jobs.defer.assert_called_once_with('data') + scheduled_jobs.filter.assert_called_once_with(interval__isnull=False, status=JobStatusChoices.STATUS_SCHEDULED) + scheduled_job.delete.assert_called_once_with() + + def test_creating_disabled_datasource_does_not_enqueue(self): + with patch('core.jobs.SyncDataSourceJob') as sync_job: + DataSource.objects.create( + name='DS 1', + type='local', + source_url='/tmp/ds1', + enabled=False, + sync_interval=None, + ) + + sync_job.enqueue_once.assert_not_called() + sync_job.get_jobs.assert_not_called() + + +class AutoSyncSignalTestCase(TestCase): + """ + Verify core.signals.auto_sync re-syncs every AutoSyncRecord linked to the + DataSource when post_sync fires. + """ + + def test_post_sync_resyncs_dependent_records(self): + ds = DataSource.objects.create( + name='DS 1', + type='local', + source_url='/tmp/ds1', + status=DataSourceStatusChoices.COMPLETED, + ) + record_a = SimpleNamespace(object=SimpleNamespace(synced=False)) + record_a.object.sync = lambda save: setattr(record_a.object, 'synced', save) + record_b = SimpleNamespace(object=SimpleNamespace(synced=False)) + record_b.object.sync = lambda save: setattr(record_b.object, 'synced', save) + + with patch('core.models.AutoSyncRecord') as autosync_model: + autosync_model.objects.filter.return_value.prefetch_related.return_value = [ + record_a, + record_b, + ] + post_sync.send(sender=ds.__class__, instance=ds) + + self.assertTrue(record_a.object.synced) + self.assertTrue(record_b.object.synced) + + +class UpdateConfigSignalTestCase(TestCase): + """ + Verify core.signals.update_config invokes activate() on a newly-saved + ConfigRevision. + """ + + def test_saving_config_revision_activates_it(self): + with patch.object(ConfigRevision, 'activate') as activate: + ConfigRevision.objects.create(data={'foo': 1}, comment='test') + + activate.assert_called_once() + + +class HandleChangedObjectDirectHandlerTestCase(SimpleTestCase): + """ + Direct-call tests for handle_changed_object branches that are not naturally + reachable through ORM operations (a real .save() always produces changes, and + Django collapses every m2m_changed action into a single dispatch the handler + cannot fully simulate via real m2m operations). + """ + + def _instance(self): + objectchange = MagicMock() + objectchange.has_changes = True + objectchange.postchange_data = {'name': 'Device 1'} + instance = SimpleNamespace( + pk=123, + _meta=SimpleNamespace(model_name='device'), + refresh_from_db=MagicMock(), + ) + instance.to_objectchange = MagicMock(return_value=objectchange) + return instance, objectchange + + def test_unhandled_m2m_action_returns_without_recording(self): + request = SimpleNamespace(id='request-id', user='alice') + instance, _ = self._instance() + current_request = MagicMock() + current_request.get.return_value = request + + with patch.object(signals, 'current_request', current_request): + signals.handle_changed_object( + sender=None, + instance=instance, + action='pre_add', + pk_set={1}, + ) + + instance.to_objectchange.assert_not_called() + + def test_objectchange_without_changes_is_not_saved(self): + request = SimpleNamespace(id='request-id', user='alice') + instance, objectchange = self._instance() + objectchange.has_changes = False + current_request = MagicMock() + current_request.get.return_value = request + events_queue_mock = MagicMock() + events_queue_mock.get.return_value = {} + update_metric = MagicMock() + + with ( + patch.object(signals, 'current_request', current_request), + patch.object(signals, 'events_queue', events_queue_mock), + patch.object(signals, 'enqueue_event') as enqueue_event, + patch.object(signals.model_updates, 'labels', return_value=update_metric), + ): + signals.handle_changed_object(sender=None, instance=instance, created=False) + + instance.to_objectchange.assert_called_once_with(ObjectChangeActionChoices.ACTION_UPDATE) + # has_changes is False, so the ObjectChange should never be saved … + objectchange.save.assert_not_called() + # … but metric counters and event enqueueing still run. + update_metric.inc.assert_called_once_with() + enqueue_event.assert_called_once() + + def test_m2m_change_updates_existing_objectchange_in_same_request(self): + request = SimpleNamespace(id='request-id', user='alice') + instance, objectchange = self._instance() + previous_change = MagicMock() + current_request = MagicMock() + current_request.get.return_value = request + events_queue_mock = MagicMock() + events_queue_mock.get.return_value = {} + objectchange_model = MagicMock() + objectchange_model.objects.filter.return_value.first.return_value = previous_change + content_type_model = MagicMock() + content_type_model.objects.get_for_model.return_value = object() + + with ( + patch.object(signals, 'current_request', current_request), + patch.object(signals, 'events_queue', events_queue_mock), + patch.object(signals, 'ObjectChange', objectchange_model), + patch.object(signals, 'ContentType', content_type_model), + patch.object(signals, 'enqueue_event'), + patch.object(signals.model_updates, 'labels', return_value=MagicMock()), + ): + signals.handle_changed_object( + sender=None, + instance=instance, + action='post_add', + pk_set={1}, + ) + + # The handler should update the existing ObjectChange instead of creating a new one. + self.assertEqual(previous_change.postchange_data, objectchange.postchange_data) + previous_change.save.assert_called_once_with() + objectchange.save.assert_not_called() + instance.refresh_from_db.assert_called_once_with() + + +class HandleDeletedObjectDirectHandlerTestCase(SimpleTestCase): + """ + Direct-call tests for handle_deleted_object branches that are hard to construct + via real model operations (notably _netbox_private skip, which requires a + private-model instance with reverse relations the test can introspect). + """ + + def setUp(self): + _signals_received.pre_delete = set() + + def test_private_model_skips_reverse_relation_processing(self): + # Build a relation the handler would normally process — a ManyToOneRel-typed + # instance pointing at a ChangeLoggingMixin subclass. The handler narrows by + # exact type (`type(relation) is ManyToOneRel`), so do not use + # MagicMock(spec=ManyToOneRel): it would be skipped before reaching the + # _netbox_private branch. Patching ManyToOneRel to this fake class keeps the + # exact-type check meaningful, so related_model.objects.filter() would be + # called if the private-model skip failed. + class FakeManyToOneRel: + pass + + class FakeChangeLoggingMixin: + pass + + class FakeRelatedModel(FakeChangeLoggingMixin): + pass + + FakeRelatedModel.objects = MagicMock() + + fake_relation = FakeManyToOneRel() + fake_relation.related_model = FakeRelatedModel + fake_relation.remote_field = SimpleNamespace(name='parent') + fake_relation.null = True + fake_relation.on_delete = object() + + sender = SimpleNamespace(_meta=SimpleNamespace(app_label='dcim', model_name='cablepath')) + request = SimpleNamespace(id='request-id', user='alice') + instance = SimpleNamespace( + pk=1, + _meta=SimpleNamespace(model_name='cablepath', related_objects=[fake_relation]), + _netbox_private=True, + ) + # Private models typically don't have to_objectchange, so skip change-log too. + config = SimpleNamespace(PROTECTION_RULES={}) + current_request = MagicMock() + current_request.get.return_value = request + events_queue_mock = MagicMock() + events_queue_mock.get.return_value = {} + + with ( + patch.object(signals, 'ManyToOneRel', FakeManyToOneRel), + patch.object(signals, 'get_config', return_value=config), + patch.object(signals, 'get_config_value_ci', return_value=[]), + patch.object(signals, 'run_validators'), + patch.object(signals, 'current_request', current_request), + patch.object(signals, 'events_queue', events_queue_mock), + patch.object(signals, 'ContentType') as content_type_model, + patch.object(signals, 'ChangeLoggingMixin', FakeChangeLoggingMixin), + patch.object(signals, 'enqueue_event'), + patch.object(signals.model_deletes, 'labels', return_value=MagicMock()), + ): + content_type_model.objects.get_for_model.return_value = object() + signals.handle_deleted_object(sender=sender, instance=instance) + + FakeRelatedModel.objects.filter.assert_not_called() diff --git a/netbox/core/tests/test_tables.py b/netbox/core/tests/test_tables.py index ca28f1830..c938fd057 100644 --- a/netbox/core/tests/test_tables.py +++ b/netbox/core/tests/test_tables.py @@ -3,24 +3,24 @@ from core.tables import * from utilities.testing import TableTestCases -class DataSourceTableTest(TableTestCases.StandardTableTestCase): +class DataSourceTableTestCase(TableTestCases.StandardTableTestCase): table = DataSourceTable -class DataFileTableTest(TableTestCases.StandardTableTestCase): +class DataFileTableTestCase(TableTestCases.StandardTableTestCase): table = DataFileTable -class JobTableTest(TableTestCases.StandardTableTestCase): +class JobTableTestCase(TableTestCases.StandardTableTestCase): table = JobTable -class ObjectChangeTableTest(TableTestCases.StandardTableTestCase): +class ObjectChangeTableTestCase(TableTestCases.StandardTableTestCase): table = ObjectChangeTable queryset_sources = [ ('ObjectChangeListView', ObjectChange.objects.all()), ] -class ConfigRevisionTableTest(TableTestCases.StandardTableTestCase): +class ConfigRevisionTableTestCase(TableTestCases.StandardTableTestCase): table = ConfigRevisionTable diff --git a/netbox/core/tests/test_views.py b/netbox/core/tests/test_views.py index 27593dc9b..b763eaed2 100644 --- a/netbox/core/tests/test_views.py +++ b/netbox/core/tests/test_views.py @@ -3,6 +3,7 @@ import urllib.parse import uuid from datetime import datetime +from django.contrib.contenttypes.models import ContentType from django.urls import reverse from django.utils import timezone from django_rq import get_queue @@ -17,6 +18,7 @@ from core.models import * from dcim.models import Site from users.models import User from utilities.testing import TestCase, ViewTestCases, create_tags, disable_logging +from utilities.testing.mixins import RQQueueTestMixin class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase): @@ -104,6 +106,52 @@ class DataFileTestCase( DataFile.objects.bulk_create(data_files) +class JobTestCase( + ViewTestCases.GetObjectViewTestCase, + ViewTestCases.ListObjectsViewTestCase, + ViewTestCases.DeleteObjectViewTestCase, + ViewTestCases.BulkDeleteObjectsViewTestCase, +): + model = Job + + @classmethod + def setUpTestData(cls): + datasource = DataSource.objects.create( + name='Data Source 1', + type='local', + source_url='file:///var/tmp/source1/', + ) + ct = ContentType.objects.get_for_model(DataSource) + Job.objects.bulk_create( + [ + Job( + name='Job 1', + object_type=ct, + object_id=datasource.pk, + status='pending', + queue_name='default', + job_id=uuid.uuid4(), + ), + Job( + name='Job 2', + object_type=ct, + object_id=datasource.pk, + status='running', + queue_name='default', + job_id=uuid.uuid4(), + ), + Job( + name='Job 3', + object_type=ct, + object_id=datasource.pk, + status='completed', + queue_name='default', + job_id=uuid.uuid4(), + ), + ] + ) + + # TODO: Convert to StandardTestCases.Views class ObjectChangeTestCase(TestCase): user_permissions = ( @@ -141,7 +189,7 @@ class ObjectChangeTestCase(TestCase): self.assertHttpStatus(response, 200) -class BackgroundTaskTestCase(TestCase): +class BackgroundTaskTestCase(RQQueueTestMixin, TestCase): user_permissions = () # Dummy worker functions @@ -163,11 +211,6 @@ class BackgroundTaskTestCase(TestCase): self.user.is_active = True self.user.save() - # Clear all queues prior to running each test - get_queue('default').connection.flushall() - get_queue('high').connection.flushall() - get_queue('low').connection.flushall() - def test_background_queue_list(self): url = reverse('core:background_queue_list') diff --git a/netbox/core/views.py b/netbox/core/views.py index fe663cdaf..760346b3d 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -14,7 +14,7 @@ from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.views.generic import View -from django_rq.queues import get_connection, get_queue_by_index, get_redis_connection +from django_rq.queues import get_queue_by_index, get_redis_connection from django_rq.settings import get_queues_list, get_queues_map from django_rq.utils import get_statistics from rq.exceptions import NoSuchJobError @@ -55,6 +55,7 @@ from utilities.forms import ConfirmationForm from utilities.htmx import htmx_partial from utilities.json import ConfigJSONEncoder from utilities.query import count_related +from utilities.rqworker import get_all_workers from utilities.views import ( ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, @@ -707,7 +708,7 @@ class SystemView(UserPassesTestMixin, View): 'postgresql_version': psql_version, 'database_name': db_name, 'database_size': db_size, - 'rq_worker_count': Worker.count(get_connection('default')), + 'rq_worker_count': len(get_all_workers()), } def _get_object_counts(self): diff --git a/netbox/dcim/api/serializers_/devicetypes.py b/netbox/dcim/api/serializers_/devicetypes.py index 9df6dddd7..1b3087229 100644 --- a/netbox/dcim/api/serializers_/devicetypes.py +++ b/netbox/dcim/api/serializers_/devicetypes.py @@ -101,11 +101,24 @@ class ModuleTypeSerializer(PrimaryModelSerializer): ) module_count = serializers.IntegerField(read_only=True) + # Counter fields + console_port_template_count = serializers.IntegerField(read_only=True) + console_server_port_template_count = serializers.IntegerField(read_only=True) + power_port_template_count = serializers.IntegerField(read_only=True) + power_outlet_template_count = serializers.IntegerField(read_only=True) + interface_template_count = serializers.IntegerField(read_only=True) + front_port_template_count = serializers.IntegerField(read_only=True) + rear_port_template_count = serializers.IntegerField(read_only=True) + module_bay_template_count = serializers.IntegerField(read_only=True) + class Meta: model = ModuleType fields = [ 'id', 'url', 'display_url', 'display', 'profile', 'manufacturer', 'model', 'part_number', 'airflow', 'weight', 'weight_unit', 'description', 'attributes', 'owner', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', 'module_count', + 'created', 'last_updated', 'module_count', 'console_port_template_count', + 'console_server_port_template_count', 'power_port_template_count', 'power_outlet_template_count', + 'interface_template_count', 'front_port_template_count', 'rear_port_template_count', + 'module_bay_template_count', ] brief_fields = ('id', 'url', 'display', 'profile', 'manufacturer', 'model', 'description', 'module_count') diff --git a/netbox/dcim/api/serializers_/rackunits.py b/netbox/dcim/api/serializers_/rackunits.py index f2ffb9a56..d93484838 100644 --- a/netbox/dcim/api/serializers_/rackunits.py +++ b/netbox/dcim/api/serializers_/rackunits.py @@ -26,7 +26,12 @@ class RackUnitSerializer(serializers.Serializer): device = DeviceSerializer(nested=True, read_only=True) occupied = serializers.BooleanField(read_only=True) display = serializers.SerializerMethodField(read_only=True) + description = serializers.SerializerMethodField(read_only=True) @extend_schema_field(OpenApiTypes.STR) def get_display(self, obj): return obj['name'] + + @extend_schema_field(OpenApiTypes.STR) + def get_description(self, obj): + return f'{obj["device"]}' if obj['device'] else None diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 05a9751dc..9ebd2ad5b 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -862,6 +862,10 @@ class ModuleTypeFilterSet(AttributeFiltersMixin, PrimaryModelFilterSet): method='_pass_through_ports', label=_('Has pass-through ports'), ) + module_bays = django_filters.BooleanFilter( + method='_module_bays', + label=_('Has module bays'), + ) class Meta: model = ModuleType @@ -869,6 +873,14 @@ class ModuleTypeFilterSet(AttributeFiltersMixin, PrimaryModelFilterSet): 'id', 'model', 'part_number', 'airflow', 'weight', 'weight_unit', 'description', # Counters + 'console_port_template_count', + 'console_server_port_template_count', + 'power_port_template_count', + 'power_outlet_template_count', + 'interface_template_count', + 'front_port_template_count', + 'rear_port_template_count', + 'module_bay_template_count', 'module_count', ) @@ -904,6 +916,9 @@ class ModuleTypeFilterSet(AttributeFiltersMixin, PrimaryModelFilterSet): rearporttemplates__isnull=value ) + def _module_bays(self, queryset, name, value): + return queryset.exclude(modulebaytemplates__isnull=value) + class DeviceTypeComponentFilterSet(django_filters.FilterSet): q = django_filters.CharFilter( @@ -2806,12 +2821,77 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet): @register_filterset class CableTerminationFilterSet(ChangeLoggedModelFilterSet): + cable_id = django_filters.ModelMultipleChoiceFilter( + queryset=Cable.objects.all(), + distinct=False, + label=_('Cable (ID)'), + ) termination_type = MultiValueContentTypeFilter() + # Termination object filters + consoleport_id = MultiValueNumberFilter( + method='filter_by_consoleport' + ) + consoleserverport_id = MultiValueNumberFilter( + method='filter_by_consoleserverport' + ) + powerport_id = MultiValueNumberFilter( + method='filter_by_powerport' + ) + poweroutlet_id = MultiValueNumberFilter( + method='filter_by_poweroutlet' + ) + interface_id = MultiValueNumberFilter( + method='filter_by_interface' + ) + frontport_id = MultiValueNumberFilter( + method='filter_by_frontport' + ) + rearport_id = MultiValueNumberFilter( + method='filter_by_rearport' + ) + powerfeed_id = MultiValueNumberFilter( + method='filter_by_powerfeed' + ) + circuittermination_id = MultiValueNumberFilter( + method='filter_by_circuittermination' + ) + class Meta: model = CableTermination fields = ('id', 'cable', 'cable_end', 'termination_type', 'termination_id') + def filter_by_termination_object(self, queryset, model, value): + content_type = ContentType.objects.get_for_model(model) + return queryset.filter(termination_type=content_type, termination_id__in=value) + + def filter_by_consoleport(self, queryset, name, value): + return self.filter_by_termination_object(queryset, ConsolePort, value) + + def filter_by_consoleserverport(self, queryset, name, value): + return self.filter_by_termination_object(queryset, ConsoleServerPort, value) + + def filter_by_powerport(self, queryset, name, value): + return self.filter_by_termination_object(queryset, PowerPort, value) + + def filter_by_poweroutlet(self, queryset, name, value): + return self.filter_by_termination_object(queryset, PowerOutlet, value) + + def filter_by_interface(self, queryset, name, value): + return self.filter_by_termination_object(queryset, Interface, value) + + def filter_by_frontport(self, queryset, name, value): + return self.filter_by_termination_object(queryset, FrontPort, value) + + def filter_by_rearport(self, queryset, name, value): + return self.filter_by_termination_object(queryset, RearPort, value) + + def filter_by_powerfeed(self, queryset, name, value): + return self.filter_by_termination_object(queryset, PowerFeed, value) + + def filter_by_circuittermination(self, queryset, name, value): + return self.filter_by_termination_object(queryset, CircuitTermination, value) + @register_filterset class PowerPanelFilterSet(PrimaryModelFilterSet, ContactModelFilterSet): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 9759a5df4..0cb86a268 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -710,7 +710,7 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm): ), FieldSet( 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', - 'pass_through_ports', name=_('Components') + 'pass_through_ports', 'module_bays', name=_('Components') ), FieldSet('weight', 'weight_unit', name=_('Weight')), FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), @@ -778,6 +778,13 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + module_bays = forms.NullBooleanField( + required=False, + label=_('Has module bays'), + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) tag = TagFilterField(model) airflow = forms.MultipleChoiceField( label=_('Airflow'), diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 9672cc54b..7745deb60 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -690,7 +690,8 @@ class DeviceForm(TenancyForm, PrimaryModelForm): ) local_context_data = JSONField( required=False, - label='' + label='', + widget=forms.Textarea(attrs={'aria-label': _('Local config context data')}) ) virtual_chassis = DynamicModelChoiceField( label=_('Virtual chassis'), diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 6cbab28f2..50aa4cb22 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -4,6 +4,7 @@ import strawberry import strawberry_django from django.db.models import Func, IntegerField +from circuits.models import CircuitTermination from core.graphql.mixins import ChangelogMixin from dcim import models from extras.graphql.mixins import ConfigContextMixin, ContactsMixin, ImageAttachmentsMixin @@ -17,6 +18,8 @@ from netbox.graphql.types import ( PrimaryObjectType, ) from users.graphql.mixins import OwnerMixin +from utilities.querysets import RestrictedPrefetch +from virtualization.models import Cluster from .filters import * from .mixins import CabledObjectMixin, PathEndpointMixin @@ -288,11 +291,11 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, Prima inventoryitems: list[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]] vdcs: list[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]] - @strawberry_django.field + @strawberry_django.field(prefetch_related='vc_master_for') def vc_master_for(self) -> Annotated["VirtualChassisType", strawberry.lazy('dcim.graphql.types')] | None: return self.vc_master_for if hasattr(self, 'vc_master_for') else None - @strawberry_django.field + @strawberry_django.field(prefetch_related='parent_bay') def parent_bay(self) -> Annotated["DeviceBayType", strawberry.lazy('dcim.graphql.types')] | None: return self.parent_bay if hasattr(self, 'parent_bay') else None @@ -327,7 +330,7 @@ class InventoryItemTemplateType(ComponentTemplateType): role: Annotated['InventoryItemRoleType', strawberry.lazy('dcim.graphql.types')] | None manufacturer: Annotated['ManufacturerType', strawberry.lazy('dcim.graphql.types')] - @strawberry_django.field + @strawberry_django.field(prefetch_related='parent') def parent(self) -> Annotated['InventoryItemTemplateType', strawberry.lazy('dcim.graphql.types')] | None: return self.parent @@ -430,7 +433,7 @@ class FrontPortTemplateType(ModularComponentTemplateType): class MACAddressType(PrimaryObjectType): mac_address: str - @strawberry_django.field + @strawberry_django.field(prefetch_related='assigned_object') def assigned_object(self) -> Annotated[ Annotated['InterfaceType', strawberry.lazy('dcim.graphql.types')] | Annotated['VMInterfaceType', strawberry.lazy('virtualization.graphql.types')], @@ -494,7 +497,7 @@ class InventoryItemType(ComponentType): child_items: list[Annotated['InventoryItemType', strawberry.lazy('dcim.graphql.types')]] - @strawberry_django.field + @strawberry_django.field(prefetch_related='parent') def parent(self) -> Annotated['InventoryItemType', strawberry.lazy('dcim.graphql.types')] | None: return self.parent @@ -541,11 +544,20 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Nested devices: list[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]] children: list[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]] - @strawberry_django.field + @strawberry_django.field( + prefetch_related=lambda info: RestrictedPrefetch( + 'cluster_set', info.context.request.user, 'view', queryset=Cluster.objects.all() + ), + ) def clusters(self) -> list[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]: return self.cluster_set.all() - @strawberry_django.field + @strawberry_django.field( + prefetch_related=lambda info: RestrictedPrefetch( + 'circuit_terminations', info.context.request.user, 'view', + queryset=CircuitTermination.objects.all() + ), + ) def circuit_terminations(self) -> list[ Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')] ]: @@ -599,7 +611,7 @@ class ModuleBayType(ModularComponentType): installed_module: Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')] | None children: list[Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')]] - @strawberry_django.field + @strawberry_django.field(prefetch_related='parent') def parent(self) -> Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')] | None: return self.parent @@ -632,6 +644,14 @@ class ModuleTypeProfileType(PrimaryObjectType): ) class ModuleTypeType(PrimaryObjectType): module_count: BigInt + console_port_template_count: BigInt + console_server_port_template_count: BigInt + power_port_template_count: BigInt + power_outlet_template_count: BigInt + interface_template_count: BigInt + front_port_template_count: BigInt + rear_port_template_count: BigInt + module_bay_template_count: BigInt profile: Annotated["ModuleTypeProfileType", strawberry.lazy('dcim.graphql.types')] | None manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] @@ -864,15 +884,24 @@ class RegionType(VLANGroupsMixin, ContactsMixin, NestedGroupObjectType): sites: list[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]] children: list[Annotated["RegionType", strawberry.lazy('dcim.graphql.types')]] - @strawberry_django.field + @strawberry_django.field(prefetch_related='parent') def parent(self) -> Annotated["RegionType", strawberry.lazy('dcim.graphql.types')] | None: return self.parent - @strawberry_django.field + @strawberry_django.field( + prefetch_related=lambda info: RestrictedPrefetch( + 'cluster_set', info.context.request.user, 'view', queryset=Cluster.objects.all() + ), + ) def clusters(self) -> list[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]: return self.cluster_set.all() - @strawberry_django.field + @strawberry_django.field( + prefetch_related=lambda info: RestrictedPrefetch( + 'circuit_terminations', info.context.request.user, 'view', + queryset=CircuitTermination.objects.all() + ), + ) def circuit_terminations(self) -> list[ Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')] ]: @@ -899,15 +928,22 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, PrimaryObj devices: list[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]] locations: list[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]] asns: list[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]] - circuit_terminations: list[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]] - clusters: list[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]] vlans: list[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]] - @strawberry_django.field + @strawberry_django.field( + prefetch_related=lambda info: RestrictedPrefetch( + 'cluster_set', info.context.request.user, 'view', queryset=Cluster.objects.all() + ), + ) def clusters(self) -> list[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]: return self.cluster_set.all() - @strawberry_django.field + @strawberry_django.field( + prefetch_related=lambda info: RestrictedPrefetch( + 'circuit_terminations', info.context.request.user, 'view', + queryset=CircuitTermination.objects.all() + ), + ) def circuit_terminations(self) -> list[ Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')] ]: @@ -925,15 +961,24 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, NestedGroupObjectType): sites: list[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]] children: list[Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')]] - @strawberry_django.field + @strawberry_django.field(prefetch_related='parent') def parent(self) -> Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')] | None: return self.parent - @strawberry_django.field + @strawberry_django.field( + prefetch_related=lambda info: RestrictedPrefetch( + 'cluster_set', info.context.request.user, 'view', queryset=Cluster.objects.all() + ), + ) def clusters(self) -> list[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]: return self.cluster_set.all() - @strawberry_django.field + @strawberry_django.field( + prefetch_related=lambda info: RestrictedPrefetch( + 'circuit_terminations', info.context.request.user, 'view', + queryset=CircuitTermination.objects.all() + ), + ) def circuit_terminations(self) -> list[ Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')] ]: diff --git a/netbox/dcim/migrations/0234_cablepath_nodes_index.py b/netbox/dcim/migrations/0234_cablepath_nodes_index.py new file mode 100644 index 000000000..af4f506a5 --- /dev/null +++ b/netbox/dcim/migrations/0234_cablepath_nodes_index.py @@ -0,0 +1,15 @@ +import django.contrib.postgres.indexes +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0233_device_render_config_permission'), + ] + + operations = [ + migrations.AddIndex( + model_name='cablepath', + index=django.contrib.postgres.indexes.GinIndex(fields=['_nodes'], name='dcim_cablep__nodes_b23b96_gin'), + ), + ] diff --git a/netbox/dcim/migrations/0235_cabletermination_circuit_site_cache.py b/netbox/dcim/migrations/0235_cabletermination_circuit_site_cache.py new file mode 100644 index 000000000..8fbaba3c9 --- /dev/null +++ b/netbox/dcim/migrations/0235_cabletermination_circuit_site_cache.py @@ -0,0 +1,35 @@ +from django.db import migrations +from django.db.models import OuterRef, Subquery + + +def populate_circuit_termination_site_cache(apps, schema_editor): + """ + Populate the cached _site and _location fields on CableTermination records whose + termination is a CircuitTermination. Earlier versions failed to cache these values, + causing site/location filters on cables to omit such terminations. + """ + ContentType = apps.get_model('contenttypes', 'ContentType') + CableTermination = apps.get_model('dcim', 'CableTermination') + CircuitTermination = apps.get_model('circuits', 'CircuitTermination') + + try: + ct = ContentType.objects.get_by_natural_key('circuits', 'circuittermination') + except ContentType.DoesNotExist: + return + + circuit_terminations = CircuitTermination.objects.filter(pk=OuterRef('termination_id')) + CableTermination.objects.filter(termination_type=ct).update( + _site=Subquery(circuit_terminations.values('_site_id')[:1]), + _location=Subquery(circuit_terminations.values('_location_id')[:1]), + ) + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0234_cablepath_nodes_index'), + ('circuits', '0057_default_ordering_indexes'), + ] + + operations = [ + migrations.RunPython(populate_circuit_termination_site_cache, migrations.RunPython.noop), + ] diff --git a/netbox/dcim/migrations/0236_moduletype_component_counts.py b/netbox/dcim/migrations/0236_moduletype_component_counts.py new file mode 100644 index 000000000..d775fa1f1 --- /dev/null +++ b/netbox/dcim/migrations/0236_moduletype_component_counts.py @@ -0,0 +1,99 @@ +from django.db import migrations +from django.db.models import Count, OuterRef, Subquery + +import utilities.fields + + +def populate_module_type_component_counts(apps, schema_editor): + """ + Populate the component template counter fields for existing ModuleTypes. + """ + ModuleType = apps.get_model('dcim', 'ModuleType') + db_alias = schema_editor.connection.alias + + counters = { + 'console_port_template_count': 'consoleporttemplates', + 'console_server_port_template_count': 'consoleserverporttemplates', + 'power_port_template_count': 'powerporttemplates', + 'power_outlet_template_count': 'poweroutlettemplates', + 'interface_template_count': 'interfacetemplates', + 'front_port_template_count': 'frontporttemplates', + 'rear_port_template_count': 'rearporttemplates', + 'module_bay_template_count': 'modulebaytemplates', + } + + for field_name, related_name in counters.items(): + count_subquery = ( + ModuleType.objects.using(db_alias) + .filter(pk=OuterRef('pk')) + .annotate(_count=Count(related_name)) + .values('_count') + ) + ModuleType.objects.using(db_alias).update(**{field_name: Subquery(count_subquery)}) + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0235_cabletermination_circuit_site_cache'), + ] + + operations = [ + migrations.AddField( + model_name='moduletype', + name='console_port_template_count', + field=utilities.fields.CounterCacheField( + default=0, editable=False, to_field='module_type', to_model='dcim.ConsolePortTemplate' + ), + ), + migrations.AddField( + model_name='moduletype', + name='console_server_port_template_count', + field=utilities.fields.CounterCacheField( + default=0, editable=False, to_field='module_type', to_model='dcim.ConsoleServerPortTemplate' + ), + ), + migrations.AddField( + model_name='moduletype', + name='front_port_template_count', + field=utilities.fields.CounterCacheField( + default=0, editable=False, to_field='module_type', to_model='dcim.FrontPortTemplate' + ), + ), + migrations.AddField( + model_name='moduletype', + name='interface_template_count', + field=utilities.fields.CounterCacheField( + default=0, editable=False, to_field='module_type', to_model='dcim.InterfaceTemplate' + ), + ), + migrations.AddField( + model_name='moduletype', + name='module_bay_template_count', + field=utilities.fields.CounterCacheField( + default=0, editable=False, to_field='module_type', to_model='dcim.ModuleBayTemplate' + ), + ), + migrations.AddField( + model_name='moduletype', + name='power_outlet_template_count', + field=utilities.fields.CounterCacheField( + default=0, editable=False, to_field='module_type', to_model='dcim.PowerOutletTemplate' + ), + ), + migrations.AddField( + model_name='moduletype', + name='power_port_template_count', + field=utilities.fields.CounterCacheField( + default=0, editable=False, to_field='module_type', to_model='dcim.PowerPortTemplate' + ), + ), + migrations.AddField( + model_name='moduletype', + name='rear_port_template_count', + field=utilities.fields.CounterCacheField( + default=0, editable=False, to_field='module_type', to_model='dcim.RearPortTemplate' + ), + ), + migrations.RunPython(populate_module_type_component_counts, migrations.RunPython.noop), + ] diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 06b5606bc..218300a53 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -1,10 +1,12 @@ import itertools import logging +import threading from collections import Counter from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields import ArrayField +from django.contrib.postgres.indexes import GinIndex from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models @@ -75,6 +77,11 @@ class Cable(PrimaryModel): """ A physical connection between two endpoints. """ + # Per-thread tracking of Cable PKs currently in delete(); referenced by + # dcim.signals.nullify_connected_endpoints to skip per-CableTermination + # cable path retracing during cascade (retrace_cable_paths handles it once). + _deletion_tracking = threading.local() + type = models.CharField( verbose_name=_('type'), max_length=50, @@ -342,6 +349,26 @@ class Cable(PrimaryModel): except UnsupportedCablePath as e: raise AbortRequest(e) + def delete(self, *args, **kwargs): + # Track this Cable as being deleted so the post_delete signal handler + # for cascaded CableTerminations can skip redundant path retracing; + # retrace_cable_paths() will retrace each affected path once after the + # Cable itself is deleted. Cache the PK locally because super().delete() + # clears self.pk before the finally block runs. The tracking set lives + # on a threading.local() to isolate concurrent deletions across threads. + if not hasattr(Cable._deletion_tracking, 'pks'): + Cable._deletion_tracking.pks = set() + pk = self.pk + Cable._deletion_tracking.pks.add(pk) + try: + return super().delete(*args, **kwargs) + finally: + Cable._deletion_tracking.pks.discard(pk) + + @classmethod + def _is_being_deleted(cls, pk): + return pk in getattr(cls._deletion_tracking, 'pks', ()) + def clone(self): """ Return attributes suitable for cloning this cable. @@ -663,9 +690,10 @@ class CableTermination(ChangeLoggedModel): self._location = self.termination.rack.location self._site = self.termination.rack.site - # Circuit terminations - elif getattr(self.termination, 'site', None): - self._site = self.termination.site + # Circuit terminations (which cache their own site/location) + elif self.termination._meta.label_lower == 'circuits.circuittermination': + self._site = self.termination._site + self._location = self.termination._location cache_related_objects.alters_data = True def to_objectchange(self, action): @@ -730,6 +758,11 @@ class CablePath(models.Model): _netbox_private = True class Meta: + indexes = ( + # GIN index supports @> operator used by `_nodes__contains` lookups, + # which fire on every cable/termination delete and path retrace. + GinIndex(fields=('_nodes',)), + ) verbose_name = _('cable path') verbose_name_plural = _('cable paths') @@ -901,7 +934,17 @@ class CablePath(models.Model): # Profile-based tracing if links[0].profile: cable_profile = links[0].profile_class() - positions = position_stack.pop() if position_stack else [None] + if position_stack: + positions = position_stack.pop() + else: + # When the position stack is empty (e.g. the trace reached this + # profiled cable after crossing single-position pass-through ports + # which don't push onto the stack), derive positions from each + # termination's own cable_positions — which were set by this + # profiled cable when it was saved. + positions = [ + pos for term in terminations for pos in (term.cable_positions or []) + ] remote_terminations = [] new_positions = [] @@ -918,9 +961,8 @@ class CablePath(models.Model): remaining[cp] -= 1 # Fallback for when positions don't match cable_positions - # (e.g., empty position stack yielding [None]) if not term_position_pairs: - term_position_pairs = [(terminations[0], pos) for pos in positions] + term_position_pairs = [(terminations[0], pos) for pos in positions or [None]] peer_results = cable_profile.get_peer_terminations(term_position_pairs) seen = set() diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index e3691fd6c..cf80b94c4 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -1313,6 +1313,29 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin): # Bays # +class ModuleBayManager(TreeManager): + """ + Order ModuleBays by the natural-sort name of each tree's root, then by MPTT + left value within the tree. Combined with the root-insert bypass in + ModuleBay.save(), this lets us keep MPTTMeta.order_insertion_by for cheap + intra-tree sibling placement while skipping the global tree_id renumbering + it would otherwise perform on every root insert. + """ + def get_queryset(self, *args, **kwargs): + # Use the raw manager to avoid recursing through this get_queryset() when + # building the annotation subquery. + root_name = self.model._objects_raw.filter( + tree_id=models.OuterRef('tree_id'), + level=0, + ).values('name')[:1] + return super().get_queryset(*args, **kwargs).annotate( + _root_name=models.Subquery( + root_name, + output_field=models.CharField(db_collation='natural_sort'), + ) + ).order_by('_root_name', 'lft') + + class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel): """ An empty space within a Device which can house a child device @@ -1337,7 +1360,10 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel): default=True, ) - objects = TreeManager() + objects = ModuleBayManager() + # Plain TreeManager used by ModuleBayManager to build the _root_name subquery + # without recursing through our annotated get_queryset(). + _objects_raw = TreeManager() clone_fields = ('device', 'enabled') @@ -1355,6 +1381,9 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel): verbose_name_plural = _('module bays') class MPTTMeta: + # Used for placing siblings within a single tree at insert time. Costs + # are bounded to that tree's rows. Cross-tree (root) insertion is + # handled in save() to avoid the O(N) tree_id shift this would trigger. order_insertion_by = ('name',) def clean(self): @@ -1376,6 +1405,36 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel): self.parent = self.module.module_bay else: self.parent = None + opts = self._mptt_meta + # For new root nodes, allocate the next tree_id and pre-set MPTT fields + # so super().save() skips MPTT's order_insertion_by-driven insertion + # path. That path would otherwise UPDATE every later tree_id on each + # root insert (NB-2800). Children still go through MPTT, which keeps + # siblings in name order via the same order_insertion_by setting. + if self._state.adding and self.parent_id is None and not self.lft and not self.rght: + max_tree_id = ModuleBay._objects_raw.aggregate( + models.Max('tree_id') + )['tree_id__max'] or 0 + self.tree_id = max_tree_id + 1 + self.lft = 1 + self.rght = 2 + self.level = 0 + elif ( + not self._state.adding + and self.parent_id is None + and self._mptt_cached_fields.get(opts.parent_attr) is None + ): + # Existing root being updated. Spoof the cached order_insertion_by + # values so MPTT's same_order check passes and it skips its reorder + # path, which would UPDATE every later tree_id on each root rename. + # ModuleBayManager._root_name recovers display order at query time, + # so the tree_id reshuffling would be wasted work. Child renames + # and root<->child transitions intentionally fall through to MPTT. + for field_name in opts.order_insertion_by: + field_name = field_name.lstrip('-') + self._mptt_cached_fields[field_name] = opts.get_raw_field_value( + self, field_name + ) super().save(*args, **kwargs) @property diff --git a/netbox/dcim/models/modules.py b/netbox/dcim/models/modules.py index 1d678deb0..857945d6c 100644 --- a/netbox/dcim/models/modules.py +++ b/netbox/dcim/models/modules.py @@ -58,8 +58,8 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): """ A ModuleType represents a hardware element that can be installed within a device and which houses additional components; for example, a line card within a chassis-based switch such as the Cisco Catalyst 6500. Like a - DeviceType, each ModuleType can have console, power, interface, and pass-through port templates assigned to it. It - cannot, however house device bays or module bays. + DeviceType, each ModuleType can have console, power, interface, pass-through port, and module bay templates assigned + to it. It cannot, however, house device bays. """ profile = models.ForeignKey( to='dcim.ModuleTypeProfile', @@ -100,6 +100,40 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): to_field='module_type' ) + # Counter fields + console_port_template_count = CounterCacheField( + to_model='dcim.ConsolePortTemplate', + to_field='module_type' + ) + console_server_port_template_count = CounterCacheField( + to_model='dcim.ConsoleServerPortTemplate', + to_field='module_type' + ) + power_port_template_count = CounterCacheField( + to_model='dcim.PowerPortTemplate', + to_field='module_type' + ) + power_outlet_template_count = CounterCacheField( + to_model='dcim.PowerOutletTemplate', + to_field='module_type' + ) + interface_template_count = CounterCacheField( + to_model='dcim.InterfaceTemplate', + to_field='module_type' + ) + front_port_template_count = CounterCacheField( + to_model='dcim.FrontPortTemplate', + to_field='module_type' + ) + rear_port_template_count = CounterCacheField( + to_model='dcim.RearPortTemplate', + to_field='module_type' + ) + module_bay_template_count = CounterCacheField( + to_model='dcim.ModuleBayTemplate', + to_field='module_type' + ) + clone_fields = ('profile', 'manufacturer', 'weight', 'weight_unit', 'airflow') prerequisite_models = ( 'dcim.Manufacturer', diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index dd37817d1..8d5baf6d1 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -185,6 +185,12 @@ def nullify_connected_endpoints(instance, **kwargs): model = instance.termination_type.model_class() model.objects.filter(pk=instance.termination_id).update(cable=None, cable_end='') + # If the parent Cable is being deleted in this same operation, skip the + # per-termination retrace; retrace_cable_paths() will retrace each affected + # path once after the Cable is deleted. + if Cable._is_being_deleted(instance.cable_id): + return + for cablepath in CablePath.objects.filter(_nodes__contains=instance.cable): # Remove the deleted CableTermination if it's one of the path's originating nodes if instance.termination in cablepath.origins: diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index e824f2683..fc177981f 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -789,8 +789,6 @@ class DeviceInterfaceTable(InterfaceTable): 'data-virtual': lambda record: "true" if record.is_virtual else "false", 'data-mark-connected': lambda record: "true" if record.mark_connected else "false", 'data-cable-status': lambda record: record.cable.status if record.cable else "", - 'data-type': lambda record: record.type, - 'data-connected': lambda record: "connected" if record.mark_connected or record.cable else "disconnected" } diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 2b786aec5..c8baf2942 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -49,7 +49,7 @@ CABLE_LENGTH = """ WEIGHT = """ {% load helpers %} -{% if value %}{{ value|floatformat:"-2" }} {{ record.weight_unit }}{% endif %} +{% display_weight record.weight record.weight_unit record.abs_weight %} """ DEVICE_LINK = """ diff --git a/netbox/dcim/tests/query_counts.json b/netbox/dcim/tests/query_counts.json new file mode 100644 index 000000000..1d7cdda72 --- /dev/null +++ b/netbox/dcim/tests/query_counts.json @@ -0,0 +1,83 @@ +{ + "cable:api_list_objects": 24, + "cable:list_objects_with_permission": 24, + "cablebundle:api_list_objects": 13, + "cablebundle:list_objects_with_permission": 20, + "cabletermination:api_list_objects": 16, + "consoleport:api_list_objects": 14, + "consoleport:list_objects_with_permission": 21, + "consoleporttemplate:api_list_objects": 11, + "consoleserverport:api_list_objects": 14, + "consoleserverport:list_objects_with_permission": 21, + "consoleserverporttemplate:api_list_objects": 11, + "device:api_list_objects": 20, + "device:list_objects_with_permission": 25, + "devicebay:api_list_objects": 14, + "devicebay:list_objects_with_permission": 21, + "devicebaytemplate:api_list_objects": 11, + "devicerole:api_list_objects": 13, + "devicerole:list_objects_with_permission": 20, + "devicetype:api_list_objects": 14, + "devicetype:list_objects_with_permission": 21, + "frontport:api_list_objects": 15, + "frontport:list_objects_with_permission": 25, + "frontporttemplate:api_list_objects": 12, + "interface:api_list_objects": 23, + "interface:list_objects_with_permission": 21, + "interfacetemplate:api_list_objects": 11, + "inventoryitem:api_list_objects": 20, + "inventoryitem:list_objects_with_permission": 23, + "inventoryitemrole:api_list_objects": 13, + "inventoryitemrole:list_objects_with_permission": 20, + "inventoryitemtemplate:api_list_objects": 13, + "location:api_list_objects": 15, + "location:list_objects_with_permission": 22, + "macaddress:api_list_objects": 17, + "macaddress:list_objects_with_permission": 24, + "manufacturer:api_list_objects": 13, + "manufacturer:list_objects_with_permission": 20, + "module:api_list_objects": 18, + "module:list_objects_with_permission": 24, + "modulebay:api_list_objects": 15, + "modulebay:list_objects_with_permission": 21, + "modulebaytemplate:api_list_objects": 11, + "moduletype:api_list_objects": 14, + "moduletype:list_objects_with_permission": 21, + "moduletypeprofile:api_list_objects": 13, + "moduletypeprofile:list_objects_with_permission": 20, + "platform:api_list_objects": 13, + "platform:list_objects_with_permission": 21, + "powerfeed:api_list_objects": 15, + "powerfeed:list_objects_with_permission": 22, + "poweroutlet:api_list_objects": 14, + "poweroutlet:list_objects_with_permission": 22, + "poweroutlettemplate:api_list_objects": 11, + "powerpanel:api_list_objects": 15, + "powerpanel:list_objects_with_permission": 22, + "powerport:api_list_objects": 14, + "powerport:list_objects_with_permission": 21, + "powerporttemplate:api_list_objects": 11, + "rack:api_list_objects": 17, + "rack:list_objects_with_permission": 29, + "rackgroup:api_list_objects": 13, + "rackgroup:list_objects_with_permission": 20, + "rackreservation:api_list_objects": 15, + "rackreservation:list_objects_with_permission": 23, + "rackrole:api_list_objects": 13, + "rackrole:list_objects_with_permission": 20, + "racktype:api_list_objects": 14, + "racktype:list_objects_with_permission": 21, + "rearport:api_list_objects": 15, + "rearport:list_objects_with_permission": 22, + "rearporttemplate:api_list_objects": 12, + "region:api_list_objects": 13, + "region:list_objects_with_permission": 20, + "site:api_list_objects": 16, + "site:list_objects_with_permission": 22, + "sitegroup:api_list_objects": 13, + "sitegroup:list_objects_with_permission": 20, + "virtualchassis:api_list_objects": 16, + "virtualchassis:list_objects_with_permission": 21, + "virtualdevicecontext:api_list_objects": 14, + "virtualdevicecontext:list_objects_with_permission": 20 +} diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 16edb205d..63f968705 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1,7 +1,7 @@ import json from django.conf import settings -from django.test import override_settings, tag +from django.test import tag from django.urls import reverse from django.utils.translation import gettext as _ from rest_framework import status @@ -29,7 +29,7 @@ from wireless.choices import WirelessChannelChoices from wireless.models import WirelessLAN -class AppTest(APITestCase): +class AppTestCase(APITestCase): def test_root(self): @@ -76,7 +76,7 @@ class Mixins: self.assertEqual(segment1[2][0]['name'], peer_obj.name) -class RegionTest(APIViewTestCases.APIViewTestCase): +class RegionTestCase(APIViewTestCases.APIViewTestCase): model = Region brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'site_count', 'slug', 'url'] create_data = [ @@ -107,7 +107,7 @@ class RegionTest(APIViewTestCases.APIViewTestCase): Region.objects.create(name='Region 3', slug='region-3') -class SiteGroupTest(APIViewTestCases.APIViewTestCase): +class SiteGroupTestCase(APIViewTestCases.APIViewTestCase): model = SiteGroup brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'site_count', 'slug', 'url'] create_data = [ @@ -140,7 +140,7 @@ class SiteGroupTest(APIViewTestCases.APIViewTestCase): SiteGroup.objects.create(name='Site Group 3', slug='site-group-3', comments='Hi!') -class SiteTest(APIViewTestCases.APIViewTestCase): +class SiteTestCase(APIViewTestCases.APIViewTestCase): model = Site brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url'] bulk_update_data = { @@ -420,7 +420,7 @@ class SiteTest(APIViewTestCases.APIViewTestCase): self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) -class LocationTest(APIViewTestCases.APIViewTestCase): +class LocationTestCase(APIViewTestCases.APIViewTestCase): model = Location brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'rack_count', 'slug', 'url'] bulk_update_data = { @@ -504,7 +504,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase): ] -class RackGroupTest(APIViewTestCases.APIViewTestCase): +class RackGroupTestCase(APIViewTestCases.APIViewTestCase): model = RackGroup brief_fields = ['description', 'display', 'id', 'name', 'rack_count', 'slug', 'url'] create_data = [ @@ -536,7 +536,7 @@ class RackGroupTest(APIViewTestCases.APIViewTestCase): RackGroup.objects.bulk_create(rack_groups) -class RackRoleTest(APIViewTestCases.APIViewTestCase): +class RackRoleTestCase(APIViewTestCases.APIViewTestCase): model = RackRole brief_fields = ['description', 'display', 'id', 'name', 'rack_count', 'slug', 'url'] create_data = [ @@ -571,7 +571,7 @@ class RackRoleTest(APIViewTestCases.APIViewTestCase): RackRole.objects.bulk_create(rack_roles) -class RackTypeTest(APIViewTestCases.APIViewTestCase): +class RackTypeTestCase(APIViewTestCases.APIViewTestCase): model = RackType brief_fields = ['description', 'display', 'id', 'manufacturer', 'model', 'rack_count', 'slug', 'url'] bulk_update_data = { @@ -631,7 +631,7 @@ class RackTypeTest(APIViewTestCases.APIViewTestCase): ] -class RackTest(APIViewTestCases.APIViewTestCase): +class RackTestCase(APIViewTestCases.APIViewTestCase): model = Rack brief_fields = ['description', 'device_count', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -716,6 +716,34 @@ class RackTest(APIViewTestCases.APIViewTestCase): response = self.client.get(f'{url}?q=U10', **self.header) self.assertEqual(response.data['count'], 2) + def test_get_rack_elevation_description_is_occupying_device_name(self): + """ + Verify occupied rack units include the occupying device in their description. + """ + rack = Rack.objects.first() + self.add_permissions('dcim.view_rack', 'dcim.view_device') + url = reverse('dcim-api:rack-elevation', kwargs={'pk': rack.pk}) + + device = create_test_device( + name='Device A', + site=rack.site, + rack=rack, + position=40, + face=DeviceFaceChoices.FACE_FRONT, + ) + + # Retrieve all units + response = self.client.get(url, **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + + occupied_unit = next(unit for unit in response.data['results'] if unit['name'] == 'U40') + self.assertEqual(occupied_unit['device']['id'], device.pk) + self.assertEqual(occupied_unit['description'], f'{device}') + + unoccupied_unit = next(unit for unit in response.data['results'] if unit['name'] == 'U39') + self.assertEqual(unoccupied_unit['device'], None) + self.assertEqual(unoccupied_unit['description'], None) + def test_get_rack_elevation_svg(self): """ GET a single rack elevation in SVG format. @@ -729,7 +757,7 @@ class RackTest(APIViewTestCases.APIViewTestCase): self.assertEqual(response.get('Content-Type'), 'image/svg+xml') -class RackReservationTest(APIViewTestCases.APIViewTestCase): +class RackReservationTestCase(APIViewTestCases.APIViewTestCase): model = RackReservation brief_fields = ['description', 'display', 'id', 'status', 'units', 'url', 'user'] bulk_update_data = { @@ -804,7 +832,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase): self.assertEqual(result['unit_count'], len(result['units'])) -class ManufacturerTest(APIViewTestCases.APIViewTestCase): +class ManufacturerTestCase(APIViewTestCases.APIViewTestCase): model = Manufacturer brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url'] create_data = [ @@ -836,7 +864,7 @@ class ManufacturerTest(APIViewTestCases.APIViewTestCase): Manufacturer.objects.bulk_create(manufacturers) -class DeviceTypeTest(APIViewTestCases.APIViewTestCase): +class DeviceTypeTestCase(APIViewTestCases.APIViewTestCase): model = DeviceType brief_fields = ['description', 'device_count', 'display', 'id', 'manufacturer', 'model', 'slug', 'url'] bulk_update_data = { @@ -882,7 +910,7 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase): ] -class ModuleTypeTest(APIViewTestCases.APIViewTestCase): +class ModuleTypeTestCase(APIViewTestCases.APIViewTestCase): model = ModuleType brief_fields = ['description', 'display', 'id', 'manufacturer', 'model', 'module_count', 'profile', 'url'] bulk_update_data = { @@ -922,7 +950,7 @@ class ModuleTypeTest(APIViewTestCases.APIViewTestCase): ] -class ModuleTypeProfileTest(APIViewTestCases.APIViewTestCase): +class ModuleTypeProfileTestCase(APIViewTestCases.APIViewTestCase): model = ModuleTypeProfile brief_fields = ['description', 'display', 'id', 'name', 'url'] SCHEMAS = [ @@ -986,7 +1014,7 @@ class ModuleTypeProfileTest(APIViewTestCases.APIViewTestCase): ModuleTypeProfile.objects.bulk_create(module_type_profiles) -class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase): +class ConsolePortTemplateTestCase(APIViewTestCases.APIViewTestCase): model = ConsolePortTemplate brief_fields = ['description', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -1030,7 +1058,7 @@ class ConsolePortTemplateTest(APIViewTestCases.APIViewTestCase): ] -class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase): +class ConsoleServerPortTemplateTestCase(APIViewTestCases.APIViewTestCase): model = ConsoleServerPortTemplate brief_fields = ['description', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -1074,7 +1102,7 @@ class ConsoleServerPortTemplateTest(APIViewTestCases.APIViewTestCase): ] -class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase): +class PowerPortTemplateTestCase(APIViewTestCases.APIViewTestCase): model = PowerPortTemplate brief_fields = ['description', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -1118,7 +1146,7 @@ class PowerPortTemplateTest(APIViewTestCases.APIViewTestCase): ] -class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase): +class PowerOutletTemplateTestCase(APIViewTestCases.APIViewTestCase): model = PowerOutletTemplate brief_fields = ['description', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -1176,7 +1204,7 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase): ] -class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase): +class InterfaceTemplateTestCase(APIViewTestCases.APIViewTestCase): model = InterfaceTemplate brief_fields = ['description', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -1224,7 +1252,7 @@ class InterfaceTemplateTest(APIViewTestCases.APIViewTestCase): ] -class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase): +class FrontPortTemplateTestCase(APIViewTestCases.APIViewTestCase): model = FrontPortTemplate brief_fields = ['description', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -1341,7 +1369,7 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase): ) -class RearPortTemplateTest(APIViewTestCases.APIViewTestCase): +class RearPortTemplateTestCase(APIViewTestCases.APIViewTestCase): model = RearPortTemplate brief_fields = ['description', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -1457,7 +1485,7 @@ class RearPortTemplateTest(APIViewTestCases.APIViewTestCase): ) -class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase): +class ModuleBayTemplateTestCase(APIViewTestCases.APIViewTestCase): model = ModuleBayTemplate brief_fields = ['description', 'display', 'enabled', 'id', 'name', 'url'] bulk_update_data = { @@ -1499,7 +1527,7 @@ class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase): ] -class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase): +class DeviceBayTemplateTestCase(APIViewTestCases.APIViewTestCase): model = DeviceBayTemplate brief_fields = ['description', 'display', 'enabled', 'id', 'name', 'url'] bulk_update_data = { @@ -1541,7 +1569,7 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase): ] -class InventoryItemTemplateTest(APIViewTestCases.APIViewTestCase): +class InventoryItemTemplateTestCase(APIViewTestCases.APIViewTestCase): model = InventoryItemTemplate brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -1601,7 +1629,7 @@ class InventoryItemTemplateTest(APIViewTestCases.APIViewTestCase): ] -class DeviceRoleTest(APIViewTestCases.APIViewTestCase): +class DeviceRoleTestCase(APIViewTestCases.APIViewTestCase): model = DeviceRole brief_fields = [ '_depth', 'description', 'device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count' @@ -1635,7 +1663,7 @@ class DeviceRoleTest(APIViewTestCases.APIViewTestCase): DeviceRole.objects.create(name='Device Role 3', slug='device-role-3', color='0000ff') -class PlatformTest(APIViewTestCases.APIViewTestCase): +class PlatformTestCase(APIViewTestCases.APIViewTestCase): model = Platform brief_fields = [ '_depth', 'description', 'device_count', 'display', 'id', 'name', 'slug', 'url', 'virtualmachine_count', @@ -1670,7 +1698,7 @@ class PlatformTest(APIViewTestCases.APIViewTestCase): platform.save() -class DeviceTest(APIViewTestCases.APIViewTestCase): +class DeviceTestCase(APIViewTestCases.APIViewTestCase): model = Device brief_fields = ['description', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -2020,7 +2048,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase): self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) -class ModuleTest(APIViewTestCases.APIViewTestCase): +class ModuleTestCase(APIViewTestCases.APIViewTestCase): model = Module brief_fields = ['description', 'device', 'display', 'id', 'module_bay', 'module_type', 'url'] bulk_update_data = { @@ -2355,7 +2383,7 @@ class ModuleTest(APIViewTestCases.APIViewTestCase): self.assertEqual(len(response.data['results']), 1) -class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): +class ConsolePortTestCase(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): model = ConsolePort brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -2398,7 +2426,7 @@ class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa ] -class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): +class ConsoleServerPortTestCase(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): model = ConsoleServerPort brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -2441,7 +2469,7 @@ class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIView ] -class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): +class PowerPortTestCase(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): model = PowerPort brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -2481,7 +2509,7 @@ class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase ] -class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): +class PowerOutletTestCase(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): model = PowerOutlet brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -2530,7 +2558,7 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa ] -class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): +class InterfaceTestCase(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): model = Interface brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -2739,7 +2767,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase self._perform_interface_test_with_invalid_data(InterfaceModeChoices.MODE_TAGGED_ALL, invalid_data) -class FrontPortTest(APIViewTestCases.APIViewTestCase): +class FrontPortTestCase(APIViewTestCases.APIViewTestCase): model = FrontPort brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -2858,7 +2886,7 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase): self.assertHttpStatus(response, status.HTTP_200_OK) -class RearPortTest(APIViewTestCases.APIViewTestCase): +class RearPortTestCase(APIViewTestCases.APIViewTestCase): model = RearPort brief_fields = ['_occupied', 'cable', 'description', 'device', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -2974,7 +3002,7 @@ class RearPortTest(APIViewTestCases.APIViewTestCase): self.assertHttpStatus(response, status.HTTP_200_OK) -class ModuleBayTest(APIViewTestCases.APIViewTestCase): +class ModuleBayTestCase(APIViewTestCases.APIViewTestCase): model = ModuleBay brief_fields = ['_occupied', 'description', 'display', 'enabled', 'id', 'installed_module', 'name', 'url'] bulk_update_data = { @@ -3016,7 +3044,7 @@ class ModuleBayTest(APIViewTestCases.APIViewTestCase): ] -class DeviceBayTest(APIViewTestCases.APIViewTestCase): +class DeviceBayTestCase(APIViewTestCases.APIViewTestCase): model = DeviceBay brief_fields = ['_occupied', 'description', 'device', 'display', 'enabled', 'id', 'name', 'url'] bulk_update_data = { @@ -3080,7 +3108,7 @@ class DeviceBayTest(APIViewTestCases.APIViewTestCase): ] -class InventoryItemTest(APIViewTestCases.APIViewTestCase): +class InventoryItemTestCase(APIViewTestCases.APIViewTestCase): model = InventoryItem brief_fields = ['_depth', 'description', 'device', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -3147,7 +3175,7 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase): ] -class InventoryItemRoleTest(APIViewTestCases.APIViewTestCase): +class InventoryItemRoleTestCase(APIViewTestCases.APIViewTestCase): model = InventoryItemRole brief_fields = ['description', 'display', 'id', 'inventoryitem_count', 'name', 'slug', 'url'] create_data = [ @@ -3182,7 +3210,7 @@ class InventoryItemRoleTest(APIViewTestCases.APIViewTestCase): InventoryItemRole.objects.bulk_create(roles) -class CableBundleTest(APIViewTestCases.APIViewTestCase): +class CableBundleTestCase(APIViewTestCases.APIViewTestCase): model = CableBundle brief_fields = ['description', 'display', 'id', 'name', 'url'] create_data = [ @@ -3236,7 +3264,7 @@ class CableBundleTest(APIViewTestCases.APIViewTestCase): self.assertEqual(response.data['cable_count'], 2) -class CableTest(APIViewTestCases.APIViewTestCase): +class CableTestCase(APIViewTestCases.APIViewTestCase): model = Cable brief_fields = ['description', 'display', 'id', 'label', 'url'] bulk_update_data = { @@ -3445,7 +3473,7 @@ class CableTest(APIViewTestCases.APIViewTestCase): self.assertSetEqual(set(ids), expected) -class CableTerminationTest( +class CableTerminationTestCase( APIViewTestCases.GetObjectViewTestCase, APIViewTestCases.ListObjectsViewTestCase, ): @@ -3474,7 +3502,7 @@ class CableTerminationTest( cable.save() -class ConnectedDeviceTest(APITestCase): +class ConnectedDeviceTestCase(APITestCase): @classmethod def setUpTestData(cls): @@ -3497,8 +3525,8 @@ class ConnectedDeviceTest(APITestCase): cable = Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[1]]) cable.save() - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_get_connected_device(self): + self.add_permissions('dcim.view_device', 'dcim.view_interface') url = reverse('dcim-api:connected-device-list') url_params = '?peer_device=TestDevice1&peer_interface=eth0' @@ -3511,7 +3539,7 @@ class ConnectedDeviceTest(APITestCase): self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND) -class VirtualChassisTest(APIViewTestCases.APIViewTestCase): +class VirtualChassisTestCase(APIViewTestCases.APIViewTestCase): model = VirtualChassis brief_fields = ['description', 'display', 'id', 'master', 'member_count', 'name', 'url'] @@ -3592,7 +3620,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase): } -class PowerPanelTest(APIViewTestCases.APIViewTestCase): +class PowerPanelTestCase(APIViewTestCases.APIViewTestCase): model = PowerPanel brief_fields = ['description', 'display', 'id', 'name', 'powerfeed_count', 'url'] user_permissions = ('dcim.view_site', ) @@ -3642,7 +3670,7 @@ class PowerPanelTest(APIViewTestCases.APIViewTestCase): } -class PowerFeedTest(APIViewTestCases.APIViewTestCase): +class PowerFeedTestCase(APIViewTestCases.APIViewTestCase): model = PowerFeed brief_fields = ['_occupied', 'cable', 'description', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -3698,7 +3726,7 @@ class PowerFeedTest(APIViewTestCases.APIViewTestCase): ] -class VirtualDeviceContextTest(APIViewTestCases.APIViewTestCase): +class VirtualDeviceContextTestCase(APIViewTestCases.APIViewTestCase): model = VirtualDeviceContext brief_fields = ['description', 'device', 'display', 'id', 'identifier', 'name', 'url'] bulk_update_data = { @@ -3752,7 +3780,7 @@ class VirtualDeviceContextTest(APIViewTestCases.APIViewTestCase): ] -class MACAddressTest(APIViewTestCases.APIViewTestCase): +class MACAddressTestCase(APIViewTestCases.APIViewTestCase): model = MACAddress brief_fields = ['description', 'display', 'id', 'mac_address', 'url'] bulk_update_data = { diff --git a/netbox/dcim/tests/test_cable_profiles.py b/netbox/dcim/tests/test_cable_profiles.py index 1d504f42b..89299bf4f 100644 --- a/netbox/dcim/tests/test_cable_profiles.py +++ b/netbox/dcim/tests/test_cable_profiles.py @@ -9,10 +9,10 @@ from dcim.cable_profiles import ( ) from dcim.choices import CableProfileChoices from dcim.models import Cable, Interface, RearPort -from dcim.tests.utils import CablePathTestCase +from dcim.tests.utils import BaseCablePathTestCase -class CableProfileLinkPeerTests(CablePathTestCase): +class CableProfileLinkPeerTestCase(BaseCablePathTestCase): """ Tests for link peer resolution with cable profiles. """ @@ -75,7 +75,7 @@ class CableProfileLinkPeerTests(CablePathTestCase): self.assertEqual(interface.link_peers, [rear_ports[1]]) -class CableProfilePeerTerminationTests(CablePathTestCase): +class CableProfilePeerTerminationTestCase(BaseCablePathTestCase): """ Tests for BaseCableProfile.get_peer_termination() and get_peer_terminations(). Verifies that the batch method produces identical results to calling diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index 08c807280..7fa4f9b8b 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -2,11 +2,11 @@ from circuits.models import * from dcim.choices import LinkStatusChoices from dcim.models import * from dcim.svg import CableTraceSVG -from dcim.tests.utils import CablePathTestCase +from dcim.tests.utils import BaseCablePathTestCase from utilities.exceptions import AbortRequest -class LegacyCablePathTests(CablePathTestCase): +class LegacyCablePathTestCase(BaseCablePathTestCase): """ Test NetBox's ability to trace and retrace CablePaths in response to data model changes, without cable profiles. @@ -55,6 +55,18 @@ class LegacyCablePathTests(CablePathTestCase): # Check that all CablePaths have been deleted self.assertEqual(CablePath.objects.count(), 0) + # Check that connected interfaces are fully cleaned up + interface1.refresh_from_db() + interface2.refresh_from_db() + + self.assertIsNone(interface1.cable_id) + self.assertEqual(interface1.cable_end, '') + self.assertPathIsNotSet(interface1) + + self.assertIsNone(interface2.cable_id) + self.assertEqual(interface2.cable_end, '') + self.assertPathIsNotSet(interface2) + def test_102_consoleport_to_consoleserverport(self): """ [CP1] --C1-- [CSP1] diff --git a/netbox/dcim/tests/test_cablepaths2.py b/netbox/dcim/tests/test_cablepaths2.py index 68208ad07..6e13f61de 100644 --- a/netbox/dcim/tests/test_cablepaths2.py +++ b/netbox/dcim/tests/test_cablepaths2.py @@ -4,10 +4,10 @@ from circuits.models import CircuitTermination from dcim.choices import CableProfileChoices from dcim.models import * from dcim.svg import CableTraceSVG -from dcim.tests.utils import CablePathTestCase +from dcim.tests.utils import BaseCablePathTestCase -class CablePathTests(CablePathTestCase): +class CablePathTestCase(BaseCablePathTestCase): """ Test the creation of CablePaths for Cables with different profiles applied. @@ -1857,6 +1857,103 @@ class CablePathTests(CablePathTestCase): ) self.assertEqual(CablePath.objects.count(), 4) + def test_225_breakout_1c2p_2c1p_to_single_position_passthroughs(self): + """ + Regression test for #22187: a 1C2P:2C1P breakout cable terminating on two + single-position FrontPorts (each mapping 1:1 to a single-position RearPort) + which are then connected to PathEndpoints via unprofiled cables. + + [IF1] --C1 (1C2P:2C1P)-- [FP1] [RP1] --C2 (unprofiled)-- [IF2] + [FP2] [RP2] --C3 (unprofiled)-- [IF3] + """ + interfaces = [ + Interface.objects.create(device=self.device, name='Interface 1'), + Interface.objects.create(device=self.device, name='Interface 2'), + Interface.objects.create(device=self.device, name='Interface 3'), + ] + rear_ports = [ + RearPort.objects.create(device=self.device, name='Rear Port 1'), + RearPort.objects.create(device=self.device, name='Rear Port 2'), + ] + front_ports = [ + FrontPort.objects.create(device=self.device, name='Front Port 1'), + FrontPort.objects.create(device=self.device, name='Front Port 2'), + ] + PortMapping.objects.bulk_create([ + PortMapping( + device=self.device, + front_port=front_ports[0], + front_port_position=1, + rear_port=rear_ports[0], + rear_port_position=1, + ), + PortMapping( + device=self.device, + front_port=front_ports[1], + front_port_position=1, + rear_port=rear_ports[1], + rear_port_position=1, + ), + ]) + + # Create cables + cable1 = Cable( + profile=CableProfileChoices.BREAKOUT_1C2P_2C1P, + a_terminations=[interfaces[0]], + b_terminations=[front_ports[0], front_ports[1]], + ) + cable1.clean() + cable1.save() + cable2 = Cable( + a_terminations=[rear_ports[0]], + b_terminations=[interfaces[1]], + ) + cable2.clean() + cable2.save() + cable3 = Cable( + a_terminations=[rear_ports[1]], + b_terminations=[interfaces[2]], + ) + cable3.clean() + cable3.save() + + # The breakout splits IF1's two positions into separate downstream cables, + # so the forward path has FP1/FP2, RP1/RP2, and the two unprofiled cables + # at the corresponding hops. + path1 = self.assertPathExists( + ( + interfaces[0], + cable1, + [front_ports[0], front_ports[1]], + [rear_ports[0], rear_ports[1]], + [cable2, cable3], + [interfaces[1], interfaces[2]], + ), + is_complete=True, + is_active=True, + ) + path2 = self.assertPathExists( + (interfaces[1], cable2, rear_ports[0], front_ports[0], cable1, interfaces[0]), + is_complete=True, + is_active=True, + ) + path3 = self.assertPathExists( + (interfaces[2], cable3, rear_ports[1], front_ports[1], cable1, interfaces[0]), + is_complete=True, + is_active=True, + ) + self.assertEqual(CablePath.objects.count(), 3) + for interface in interfaces: + interface.refresh_from_db() + self.assertPathIsSet(interfaces[0], path1) + self.assertPathIsSet(interfaces[1], path2) + self.assertPathIsSet(interfaces[2], path3) + + # Test SVG generation from both directions + CableTraceSVG(interfaces[0]).render() + CableTraceSVG(interfaces[1]).render() + CableTraceSVG(interfaces[2]).render() + def test_304_add_port_mapping_between_connected_ports(self): """ [IF1] --C1-- [FP1] [RP1] --C2-- [IF2] diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index e003487dd..007445670 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.test import TestCase from circuits.models import Circuit, CircuitTermination, CircuitType, Provider @@ -1706,6 +1707,10 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests): PortTemplateMapping(module_type=module_types[0], front_port=front_ports[0], rear_port=rear_ports[0]), PortTemplateMapping(module_type=module_types[1], front_port=front_ports[1], rear_port=rear_ports[1]), ]) + ModuleBayTemplate.objects.bulk_create(( + ModuleBayTemplate(module_type=module_types[0], name='Module Bay 1'), + ModuleBayTemplate(module_type=module_types[1], name='Module Bay 2'), + )) def test_q(self): params = {'q': 'foobar1'} @@ -1766,6 +1771,12 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'pass_through_ports': 'false'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_module_bays(self): + params = {'module_bays': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'module_bays': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_weight(self): params = {'weight': [10, 20]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -6813,6 +6824,13 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests): circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') circuit = Circuit.objects.create(cid='Circuit 1', provider=provider, type=circuit_type) circuit_termination = CircuitTermination.objects.create(circuit=circuit, term_side='A', termination=sites[0]) + circuit2 = Circuit.objects.create(cid='Circuit 2', provider=provider, type=circuit_type) + circuit_termination_a = CircuitTermination.objects.create( + circuit=circuit2, term_side='A', termination=sites[0] + ) + circuit_termination_z = CircuitTermination.objects.create( + circuit=circuit2, term_side='Z', termination=locations[0] + ) # Cables cables = ( @@ -6919,6 +6937,11 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests): a_terminations=[circuit_termination], label='Cable 14' ), + Cable( + a_terminations=[circuit_termination_a], + b_terminations=[circuit_termination_z], + label='Cable 15' + ), ) for cable in cables: cable.save() @@ -6943,13 +6966,13 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'type': [CableTypeChoices.TYPE_CAT3, CableTypeChoices.TYPE_CAT5E]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) params = {'type__empty': 'true'} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 9) params = {'type__empty': 'false'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) def test_status(self): params = {'status': [LinkStatusChoices.STATUS_CONNECTED]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 11) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 12) params = {'status': [LinkStatusChoices.STATUS_PLANNED]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) @@ -6978,16 +7001,16 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests): def test_location(self): locations = Location.objects.all()[:2] params = {'location_id': [locations[0].pk, locations[1].pk]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 11) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 12) params = {'location': [locations[0].name, locations[1].name]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 11) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 12) def test_site(self): site = Site.objects.all()[:2] params = {'site_id': [site[0].pk, site[1].pk]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 11) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 13) params = {'site': [site[0].slug, site[1].slug]} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 11) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 13) def test_tenant(self): tenant = Tenant.objects.all()[:2] @@ -7015,7 +7038,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'unterminated': True} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) params = {'unterminated': False} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7) def test_consoleport(self): params = {'consoleport_id': [ConsolePort.objects.first().pk]} @@ -7050,6 +7073,96 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) +class CableTerminationTestCase(TestCase, ChangeLoggedFilterSetTests): + queryset = CableTermination.objects.all() + filterset = CableTerminationFilterSet + ignore_fields = ('connector', 'positions') + filter_name_map = { + 'consoleport': 'consoleport_id', + 'consoleserverport': 'consoleserverport_id', + 'powerport': 'powerport_id', + 'poweroutlet': 'poweroutlet_id', + 'interface': 'interface_id', + 'frontport': 'frontport_id', + 'rearport': 'rearport_id', + 'powerfeed': 'powerfeed_id', + 'circuittermination': 'circuittermination_id', + } + + @classmethod + def setUpTestData(cls): + site = Site.objects.create(name='Site 1', slug='site-1') + device = create_test_device('Device 1', site=site) + + cls.interfaces = [ + Interface(device=device, name=f'eth{i}', type=InterfaceTypeChoices.TYPE_1GE_FIXED) + for i in range(4) + ] + Interface.objects.bulk_create(cls.interfaces) + + cls.consoleport = ConsolePort.objects.create(device=device, name='Console Port 1') + cls.consoleserverport = ConsoleServerPort.objects.create(device=device, name='Console Server Port 1') + cls.powerport = PowerPort.objects.create(device=device, name='Power Port 1') + cls.poweroutlet = PowerOutlet.objects.create(device=device, name='Power Outlet 1') + cls.rearport = RearPort.objects.create(device=device, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C) + cls.frontport = FrontPort.objects.create(device=device, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C) + PortMapping.objects.create(device=device, front_port=cls.frontport, rear_port=cls.rearport) + + power_panel = PowerPanel.objects.create(name='Power Panel 1', site=site) + cls.powerfeed = PowerFeed.objects.create(name='Power Feed 1', power_panel=power_panel) + + provider = Provider.objects.create(name='Provider 1', slug='provider-1') + circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') + circuit = Circuit.objects.create(cid='Circuit 1', provider=provider, type=circuit_type) + cls.circuittermination = CircuitTermination.objects.create( + circuit=circuit, term_side='A', termination=site, + ) + + # Two bipartite interface cables (4 CableTerminations) plus one single-end cable per + # non-Interface component (8 CableTerminations). + cables = [ + Cable(a_terminations=[cls.interfaces[0]], b_terminations=[cls.interfaces[1]], label='Cable 1'), + Cable(a_terminations=[cls.interfaces[2]], b_terminations=[cls.interfaces[3]], label='Cable 2'), + ] + for component in ( + cls.consoleport, cls.consoleserverport, cls.powerport, cls.poweroutlet, + cls.rearport, cls.frontport, cls.powerfeed, cls.circuittermination, + ): + cables.append(Cable(a_terminations=[component], label=f'Cable for {component._meta.model_name}')) + for cable in cables: + cable.save() + + def test_cable(self): + """Filter CableTerminations by cable ID.""" + cables = Cable.objects.all()[:2] + params = {'cable_id': [cables[0].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'cable_id': [cables[0].pk, cables[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + + def test_termination_object_filters(self): + """Each _id filter resolves to only matching terminations.""" + cases = ( + ('consoleport_id', ConsolePort, self.consoleport), + ('consoleserverport_id', ConsoleServerPort, self.consoleserverport), + ('powerport_id', PowerPort, self.powerport), + ('poweroutlet_id', PowerOutlet, self.poweroutlet), + ('interface_id', Interface, self.interfaces[0]), + ('frontport_id', FrontPort, self.frontport), + ('rearport_id', RearPort, self.rearport), + ('powerfeed_id', PowerFeed, self.powerfeed), + ('circuittermination_id', CircuitTermination, self.circuittermination), + ) + for filter_name, model, obj in cases: + with self.subTest(filter_name=filter_name): + ct = ContentType.objects.get_for_model(model) + params = {filter_name: [obj.pk]} + results = self.filterset(params, self.queryset).qs + self.assertEqual(results.count(), 1) + self.assertEqual(results.first().termination_type, ct) + self.assertEqual(results.first().termination_id, obj.pk) + + class PowerPanelTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = PowerPanel.objects.all() filterset = PowerPanelFilterSet diff --git a/netbox/dcim/tests/test_management_commands.py b/netbox/dcim/tests/test_management_commands.py new file mode 100644 index 000000000..453171d4d --- /dev/null +++ b/netbox/dcim/tests/test_management_commands.py @@ -0,0 +1,150 @@ +import json +import tempfile +from io import StringIO +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +from django.core.management import call_command +from django.test import TestCase, override_settings + + +class BuildSchemaTestCase(TestCase): + def test_output_is_valid_json(self): + out = StringIO() + + call_command('buildschema', stdout=out) + + self.assertIsInstance(json.loads(out.getvalue()), dict) + + def test_write_flag_writes_schema_to_configured_base_dir(self): + with tempfile.TemporaryDirectory() as tmpdir: + base_dir = Path(tmpdir) / 'netbox' + output_dir = Path(tmpdir) / 'contrib' + output_dir.mkdir() + + out = StringIO() + with override_settings(BASE_DIR=base_dir): + call_command('buildschema', write=True, stdout=out) + + output_file = output_dir / 'generated_schema.json' + self.assertTrue(output_file.exists()) + self.assertIsInstance(json.loads(output_file.read_text(encoding='utf-8')), dict) + self.assertIn(str(output_file), out.getvalue()) + + +class TracePathsTestCase(TestCase): + def test_no_cables(self): + out = StringIO() + + call_command('trace_paths', no_input=True, stdout=out) + + self.assertIn('Finished.', out.getvalue()) + + def test_force_no_cable_paths(self): + out = StringIO() + + call_command('trace_paths', force=True, no_input=True, stdout=out) + + self.assertIn('Finished.', out.getvalue()) + + def test_retraces_missing_cabled_endpoint_path(self): + endpoint = object() + + class FakeQuerySet(list): + def filter(self, *args, **kwargs): + return self + + def count(self): + return len(self) + + class FakeObjects: + def filter(self, *args, **kwargs): + return FakeQuerySet([endpoint]) + + model = SimpleNamespace( + objects=FakeObjects(), + wireless_link=object(), + _meta=SimpleNamespace(verbose_name='interface', verbose_name_plural='interfaces'), + ) + out = StringIO() + + with ( + patch('dcim.management.commands.trace_paths.ENDPOINT_MODELS', (model,)), + patch('dcim.management.commands.trace_paths.create_cablepaths') as create_cablepaths, + ): + call_command('trace_paths', no_input=True, stdout=out) + + create_cablepaths.assert_called_once_with([endpoint]) + self.assertIn('Retracing 1 cabled interfaces', out.getvalue()) + self.assertIn('Retraced 1 interfaces', out.getvalue()) + self.assertIn('Finished.', out.getvalue()) + + def test_progress_bar_drawn_every_100_endpoints(self): + endpoints = [object() for _ in range(100)] + + class FakeQuerySet(list): + def filter(self, *args, **kwargs): + return self + + def count(self): + return len(self) + + class FakeObjects: + def filter(self, *args, **kwargs): + return FakeQuerySet(endpoints) + + model = SimpleNamespace( + objects=FakeObjects(), + wireless_link=object(), + _meta=SimpleNamespace(verbose_name='interface', verbose_name_plural='interfaces'), + ) + out = StringIO() + + with ( + patch('dcim.management.commands.trace_paths.ENDPOINT_MODELS', (model,)), + patch('dcim.management.commands.trace_paths.create_cablepaths'), + ): + call_command('trace_paths', no_input=True, stdout=out) + + self.assertIn('[####################] 100%', out.getvalue()) + self.assertIn('Retraced 100 interfaces', out.getvalue()) + + def test_force_aborts_when_confirmation_is_not_yes(self): + out = StringIO() + cable_paths = MagicMock() + cable_paths.count.return_value = 1 + + with ( + patch('dcim.management.commands.trace_paths.CablePath') as cable_path_model, + patch('builtins.input', return_value='no'), + ): + cable_path_model.objects.all.return_value = cable_paths + call_command('trace_paths', force=True, stdout=out) + + cable_paths.delete.assert_not_called() + self.assertIn('WARNING: Forcing recalculation', out.getvalue()) + self.assertIn('Aborting', out.getvalue()) + + def test_force_deletes_existing_paths_and_resets_sequence(self): + out = StringIO() + cable_paths = MagicMock() + cable_paths.count.return_value = 2 + cable_paths.delete.return_value = (2, {}) + + with ( + patch('dcim.management.commands.trace_paths.CablePath') as cable_path_model, + patch('dcim.management.commands.trace_paths.ENDPOINT_MODELS', ()), + patch('dcim.management.commands.trace_paths.connection') as connection, + ): + cable_path_model.objects.all.return_value = cable_paths + connection.ops.sequence_reset_sql.return_value = ['RESET SEQUENCE'] + cursor = connection.cursor.return_value.__enter__.return_value + + call_command('trace_paths', force=True, no_input=True, stdout=out) + + cable_paths.delete.assert_called_once_with() + cursor.execute.assert_called_once_with('RESET SEQUENCE') + self.assertIn('Deleting 2 existing cable paths', out.getvalue()) + self.assertIn('Deleted 2 paths', out.getvalue()) + self.assertIn('Finished.', out.getvalue()) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 3e3d1c526..260615b4e 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -114,6 +114,71 @@ class LocationTestCase(TestCase): self.assertEqual(PowerPanel.objects.get(pk=powerpanel1.pk).site, site_b) +class DeviceTypeTestCase(TestCase): + + def test_component_template_counts(self): + """ + DeviceType component template counters should track the addition and removal of templates. + """ + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + device_type = DeviceType.objects.create( + manufacturer=manufacturer, model='Device Type 1', slug='device-type-1' + ) + + # Counters should start at zero + self.assertEqual(device_type.interface_template_count, 0) + self.assertEqual(device_type.console_port_template_count, 0) + self.assertEqual(device_type.module_bay_template_count, 0) + self.assertEqual(device_type.device_bay_template_count, 0) + + # Adding templates should increment the relevant counters + InterfaceTemplate.objects.create(device_type=device_type, name='Interface 1') + InterfaceTemplate.objects.create(device_type=device_type, name='Interface 2') + ConsolePortTemplate.objects.create(device_type=device_type, name='Console 1') + ModuleBayTemplate.objects.create(device_type=device_type, name='Module Bay 1') + DeviceBayTemplate.objects.create(device_type=device_type, name='Device Bay 1') + device_type.refresh_from_db() + self.assertEqual(device_type.interface_template_count, 2) + self.assertEqual(device_type.console_port_template_count, 1) + self.assertEqual(device_type.module_bay_template_count, 1) + self.assertEqual(device_type.device_bay_template_count, 1) + + # Deleting a template should decrement the counter + InterfaceTemplate.objects.get(device_type=device_type, name='Interface 1').delete() + device_type.refresh_from_db() + self.assertEqual(device_type.interface_template_count, 1) + + +class ModuleTypeTestCase(TestCase): + + def test_component_template_counts(self): + """ + ModuleType component template counters should track the addition and removal of templates. + """ + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1') + + # Counters should start at zero + self.assertEqual(module_type.interface_template_count, 0) + self.assertEqual(module_type.console_port_template_count, 0) + self.assertEqual(module_type.module_bay_template_count, 0) + + # Adding templates should increment the relevant counters + InterfaceTemplate.objects.create(module_type=module_type, name='Interface 1') + InterfaceTemplate.objects.create(module_type=module_type, name='Interface 2') + ConsolePortTemplate.objects.create(module_type=module_type, name='Console 1') + ModuleBayTemplate.objects.create(module_type=module_type, name='Module Bay 1') + module_type.refresh_from_db() + self.assertEqual(module_type.interface_template_count, 2) + self.assertEqual(module_type.console_port_template_count, 1) + self.assertEqual(module_type.module_bay_template_count, 1) + + # Deleting a template should decrement the counter + InterfaceTemplate.objects.get(module_type=module_type, name='Interface 1').delete() + module_type.refresh_from_db() + self.assertEqual(module_type.interface_template_count, 1) + + class RackTypeTestCase(TestCase): @classmethod @@ -915,6 +980,214 @@ class ModuleBayTestCase(TestCase): module_1.clean() module_1.save() + @tag('regression') # #22146 + def test_module_bay_ordering_after_recreate(self): + """ + Module bays must remain in name order after a delete-and-recreate cycle, + even though MPTT no longer renumbers tree_ids on root insertion. + """ + device_type = DeviceType.objects.first() + device_role = DeviceRole.objects.first() + site = Site.objects.first() + device = Device.objects.create( + name='Ordering Test Device', + device_type=device_type, + role=device_role, + site=site, + ) + for name in ('Bay 1', 'Bay 2', 'Bay 3', 'Bay 4'): + ModuleBay.objects.create(device=device, name=name) + + ModuleBay.objects.get(device=device, name='Bay 3').delete() + ModuleBay.objects.create(device=device, name='Bay 3') + + names = list(ModuleBay.objects.filter(device=device).values_list('name', flat=True)) + self.assertEqual(names, ['Bay 1', 'Bay 2', 'Bay 3', 'Bay 4']) + + @tag('regression') # #22146 + def test_module_bay_natural_ordering(self): + """ + Module bays must be returned in natural (numeric-aware) order, e.g. + "Bay 2" before "Bay 10". + """ + device_type = DeviceType.objects.first() + device_role = DeviceRole.objects.first() + site = Site.objects.first() + device = Device.objects.create( + name='Natural Sort Device', + device_type=device_type, + role=device_role, + site=site, + ) + # Insert in non-natural order to confirm sort is not insertion-driven. + for name in ('Bay 10', 'Bay 1', 'Bay 2', 'Bay 11'): + ModuleBay.objects.create(device=device, name=name) + + names = list(ModuleBay.objects.filter(device=device).values_list('name', flat=True)) + self.assertEqual(names, ['Bay 1', 'Bay 2', 'Bay 10', 'Bay 11']) + + @tag('regression') # #22146 + def test_child_module_bay_ordering(self): + """ + Child module bays inside a module must be returned in name order even + when inserted out of order. + """ + device_type = DeviceType.objects.first() + device_role = DeviceRole.objects.first() + site = Site.objects.first() + device = Device.objects.create( + name='Child Ordering Device', + device_type=device_type, + role=device_role, + site=site, + ) + root_bay = ModuleBay.objects.create(device=device, name='Bay 1') + manufacturer = Manufacturer.objects.first() + module_type = ModuleType.objects.create( + manufacturer=manufacturer, model='Child Ordering Type' + ) + module = Module.objects.create( + device=device, module_bay=root_bay, module_type=module_type + ) + # Insert children out of name order. + for name in ('Bay 1.1', 'Bay 1.3', 'Bay 1.2'): + ModuleBay.objects.create(device=device, module=module, name=name) + + names = list(ModuleBay.objects.filter(device=device).values_list('name', flat=True)) + self.assertEqual(names, ['Bay 1', 'Bay 1.1', 'Bay 1.2', 'Bay 1.3']) + + @tag('regression') # #22146 + def test_root_module_bay_rename_preserves_tree_ids(self): + """ + Renaming a root module bay must not renumber any other root tree's + tree_id. The renamed bay's own tree_id is also expected to remain + stable, but the load-bearing assertion is that the *other* bays are + not shifted. + """ + device_type = DeviceType.objects.first() + device_role = DeviceRole.objects.first() + site = Site.objects.first() + device = Device.objects.create( + name='Rename TreeID Device', + device_type=device_type, + role=device_role, + site=site, + ) + for name in ('Bay 1', 'Bay 2', 'Bay 3', 'Bay 4'): + ModuleBay.objects.create(device=device, name=name) + + tree_ids_before = { + bay.name: bay.tree_id + for bay in ModuleBay.objects.filter(device=device) + } + + bay = ModuleBay.objects.get(device=device, name='Bay 2') + bay.name = 'Bay 99' + bay.save() + + tree_ids_after = { + bay.name: bay.tree_id + for bay in ModuleBay.objects.filter(device=device) + } + for name in ('Bay 1', 'Bay 3', 'Bay 4'): + self.assertEqual(tree_ids_after[name], tree_ids_before[name]) + self.assertEqual(tree_ids_after['Bay 99'], tree_ids_before['Bay 2']) + + @tag('regression') # #22146 + def test_root_module_bay_rename_updates_display_order(self): + """ + Even though renaming a root module bay does not renumber tree_ids, + the manager's _root_name annotation must reflect the new name so the + display ordering is correct. + """ + device_type = DeviceType.objects.first() + device_role = DeviceRole.objects.first() + site = Site.objects.first() + device = Device.objects.create( + name='Rename Order Device', + device_type=device_type, + role=device_role, + site=site, + ) + for name in ('Bay 1', 'Bay 2', 'Bay 3'): + ModuleBay.objects.create(device=device, name=name) + + bay = ModuleBay.objects.get(device=device, name='Bay 1') + bay.name = 'Bay 4' + bay.save() + + names = list(ModuleBay.objects.filter(device=device).values_list('name', flat=True)) + self.assertEqual(names, ['Bay 2', 'Bay 3', 'Bay 4']) + + @tag('regression') # #22146 + def test_child_module_bay_rename_preserves_intra_tree_ordering(self): + """ + Renaming a *child* module bay must still trigger MPTT's intra-tree + reorder, so siblings appear in name order after the rename. The + rename-bypass only covers root bays. + """ + device_type = DeviceType.objects.first() + device_role = DeviceRole.objects.first() + site = Site.objects.first() + device = Device.objects.create( + name='Child Rename Device', + device_type=device_type, + role=device_role, + site=site, + ) + root_bay = ModuleBay.objects.create(device=device, name='Bay 1') + manufacturer = Manufacturer.objects.first() + module_type = ModuleType.objects.create( + manufacturer=manufacturer, model='Child Rename Type' + ) + module = Module.objects.create( + device=device, module_bay=root_bay, module_type=module_type + ) + for name in ('Bay 1.1', 'Bay 1.2', 'Bay 1.3'): + ModuleBay.objects.create(device=device, module=module, name=name) + + child = ModuleBay.objects.get(device=device, name='Bay 1.1') + child.name = 'Bay 1.4' + child.save() + + names = list(ModuleBay.objects.filter(device=device).values_list('name', flat=True)) + self.assertEqual(names, ['Bay 1', 'Bay 1.2', 'Bay 1.3', 'Bay 1.4']) + + @tag('regression') # #22146 + def test_root_to_child_transition_still_relocates(self): + """ + Promoting an existing root module bay to a child (by assigning a + module) must still flow through MPTT's normal move logic. The + rename-bypass must not suppress legitimate parent changes. + """ + device_type = DeviceType.objects.first() + device_role = DeviceRole.objects.first() + site = Site.objects.first() + device = Device.objects.create( + name='Root To Child Device', + device_type=device_type, + role=device_role, + site=site, + ) + host_bay = ModuleBay.objects.create(device=device, name='Host Bay') + movable_bay = ModuleBay.objects.create(device=device, name='Movable Bay') + + manufacturer = Manufacturer.objects.first() + module_type = ModuleType.objects.create( + manufacturer=manufacturer, model='Root To Child Type' + ) + host_module = Module.objects.create( + device=device, module_bay=host_bay, module_type=module_type + ) + + movable_bay.module = host_module + movable_bay.save() + + movable_bay.refresh_from_db() + host_bay.refresh_from_db() + self.assertEqual(movable_bay.parent_id, host_bay.pk) + self.assertEqual(movable_bay.tree_id, host_bay.tree_id) + def test_single_module_token(self): device_type = DeviceType.objects.first() device_role = DeviceRole.objects.first() diff --git a/netbox/dcim/tests/test_signals.py b/netbox/dcim/tests/test_signals.py new file mode 100644 index 000000000..9edcfbd46 --- /dev/null +++ b/netbox/dcim/tests/test_signals.py @@ -0,0 +1,488 @@ +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +from django.contrib.contenttypes.models import ContentType +from django.test import SimpleTestCase, TestCase + +from dcim import signals +from dcim.choices import CableEndChoices, LinkStatusChoices +from dcim.models import ( + Cable, + CablePath, + Device, + DeviceRole, + DeviceType, + FrontPort, + Interface, + Location, + MACAddress, + Manufacturer, + PortMapping, + PowerPanel, + Rack, + RearPort, + Site, + SiteGroup, + VirtualChassis, +) +from ipam.models import Prefix +from virtualization.models import Cluster, ClusterType +from wireless.models import WirelessLAN + + +class LocationSiteChangeSignalTestCase(TestCase): + """ + Verify dcim.signals.handle_location_site_change propagates a Location's new Site to + every descendant Location, Rack, Device, PowerPanel, and component when the parent + Location's site assignment changes. + """ + + @classmethod + def setUpTestData(cls): + cls.site_a = Site.objects.create(name='Site A', slug='site-a') + cls.site_b = Site.objects.create(name='Site B', slug='site-b') + manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer') + cls.device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type') + cls.device_role = DeviceRole.objects.create(name='Device Role', slug='device-role') + + def test_changing_location_site_propagates_to_children(self): + parent_location = Location.objects.create(name='Parent', slug='parent', site=self.site_a) + child_location = Location.objects.create(name='Child', slug='child', site=self.site_a, parent=parent_location) + rack = Rack.objects.create(name='Rack', site=self.site_a, location=parent_location) + device = Device.objects.create( + name='Device', + site=self.site_a, + location=parent_location, + device_type=self.device_type, + role=self.device_role, + ) + interface = Interface.objects.create(device=device, name='Interface 1') + power_panel = PowerPanel.objects.create(name='Panel', site=self.site_a, location=parent_location) + + parent_location.site = self.site_b + parent_location.save() + + child_location.refresh_from_db() + rack.refresh_from_db() + device.refresh_from_db() + interface.refresh_from_db() + power_panel.refresh_from_db() + self.assertEqual(child_location.site, self.site_b) + self.assertEqual(rack.site, self.site_b) + self.assertEqual(device.site, self.site_b) + self.assertEqual(interface._site, self.site_b) + self.assertEqual(power_panel.site, self.site_b) + + def test_creating_location_does_not_attempt_to_propagate(self): + # Should not raise — newly-created locations have no descendants. + Location.objects.create(name='New', slug='new', site=self.site_a) + + +class RackSiteChangeSignalTestCase(TestCase): + """ + Verify dcim.signals.handle_rack_site_change propagates a Rack's site/location to its + Devices and their components when the Rack is moved. + """ + + @classmethod + def setUpTestData(cls): + cls.site_a = Site.objects.create(name='Site A', slug='site-a') + cls.site_b = Site.objects.create(name='Site B', slug='site-b') + cls.location_b = Location.objects.create(name='Loc B', slug='loc-b', site=cls.site_b) + manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer') + cls.device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type') + cls.device_role = DeviceRole.objects.create(name='Device Role', slug='device-role') + + def test_changing_rack_site_propagates_to_devices_and_components(self): + rack = Rack.objects.create(name='Rack', site=self.site_a) + device = Device.objects.create( + name='Device', + site=self.site_a, + rack=rack, + device_type=self.device_type, + role=self.device_role, + ) + interface = Interface.objects.create(device=device, name='Interface 1') + + rack.site = self.site_b + rack.location = self.location_b + rack.save() + + device.refresh_from_db() + interface.refresh_from_db() + self.assertEqual(device.site, self.site_b) + self.assertEqual(device.location, self.location_b) + self.assertEqual(interface._site, self.site_b) + self.assertEqual(interface._location, self.location_b) + + +class DeviceSiteChangeSignalTestCase(TestCase): + """ + Verify dcim.signals.handle_device_site_change propagates a Device's site/location/rack + to its components on save. + """ + + @classmethod + def setUpTestData(cls): + cls.site_a = Site.objects.create(name='Site A', slug='site-a') + cls.site_b = Site.objects.create(name='Site B', slug='site-b') + manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer') + cls.device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type') + cls.device_role = DeviceRole.objects.create(name='Device Role', slug='device-role') + + def test_moving_device_updates_components_cached_scope(self): + device = Device.objects.create( + name='Device', + site=self.site_a, + device_type=self.device_type, + role=self.device_role, + ) + interface = Interface.objects.create(device=device, name='Interface 1') + self.assertEqual(interface._site, self.site_a) + + device.site = self.site_b + device.save() + + interface.refresh_from_db() + self.assertEqual(interface._site, self.site_b) + + +class VirtualChassisMasterSignalTestCase(TestCase): + """ + Verify dcim.signals.assign_virtualchassis_master links the master device back to a + newly-created VirtualChassis. + """ + + @classmethod + def setUpTestData(cls): + cls.site = Site.objects.create(name='Site', slug='site') + manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer') + cls.device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type') + cls.device_role = DeviceRole.objects.create(name='Device Role', slug='device-role') + + def test_master_is_assigned_to_new_virtual_chassis(self): + master = Device.objects.create( + name='Master', + site=self.site, + device_type=self.device_type, + role=self.device_role, + ) + vc = VirtualChassis.objects.create(name='VC 1', master=master) + + master.refresh_from_db() + self.assertEqual(master.virtual_chassis, vc) + self.assertEqual(master.vc_position, 1) + + def test_updating_virtual_chassis_does_not_reassign_master(self): + master = Device.objects.create( + name='Master', + site=self.site, + device_type=self.device_type, + role=self.device_role, + ) + vc = VirtualChassis.objects.create(name='VC 1', master=master) + + # Detach the master, then save the VC again — the signal should not re-link. + master.virtual_chassis = None + master.vc_position = None + master.save() + + vc.domain = 'updated' + vc.save() + + master.refresh_from_db() + self.assertIsNone(master.virtual_chassis) + + +class CableSignalTestCase(TestCase): + """ + Verify dcim.signals.update_connected_endpoints, retrace_cable_paths, and + nullify_connected_endpoints maintain CablePaths in response to Cable lifecycle events. + """ + + @classmethod + def setUpTestData(cls): + cls.site = Site.objects.create(name='Site', slug='site') + manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer') + device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type') + role = DeviceRole.objects.create(name='Device Role', slug='device-role') + cls.device = Device.objects.create( + name='Device', + site=cls.site, + device_type=device_type, + role=role, + ) + + def test_creating_cable_creates_endpoint_paths(self): + interface_a = Interface.objects.create(device=self.device, name='Interface A') + interface_b = Interface.objects.create(device=self.device, name='Interface B') + cable = Cable(a_terminations=[interface_a], b_terminations=[interface_b]) + cable.save() + + self.assertEqual(CablePath.objects.count(), 2) + interface_a.refresh_from_db() + self.assertIsNotNone(interface_a._path_id) + + def test_changing_cable_status_marks_paths_inactive(self): + interface_a = Interface.objects.create(device=self.device, name='Interface A') + interface_b = Interface.objects.create(device=self.device, name='Interface B') + cable = Cable(a_terminations=[interface_a], b_terminations=[interface_b]) + cable.save() + self.assertTrue(all(cp.is_active for cp in CablePath.objects.all())) + + # Reload the cable so _orig_status reflects the persisted value and + # _terminations_modified resets to False. + cable = Cable.objects.get(pk=cable.pk) + cable.status = LinkStatusChoices.STATUS_PLANNED + cable.save() + + self.assertFalse(any(cp.is_active for cp in CablePath.objects.all())) + + def test_reconnecting_cable_marks_paths_active(self): + interface_a = Interface.objects.create(device=self.device, name='Interface A') + interface_b = Interface.objects.create(device=self.device, name='Interface B') + cable = Cable( + a_terminations=[interface_a], + b_terminations=[interface_b], + status=LinkStatusChoices.STATUS_PLANNED, + ) + cable.save() + self.assertFalse(any(cp.is_active for cp in CablePath.objects.all())) + + cable = Cable.objects.get(pk=cable.pk) + cable.status = LinkStatusChoices.STATUS_CONNECTED + cable.save() + + self.assertTrue(all(cp.is_active for cp in CablePath.objects.all())) + + def test_deleting_cable_retraces_paths(self): + interface_a = Interface.objects.create(device=self.device, name='Interface A') + interface_b = Interface.objects.create(device=self.device, name='Interface B') + cable = Cable(a_terminations=[interface_a], b_terminations=[interface_b]) + cable.save() + self.assertEqual(CablePath.objects.count(), 2) + + cable.delete() + self.assertEqual(CablePath.objects.count(), 0) + interface_a.refresh_from_db() + interface_b.refresh_from_db() + # Cable deletion must fully detach both endpoints, even though the + # nullify_connected_endpoints signal short-circuits during Cable cascade. + self.assertIsNone(interface_a._path_id) + self.assertIsNone(interface_b._path_id) + self.assertIsNone(interface_a.cable_id) + self.assertIsNone(interface_b.cable_id) + self.assertEqual(interface_a.cable_end, '') + self.assertEqual(interface_b.cable_end, '') + + def test_deleting_cable_skips_per_termination_retrace(self): + """ + When a Cable is deleted, nullify_connected_endpoints (post_delete on each + cascaded CableTermination) must skip retracing — retrace_cable_paths + retraces each affected path once on Cable post_delete instead. See #22104. + + Without the short-circuit, retrace would fire (n_terminations * n_paths) + times from the per-termination handler plus n_paths times from the Cable + handler — for this 2-termination, 2-path cable, 6 calls total. With the + short-circuit, only the n_paths calls from retrace_cable_paths remain. + """ + interface_a = Interface.objects.create(device=self.device, name='Interface A') + interface_b = Interface.objects.create(device=self.device, name='Interface B') + cable = Cable(a_terminations=[interface_a], b_terminations=[interface_b]) + cable.save() + self.assertEqual(CablePath.objects.count(), 2) + self.assertFalse(Cable._is_being_deleted(cable.pk)) + + with patch('dcim.models.cables.CablePath.retrace') as retrace: + cable.delete() + + # Exactly one retrace per affected CablePath (from retrace_cable_paths), + # not the n*m calls the per-termination handler would have made. + self.assertEqual(retrace.call_count, 2) + # The deletion-tracking set must be cleaned up after delete() returns, + # even when the cascade runs to completion. + self.assertFalse(Cable._is_being_deleted(cable.pk)) + + def test_creating_portmapping_retraces_dependent_paths(self): + interface = Interface.objects.create(device=self.device, name='Interface A') + front_port = FrontPort.objects.create(device=self.device, name='Front Port 1') + rear_port = RearPort.objects.create(device=self.device, name='Rear Port 1') + Cable(a_terminations=[interface], b_terminations=[front_port]).save() + + # Creating a PortMapping connecting the front and rear ports should retrace paths + # that traverse either port (i.e. the incomplete path through front_port). + PortMapping.objects.create( + device=self.device, + front_port=front_port, + front_port_position=1, + rear_port=rear_port, + rear_port_position=1, + ) + + path = CablePath.objects.filter(_nodes__contains=front_port).first() + self.assertIsNotNone(path) + # The retraced path should now extend through to the rear port. Path nodes are + # encoded as ":". + rear_port_node = f'{ContentType.objects.get_for_model(RearPort).pk}:{rear_port.pk}' + flat_nodes = [n for step in path.path for n in step] + self.assertIn(rear_port_node, flat_nodes) + + def test_deleting_cabletermination_nullifies_endpoints(self): + interface_a = Interface.objects.create(device=self.device, name='Interface A') + interface_b = Interface.objects.create(device=self.device, name='Interface B') + cable = Cable(a_terminations=[interface_a], b_terminations=[interface_b]) + cable.save() + termination = cable.terminations.get(cable_end=CableEndChoices.SIDE_A) + + termination.delete() + interface_a.refresh_from_db() + self.assertIsNone(interface_a.cable_id) + self.assertEqual(interface_a.cable_end, '') + + +class MACAddressInterfaceSignalTestCase(TestCase): + """ + Verify dcim.signals.update_mac_address_interface assigns a designated primary MAC to + the newly-created Interface or VMInterface. + """ + + @classmethod + def setUpTestData(cls): + cls.site = Site.objects.create(name='Site', slug='site') + manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer') + device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type') + role = DeviceRole.objects.create(name='Device Role', slug='device-role') + cls.device = Device.objects.create( + name='Device', + site=cls.site, + device_type=device_type, + role=role, + ) + + def test_primary_mac_is_assigned_to_new_interface(self): + mac = MACAddress.objects.create(mac_address='00:11:22:33:44:55') + interface = Interface(device=self.device, name='Interface 1', primary_mac_address=mac) + interface.save() + + mac.refresh_from_db() + self.assertEqual(mac.assigned_object, interface) + + def test_primary_mac_is_not_reassigned_on_interface_update(self): + mac = MACAddress.objects.create(mac_address='00:11:22:33:44:55') + interface = Interface.objects.create(device=self.device, name='Interface 1') + mac.assigned_object = interface + mac.save() + # Detach (simulate the MAC having been moved off the interface). + mac.assigned_object = None + mac.save() + + interface.primary_mac_address = mac + interface.description = 'updated' + interface.save() + + mac.refresh_from_db() + # Updating an existing interface should not re-assign the MAC. + self.assertIsNone(mac.assigned_object) + + +class SyncCachedScopeFieldsSignalTestCase(TestCase): + """ + Verify dcim.signals.sync_cached_scope_fields recomputes cached scope fields on + Prefix, Cluster, and WirelessLAN when a Site or Location is modified. + """ + + def test_site_group_change_updates_prefix_cached_scope(self): + group_a = SiteGroup.objects.create(name='Group A', slug='group-a') + group_b = SiteGroup.objects.create(name='Group B', slug='group-b') + site = Site.objects.create(name='Site', slug='site', group=group_a) + prefix = Prefix.objects.create( + prefix='10.0.0.0/24', + scope_type=ContentType.objects.get_for_model(Site), + scope_id=site.pk, + ) + self.assertEqual(prefix._site_group, group_a) + + site.group = group_b + site.save() + + prefix.refresh_from_db() + self.assertEqual(prefix._site, site) + self.assertEqual(prefix._site_group, group_b) + + def test_location_site_change_updates_prefix_cached_scope(self): + site_a = Site.objects.create(name='Site A', slug='site-a') + site_b = Site.objects.create(name='Site B', slug='site-b') + location = Location.objects.create(name='Loc', slug='loc', site=site_a) + prefix = Prefix.objects.create( + prefix='10.0.0.0/24', + scope_type=ContentType.objects.get_for_model(Location), + scope_id=location.pk, + ) + self.assertEqual(prefix._site, site_a) + self.assertEqual(prefix._location, location) + + location.site = site_b + location.save() + + prefix.refresh_from_db() + self.assertEqual(prefix._location, location) + self.assertEqual(prefix._site, site_b) + + def test_signal_updates_cluster_and_wirelesslan_cached_scope(self): + # Lock down the explicit (Prefix, Cluster, WirelessLAN) tuple in the + # signal by exercising Cluster and WirelessLAN alongside Prefix. If a + # future change drops Cluster or WirelessLAN from that tuple, this test + # will catch it. + group_a = SiteGroup.objects.create(name='Group A', slug='group-a') + group_b = SiteGroup.objects.create(name='Group B', slug='group-b') + site = Site.objects.create(name='Site', slug='site', group=group_a) + cluster_type = ClusterType.objects.create(name='CT', slug='ct') + cluster = Cluster.objects.create(name='Cluster', type=cluster_type, scope=site) + wireless_lan = WirelessLAN.objects.create(ssid='LAN', scope=site) + + self.assertEqual(cluster._site_group, group_a) + self.assertEqual(wireless_lan._site_group, group_a) + + site.group = group_b + site.save() + + cluster.refresh_from_db() + wireless_lan.refresh_from_db() + self.assertEqual(cluster._site_group, group_b) + self.assertEqual(wireless_lan._site_group, group_b) + + def test_create_site_does_not_attempt_to_resync(self): + # Should not raise — newly-created sites have nothing to sync. + Site.objects.create(name='New Site', slug='new-site') + + +class CableSignalDirectHandlerTestCase(SimpleTestCase): + """ + Direct-call tests for dcim signal branches that are not reachable through normal + model operations (raw=True is set only by Django's loaddata pathway). + """ + + def test_update_connected_endpoints_raw_import_is_a_no_op(self): + cable = SimpleNamespace(_terminations_modified=True) + logger = MagicMock() + + with ( + patch.object(signals.logging, 'getLogger', return_value=logger), + patch.object(signals, 'CableTermination') as cabletermination_model, + patch.object(signals, 'create_cablepaths') as create_cablepaths, + patch.object(signals, 'rebuild_paths') as rebuild_paths, + ): + signals.update_connected_endpoints(instance=cable, created=True, raw=True) + + logger.debug.assert_called_once() + cabletermination_model.objects.filter.assert_not_called() + create_cablepaths.assert_not_called() + rebuild_paths.assert_not_called() + + def test_update_mac_address_interface_raw_import_is_a_no_op(self): + primary_mac = SimpleNamespace(save=MagicMock()) + interface = SimpleNamespace(primary_mac_address=primary_mac) + + signals.update_mac_address_interface(instance=interface, created=True, raw=True) + + primary_mac.save.assert_not_called() diff --git a/netbox/dcim/tests/test_tables.py b/netbox/dcim/tests/test_tables.py index 336aa186c..b21ab59c3 100644 --- a/netbox/dcim/tests/test_tables.py +++ b/netbox/dcim/tests/test_tables.py @@ -7,19 +7,19 @@ from utilities.testing import TableTestCases # -class RegionTableTest(TableTestCases.StandardTableTestCase): +class RegionTableTestCase(TableTestCases.StandardTableTestCase): table = RegionTable -class SiteGroupTableTest(TableTestCases.StandardTableTestCase): +class SiteGroupTableTestCase(TableTestCases.StandardTableTestCase): table = SiteGroupTable -class SiteTableTest(TableTestCases.StandardTableTestCase): +class SiteTableTestCase(TableTestCases.StandardTableTestCase): table = SiteTable -class LocationTableTest(TableTestCases.StandardTableTestCase): +class LocationTableTestCase(TableTestCases.StandardTableTestCase): table = LocationTable @@ -27,19 +27,23 @@ class LocationTableTest(TableTestCases.StandardTableTestCase): # Racks # -class RackRoleTableTest(TableTestCases.StandardTableTestCase): +class RackRoleTableTestCase(TableTestCases.StandardTableTestCase): table = RackRoleTable -class RackTypeTableTest(TableTestCases.StandardTableTestCase): +class RackGroupTableTestCase(TableTestCases.StandardTableTestCase): + table = RackGroupTable + + +class RackTypeTableTestCase(TableTestCases.StandardTableTestCase): table = RackTypeTable -class RackTableTest(TableTestCases.StandardTableTestCase): +class RackTableTestCase(TableTestCases.StandardTableTestCase): table = RackTable -class RackReservationTableTest(TableTestCases.StandardTableTestCase): +class RackReservationTableTestCase(TableTestCases.StandardTableTestCase): table = RackReservationTable @@ -47,11 +51,11 @@ class RackReservationTableTest(TableTestCases.StandardTableTestCase): # Device types # -class ManufacturerTableTest(TableTestCases.StandardTableTestCase): +class ManufacturerTableTestCase(TableTestCases.StandardTableTestCase): table = ManufacturerTable -class DeviceTypeTableTest(TableTestCases.StandardTableTestCase): +class DeviceTypeTableTestCase(TableTestCases.StandardTableTestCase): table = DeviceTypeTable @@ -59,15 +63,15 @@ class DeviceTypeTableTest(TableTestCases.StandardTableTestCase): # Module types # -class ModuleTypeProfileTableTest(TableTestCases.StandardTableTestCase): +class ModuleTypeProfileTableTestCase(TableTestCases.StandardTableTestCase): table = ModuleTypeProfileTable -class ModuleTypeTableTest(TableTestCases.StandardTableTestCase): +class ModuleTypeTableTestCase(TableTestCases.StandardTableTestCase): table = ModuleTypeTable -class ModuleTableTest(TableTestCases.StandardTableTestCase): +class ModuleTableTestCase(TableTestCases.StandardTableTestCase): table = ModuleTable def test_profile_column_available(self): @@ -78,15 +82,15 @@ class ModuleTableTest(TableTestCases.StandardTableTestCase): # Devices # -class DeviceRoleTableTest(TableTestCases.StandardTableTestCase): +class DeviceRoleTableTestCase(TableTestCases.StandardTableTestCase): table = DeviceRoleTable -class PlatformTableTest(TableTestCases.StandardTableTestCase): +class PlatformTableTestCase(TableTestCases.StandardTableTestCase): table = PlatformTable -class DeviceTableTest(TableTestCases.StandardTableTestCase): +class DeviceTableTestCase(TableTestCases.StandardTableTestCase): table = DeviceTable @@ -94,47 +98,47 @@ class DeviceTableTest(TableTestCases.StandardTableTestCase): # Device components # -class ConsolePortTableTest(TableTestCases.StandardTableTestCase): +class ConsolePortTableTestCase(TableTestCases.StandardTableTestCase): table = ConsolePortTable -class ConsoleServerPortTableTest(TableTestCases.StandardTableTestCase): +class ConsoleServerPortTableTestCase(TableTestCases.StandardTableTestCase): table = ConsoleServerPortTable -class PowerPortTableTest(TableTestCases.StandardTableTestCase): +class PowerPortTableTestCase(TableTestCases.StandardTableTestCase): table = PowerPortTable -class PowerOutletTableTest(TableTestCases.StandardTableTestCase): +class PowerOutletTableTestCase(TableTestCases.StandardTableTestCase): table = PowerOutletTable -class InterfaceTableTest(TableTestCases.StandardTableTestCase): +class InterfaceTableTestCase(TableTestCases.StandardTableTestCase): table = InterfaceTable -class FrontPortTableTest(TableTestCases.StandardTableTestCase): +class FrontPortTableTestCase(TableTestCases.StandardTableTestCase): table = FrontPortTable -class RearPortTableTest(TableTestCases.StandardTableTestCase): +class RearPortTableTestCase(TableTestCases.StandardTableTestCase): table = RearPortTable -class ModuleBayTableTest(TableTestCases.StandardTableTestCase): +class ModuleBayTableTestCase(TableTestCases.StandardTableTestCase): table = ModuleBayTable -class DeviceBayTableTest(TableTestCases.StandardTableTestCase): +class DeviceBayTableTestCase(TableTestCases.StandardTableTestCase): table = DeviceBayTable -class InventoryItemTableTest(TableTestCases.StandardTableTestCase): +class InventoryItemTableTestCase(TableTestCases.StandardTableTestCase): table = InventoryItemTable -class InventoryItemRoleTableTest(TableTestCases.StandardTableTestCase): +class InventoryItemRoleTableTestCase(TableTestCases.StandardTableTestCase): table = InventoryItemRoleTable @@ -142,21 +146,21 @@ class InventoryItemRoleTableTest(TableTestCases.StandardTableTestCase): # Connections # -class ConsoleConnectionTableTest(TableTestCases.StandardTableTestCase): +class ConsoleConnectionTableTestCase(TableTestCases.StandardTableTestCase): table = ConsoleConnectionTable queryset_sources = [ ('ConsoleConnectionsListView', ConsolePort.objects.filter(_path__is_complete=True)), ] -class PowerConnectionTableTest(TableTestCases.StandardTableTestCase): +class PowerConnectionTableTestCase(TableTestCases.StandardTableTestCase): table = PowerConnectionTable queryset_sources = [ ('PowerConnectionsListView', PowerPort.objects.filter(_path__is_complete=True)), ] -class InterfaceConnectionTableTest(TableTestCases.StandardTableTestCase): +class InterfaceConnectionTableTestCase(TableTestCases.StandardTableTestCase): table = InterfaceConnectionTable queryset_sources = [ ('InterfaceConnectionsListView', Interface.objects.filter(_path__is_complete=True)), @@ -167,19 +171,23 @@ class InterfaceConnectionTableTest(TableTestCases.StandardTableTestCase): # Cables # -class CableTableTest(TableTestCases.StandardTableTestCase): +class CableTableTestCase(TableTestCases.StandardTableTestCase): table = CableTable +class CableBundleTableTestCase(TableTestCases.StandardTableTestCase): + table = CableBundleTable + + # # Power # -class PowerPanelTableTest(TableTestCases.StandardTableTestCase): +class PowerPanelTableTestCase(TableTestCases.StandardTableTestCase): table = PowerPanelTable -class PowerFeedTableTest(TableTestCases.StandardTableTestCase): +class PowerFeedTableTestCase(TableTestCases.StandardTableTestCase): table = PowerFeedTable @@ -187,7 +195,7 @@ class PowerFeedTableTest(TableTestCases.StandardTableTestCase): # Virtual chassis # -class VirtualChassisTableTest(TableTestCases.StandardTableTestCase): +class VirtualChassisTableTestCase(TableTestCases.StandardTableTestCase): table = VirtualChassisTable @@ -195,7 +203,7 @@ class VirtualChassisTableTest(TableTestCases.StandardTableTestCase): # Virtual device contexts # -class VirtualDeviceContextTableTest(TableTestCases.StandardTableTestCase): +class VirtualDeviceContextTableTestCase(TableTestCases.StandardTableTestCase): table = VirtualDeviceContextTable @@ -203,5 +211,5 @@ class VirtualDeviceContextTableTest(TableTestCases.StandardTableTestCase): # MAC addresses # -class MACAddressTableTest(TableTestCases.StandardTableTestCase): +class MACAddressTableTestCase(TableTestCases.StandardTableTestCase): table = MACAddressTable diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index a9e715020..abbb222ab 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -622,11 +622,11 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'comments': 'New comments', } - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_list_rack_elevations(self): """ Test viewing the list of rack elevations. """ + self.add_permissions('dcim.view_rack') response = self.client.get(reverse('dcim:rack_elevation_list')) self.assertHttpStatus(response, 200) @@ -730,8 +730,8 @@ class DeviceTypeTestCase( 'is_full_depth': False, } - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_devicetype_consoleports(self): + self.add_permissions('dcim.view_devicetype', 'dcim.view_consoleporttemplate') devicetype = DeviceType.objects.first() console_ports = ( ConsolePortTemplate(device_type=devicetype, name='Console Port 1'), @@ -743,8 +743,8 @@ class DeviceTypeTestCase( url = reverse('dcim:devicetype_consoleports', kwargs={'pk': devicetype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_devicetype_consoleserverports(self): + self.add_permissions('dcim.view_devicetype', 'dcim.view_consoleserverporttemplate') devicetype = DeviceType.objects.first() console_server_ports = ( ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port 1'), @@ -756,8 +756,8 @@ class DeviceTypeTestCase( url = reverse('dcim:devicetype_consoleserverports', kwargs={'pk': devicetype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_devicetype_powerports(self): + self.add_permissions('dcim.view_devicetype', 'dcim.view_powerporttemplate') devicetype = DeviceType.objects.first() power_ports = ( PowerPortTemplate(device_type=devicetype, name='Power Port 1'), @@ -769,8 +769,8 @@ class DeviceTypeTestCase( url = reverse('dcim:devicetype_powerports', kwargs={'pk': devicetype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_devicetype_poweroutlets(self): + self.add_permissions('dcim.view_devicetype', 'dcim.view_poweroutlettemplate') devicetype = DeviceType.objects.first() power_outlets = ( PowerOutletTemplate(device_type=devicetype, name='Power Outlet 1'), @@ -782,8 +782,8 @@ class DeviceTypeTestCase( url = reverse('dcim:devicetype_poweroutlets', kwargs={'pk': devicetype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_devicetype_interfaces(self): + self.add_permissions('dcim.view_devicetype', 'dcim.view_interfacetemplate') devicetype = DeviceType.objects.first() interfaces = ( InterfaceTemplate(device_type=devicetype, name='Interface 1'), @@ -795,8 +795,8 @@ class DeviceTypeTestCase( url = reverse('dcim:devicetype_interfaces', kwargs={'pk': devicetype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_devicetype_rearports(self): + self.add_permissions('dcim.view_devicetype', 'dcim.view_rearporttemplate') devicetype = DeviceType.objects.first() rear_ports = ( RearPortTemplate(device_type=devicetype, name='Rear Port 1'), @@ -808,8 +808,12 @@ class DeviceTypeTestCase( url = reverse('dcim:devicetype_rearports', kwargs={'pk': devicetype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_devicetype_frontports(self): + self.add_permissions( + 'dcim.view_devicetype', + 'dcim.view_frontporttemplate', + 'dcim.view_rearporttemplate', + ) devicetype = DeviceType.objects.first() rear_ports = ( RearPortTemplate(device_type=devicetype, name='Rear Port 1'), @@ -832,8 +836,8 @@ class DeviceTypeTestCase( url = reverse('dcim:devicetype_frontports', kwargs={'pk': devicetype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_devicetype_modulebays(self): + self.add_permissions('dcim.view_devicetype', 'dcim.view_modulebaytemplate') devicetype = DeviceType.objects.first() module_bays = ( ModuleBayTemplate(device_type=devicetype, name='Module Bay 1'), @@ -845,8 +849,8 @@ class DeviceTypeTestCase( url = reverse('dcim:devicetype_modulebays', kwargs={'pk': devicetype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_devicetype_devicebays(self): + self.add_permissions('dcim.view_devicetype', 'dcim.view_devicebaytemplate') devicetype = DeviceType.objects.first() device_bays = ( DeviceBayTemplate(device_type=devicetype, name='Device Bay 1'), @@ -858,8 +862,8 @@ class DeviceTypeTestCase( url = reverse('dcim:devicetype_devicebays', kwargs={'pk': devicetype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_devicetype_inventoryitems(self): + self.add_permissions('dcim.view_devicetype', 'dcim.view_inventoryitemtemplate') devicetype = DeviceType.objects.first() inventory_items = ( DeviceBayTemplate(device_type=devicetype, name='Device Bay 1'), @@ -872,11 +876,11 @@ class DeviceTypeTestCase( url = reverse('dcim:devicetype_inventoryitems', kwargs={'pk': devicetype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_import_objects(self): """ Custom import test for YAML-based imports (versus CSV) """ + self.add_permissions('dcim.view_manufacturer', 'dcim.view_platform') IMPORT_DATA = """ manufacturer: Generic model: TEST-1000 @@ -1070,12 +1074,12 @@ inventory-items: ii1 = InventoryItemTemplate.objects.first() self.assertEqual(ii1.name, 'Inventory Item 1') - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_import_error_numbering(self): # Add all required permissions to the test user self.add_permissions( 'dcim.view_devicetype', 'dcim.add_devicetype', + 'dcim.view_manufacturer', 'dcim.add_consoleporttemplate', 'dcim.add_consoleserverporttemplate', 'dcim.add_powerporttemplate', @@ -1122,12 +1126,12 @@ module-bays: self.assertHttpStatus(response, 200) self.assertContains(response, "Record 2 module-bays[3].name: This field is required.") - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_import_nolist(self): # Add all required permissions to the test user self.add_permissions( 'dcim.view_devicetype', 'dcim.add_devicetype', + 'dcim.view_manufacturer', 'dcim.add_consoleporttemplate', 'dcim.add_consoleserverporttemplate', 'dcim.add_powerporttemplate', @@ -1158,12 +1162,12 @@ console-ports: {value} self.assertHttpStatus(response, 200) self.assertContains(response, "Record 1 console-ports: Must be a list.") - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_import_nodict(self): # Add all required permissions to the test user self.add_permissions( 'dcim.view_devicetype', 'dcim.add_devicetype', + 'dcim.view_manufacturer', 'dcim.add_consoleporttemplate', 'dcim.add_consoleserverporttemplate', 'dcim.add_powerporttemplate', @@ -1264,7 +1268,6 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): f"{module_types[0].id},test model", ) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_bulk_update_objects_with_permission(self): self.add_permissions( 'dcim.add_consoleporttemplate', @@ -1281,7 +1284,6 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): super().test_bulk_update_objects_with_permission() @tag('regression') - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[]) def test_bulk_import_objects_with_permission(self): self.add_permissions( 'dcim.add_consoleporttemplate', @@ -1303,7 +1305,6 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): # run base test super().test_bulk_import_objects_with_permission(post_import_callback=verify_module_type_profile) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[]) def test_bulk_import_objects_with_constrained_permission(self): self.add_permissions( 'dcim.add_consoleporttemplate', @@ -1318,8 +1319,8 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): super().test_bulk_import_objects_with_constrained_permission() - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_moduletype_consoleports(self): + self.add_permissions('dcim.view_moduletype', 'dcim.view_consoleporttemplate') moduletype = ModuleType.objects.first() console_ports = ( ConsolePortTemplate(module_type=moduletype, name='Console Port 1'), @@ -1331,8 +1332,8 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('dcim:moduletype_consoleports', kwargs={'pk': moduletype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_moduletype_consoleserverports(self): + self.add_permissions('dcim.view_moduletype', 'dcim.view_consoleserverporttemplate') moduletype = ModuleType.objects.first() console_server_ports = ( ConsoleServerPortTemplate(module_type=moduletype, name='Console Server Port 1'), @@ -1344,8 +1345,8 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('dcim:moduletype_consoleserverports', kwargs={'pk': moduletype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_moduletype_powerports(self): + self.add_permissions('dcim.view_moduletype', 'dcim.view_powerporttemplate') moduletype = ModuleType.objects.first() power_ports = ( PowerPortTemplate(module_type=moduletype, name='Power Port 1'), @@ -1357,8 +1358,8 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('dcim:moduletype_powerports', kwargs={'pk': moduletype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_moduletype_poweroutlets(self): + self.add_permissions('dcim.view_moduletype', 'dcim.view_poweroutlettemplate') moduletype = ModuleType.objects.first() power_outlets = ( PowerOutletTemplate(module_type=moduletype, name='Power Outlet 1'), @@ -1370,8 +1371,8 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('dcim:moduletype_poweroutlets', kwargs={'pk': moduletype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_moduletype_interfaces(self): + self.add_permissions('dcim.view_moduletype', 'dcim.view_interfacetemplate') moduletype = ModuleType.objects.first() interfaces = ( InterfaceTemplate(module_type=moduletype, name='Interface 1'), @@ -1383,8 +1384,8 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('dcim:moduletype_interfaces', kwargs={'pk': moduletype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_moduletype_rearports(self): + self.add_permissions('dcim.view_moduletype', 'dcim.view_rearporttemplate') moduletype = ModuleType.objects.first() rear_ports = ( RearPortTemplate(module_type=moduletype, name='Rear Port 1'), @@ -1396,8 +1397,12 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('dcim:moduletype_rearports', kwargs={'pk': moduletype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_moduletype_frontports(self): + self.add_permissions( + 'dcim.view_moduletype', + 'dcim.view_frontporttemplate', + 'dcim.view_rearporttemplate', + ) moduletype = ModuleType.objects.first() rear_ports = ( RearPortTemplate(module_type=moduletype, name='Rear Port 1'), @@ -1420,11 +1425,11 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('dcim:moduletype_frontports', kwargs={'pk': moduletype.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_import_objects(self): """ Custom import test for YAML-based imports (versus CSV) """ + self.add_permissions('dcim.view_manufacturer') IMPORT_DATA = """ manufacturer: Generic model: TEST-1000 @@ -2289,8 +2294,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'status': DeviceStatusChoices.STATUS_DECOMMISSIONING, } - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_device_consoleports(self): + self.add_permissions('dcim.view_device', 'dcim.view_consoleport') device = Device.objects.first() console_ports = ( ConsolePort(device=device, name='Console Port 1'), @@ -2302,8 +2307,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('dcim:device_consoleports', kwargs={'pk': device.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_device_consoleserverports(self): + self.add_permissions('dcim.view_device', 'dcim.view_consoleserverport') device = Device.objects.first() console_server_ports = ( ConsoleServerPort(device=device, name='Console Server Port 1'), @@ -2315,8 +2320,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('dcim:device_consoleserverports', kwargs={'pk': device.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_device_powerports(self): + self.add_permissions('dcim.view_device', 'dcim.view_powerport') device = Device.objects.first() power_ports = ( PowerPort(device=device, name='Power Port 1'), @@ -2328,8 +2333,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('dcim:device_powerports', kwargs={'pk': device.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_device_poweroutlets(self): + self.add_permissions('dcim.view_device', 'dcim.view_poweroutlet') device = Device.objects.first() power_outlets = ( PowerOutlet(device=device, name='Power Outlet 1'), @@ -2341,8 +2346,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('dcim:device_poweroutlets', kwargs={'pk': device.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_device_interfaces(self): + self.add_permissions('dcim.view_device', 'dcim.view_interface') device = Device.objects.first() interfaces = ( Interface(device=device, name='Interface 1'), @@ -2354,8 +2359,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('dcim:device_interfaces', kwargs={'pk': device.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_device_rearports(self): + self.add_permissions('dcim.view_device', 'dcim.view_rearport') device = Device.objects.first() rear_ports = ( RearPort(device=device, name='Rear Port 1'), @@ -2367,8 +2372,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('dcim:device_rearports', kwargs={'pk': device.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_device_frontports(self): + self.add_permissions('dcim.view_device', 'dcim.view_frontport', 'dcim.view_rearport') device = Device.objects.first() rear_ports = ( RearPort(device=device, name='Rear Port 1'), @@ -2391,8 +2396,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('dcim:device_frontports', kwargs={'pk': device.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_device_modulebays(self): + self.add_permissions('dcim.view_device', 'dcim.view_modulebay') device = Device.objects.first() ModuleBay.objects.create(device=device, name='Module Bay 1') ModuleBay.objects.create(device=device, name='Module Bay 2') @@ -2401,8 +2406,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('dcim:device_modulebays', kwargs={'pk': device.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_device_devicebays(self): + self.add_permissions('dcim.view_device', 'dcim.view_devicebay') device = Device.objects.first() device_bays = ( DeviceBay(device=device, name='Device Bay 1'), @@ -2414,8 +2419,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('dcim:device_devicebays', kwargs={'pk': device.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_device_inventory(self): + self.add_permissions('dcim.view_device', 'dcim.view_inventoryitem') device = Device.objects.first() inventory_items = ( InventoryItem(device=device, name='Inventory Item 1'), @@ -2504,7 +2509,6 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): self.assertContains(response, 'background-color: #aa00bb') self.assertNotContains(response, 'background-color: #111111') - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_bulk_import_duplicate_ids_error_message(self): device = Device.objects.first() csv_data = ( @@ -2513,7 +2517,12 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): f"{device.pk},Device Role 2", ) - self.add_permissions('dcim.add_device', 'dcim.change_device') + self.add_permissions( + 'dcim.view_device', + 'dcim.add_device', + 'dcim.change_device', + 'dcim.view_devicerole', + ) response = self.client.post( self._get_url('bulk_import'), { @@ -2615,15 +2624,26 @@ class ModuleTestCase( f"{modules[2].pk},offline,Serial 1", ) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_module_detail_includes_module_type_profile(self): + self.add_permissions( + 'dcim.view_module', + 'dcim.view_moduletype', + 'dcim.view_moduletypeprofile', + ) response = self.client.get(self._get_queryset().first().get_absolute_url()) self.assertContains(response, 'Module Type Profile 1') - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_module_component_replication(self): - self.add_permissions('dcim.add_module') + self.add_permissions( + 'dcim.view_module', + 'dcim.add_module', + 'dcim.view_moduletype', + 'dcim.view_device', + 'dcim.view_modulebay', + 'dcim.view_interface', + 'extras.view_tag', + ) # Add 5 InterfaceTemplates to a ModuleType module_type = ModuleType.objects.first() @@ -2654,9 +2674,15 @@ class ModuleTestCase( self.assertHttpStatus(self.client.post(**request), 302) self.assertEqual(Interface.objects.filter(device=device).count(), 5) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_module_bulk_replication(self): - self.add_permissions('dcim.add_module') + self.add_permissions( + 'dcim.view_module', + 'dcim.add_module', + 'dcim.view_moduletype', + 'dcim.view_device', + 'dcim.view_modulebay', + 'dcim.view_interface', + ) # Add 5 InterfaceTemplates to a ModuleType module_type = ModuleType.objects.first() @@ -2704,9 +2730,17 @@ class ModuleTestCase( self.assertEqual(Module.objects.count(), initial_count + len(csv_data) - 1) self.assertEqual(Interface.objects.filter(device=device).count(), 5) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_module_component_adoption(self): - self.add_permissions('dcim.add_module') + self.add_permissions( + 'dcim.view_module', + 'dcim.add_module', + 'dcim.view_moduletype', + 'dcim.view_device', + 'dcim.view_modulebay', + 'dcim.view_interface', + 'dcim.change_interface', + 'extras.view_tag', + ) interface_name = "Interface-1" @@ -2741,9 +2775,16 @@ class ModuleTestCase( # Check that the Interface now has a module self.assertIsNotNone(interface.module) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_module_bulk_adoption(self): - self.add_permissions('dcim.add_module') + self.add_permissions( + 'dcim.view_module', + 'dcim.add_module', + 'dcim.view_moduletype', + 'dcim.view_device', + 'dcim.view_modulebay', + 'dcim.view_interface', + 'dcim.change_interface', + ) interface_name = "Interface-1" @@ -2841,8 +2882,8 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase): f"{console_ports[2].pk},Console Port 9,New description9", ) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[]) def test_bulk_add_components_with_changelog_message(self): + self.add_permissions('dcim.view_consoleport', 'dcim.view_device') device1 = Device.objects.get(name='Device 1') device2 = create_test_device('Device 2') changelog_message = 'Bulk-created console ports' @@ -2885,8 +2926,13 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase): for objectchange in objectchanges: self.assertEqual(objectchange.message, changelog_message) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): + self.add_permissions( + 'dcim.view_consoleport', + 'dcim.view_consoleserverport', + 'dcim.view_cable', + 'dcim.view_device', + ) consoleport = ConsolePort.objects.first() consoleserverport = ConsoleServerPort.objects.create( device=consoleport.device, @@ -2950,8 +2996,13 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): f"{console_server_ports[2].pk},Console Server Port 9,New description 9", ) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): + self.add_permissions( + 'dcim.view_consoleserverport', + 'dcim.view_consoleport', + 'dcim.view_cable', + 'dcim.view_device', + ) consoleserverport = ConsoleServerPort.objects.first() consoleport = ConsolePort.objects.create( device=consoleserverport.device, @@ -3021,8 +3072,13 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): f"{power_ports[2].pk},Power Port 9,New description9", ) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): + self.add_permissions( + 'dcim.view_powerport', + 'dcim.view_poweroutlet', + 'dcim.view_cable', + 'dcim.view_device', + ) powerport = PowerPort.objects.first() poweroutlet = PowerOutlet.objects.create( device=powerport.device, @@ -3101,8 +3157,13 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase): f"{power_outlets[2].pk},Power Outlet 9,New description9", ) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): + self.add_permissions( + 'dcim.view_poweroutlet', + 'dcim.view_powerport', + 'dcim.view_cable', + 'dcim.view_device', + ) poweroutlet = PowerOutlet.objects.first() powerport = PowerPort.objects.first() Cable(a_terminations=[poweroutlet], b_terminations=[powerport]).save() @@ -3240,8 +3301,12 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): f"{interfaces[2].pk},Interface 9,New description9", ) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): + self.add_permissions( + 'dcim.view_interface', + 'dcim.view_cable', + 'dcim.view_device', + ) interface1, interface2 = Interface.objects.all()[:2] Cable(a_terminations=[interface1], b_terminations=[interface2]).save() @@ -3295,7 +3360,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): post_url = f'{self._get_url("bulk_rename")}?device_id={get_qs["device_id"]}' # Preview step: ensure 37 selected (not just one page) - data = {'_preview': '1', '_all': '1', 'find': 'eth', 'replace': 'xe'} + data = {'_preview': '1', '_all': '1', 'find': 'eth', 'replace': 'xe', 'field_names': ['name']} response = self.client.post(post_url, data=data) self.assertHttpStatus(response, 200) self.assertEqual(len(response.context['selected_objects']), 37) @@ -3308,7 +3373,9 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): pk_list = [str(pk) for pk in pk_list] # Apply step: include pk[] in the POST - apply_data = {'_apply': '1', '_all': '1', 'find': 'eth', 'replace': 'xe', 'pk': pk_list} + apply_data = { + '_apply': '1', '_all': '1', 'find': 'eth', 'replace': 'xe', 'pk': pk_list, 'field_names': ['name'], + } response = self.client.post(post_url, data=apply_data) # On success the view redirects back to the return URL @@ -3387,8 +3454,14 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase): f"{front_ports[2].pk},Front Port 9,New description9", ) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): + self.add_permissions( + 'dcim.view_frontport', + 'dcim.view_rearport', + 'dcim.view_interface', + 'dcim.view_cable', + 'dcim.view_device', + ) frontport = FrontPort.objects.first() interface = Interface.objects.create( device=frontport.device, @@ -3454,8 +3527,14 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase): f"{rear_ports[2].pk},Rear Port 9,New description9", ) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): + self.add_permissions( + 'dcim.view_rearport', + 'dcim.view_frontport', + 'dcim.view_interface', + 'dcim.view_cable', + 'dcim.view_device', + ) rearport = RearPort.objects.first() interface = Interface.objects.create( device=rearport.device, @@ -4108,8 +4187,13 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'comments': 'New comments', } - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_trace(self): + self.add_permissions( + 'dcim.view_powerfeed', + 'dcim.view_powerport', + 'dcim.view_cable', + 'dcim.view_device', + ) manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1') device_type = DeviceType.objects.create( manufacturer=manufacturer, model='Device Type 1', slug='device-type-1' @@ -4255,12 +4339,12 @@ class MACAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase): } @tag('regression') # Issue #20542 - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[]) def test_create_macaddress_via_quickadd(self): """ Test creating a MAC address via quick-add modal (e.g., from Interface form). Regression test for issue #20542 where form prefix was missing in POST handler. """ + self.add_permissions('dcim.view_macaddress', 'dcim.view_interface', 'extras.view_tag') obj_perm = ObjectPermission(name='Test permission', actions=['add']) obj_perm.save() obj_perm.users.add(self.user) diff --git a/netbox/dcim/tests/utils.py b/netbox/dcim/tests/utils.py index 575034201..5fe270b34 100644 --- a/netbox/dcim/tests/utils.py +++ b/netbox/dcim/tests/utils.py @@ -5,11 +5,11 @@ from dcim.models import * from dcim.utils import object_to_path_node __all__ = ( - 'CablePathTestCase', + 'BaseCablePathTestCase', ) -class CablePathTestCase(TestCase): +class BaseCablePathTestCase(TestCase): """ Base class for test cases for cable paths. """ diff --git a/netbox/dcim/ui/panels.py b/netbox/dcim/ui/panels.py index 29d0a1ed3..478ddf634 100644 --- a/netbox/dcim/ui/panels.py +++ b/netbox/dcim/ui/panels.py @@ -60,9 +60,9 @@ class RackPanel(panels.ObjectAttributesPanel): class RackWeightPanel(panels.ObjectAttributesPanel): - weight = attrs.NumericAttr('weight', unit_accessor='get_weight_unit_display') - max_weight = attrs.NumericAttr('max_weight', unit_accessor='get_weight_unit_display', label=_('Maximum weight')) - total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/rack/attrs/total_weight.html') + weight = attrs.WeightAttr('weight') + max_weight = attrs.WeightAttr('max_weight', label=_('Maximum weight')) + total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/attrs/total_weight.html') class RackRolePanel(panels.OrganizationalObjectPanel): @@ -137,7 +137,7 @@ class DeviceDeviceTypePanel(panels.ObjectAttributesPanel): class DeviceDimensionsPanel(panels.ObjectAttributesPanel): title = _('Dimensions') - total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/device/attrs/total_weight.html') + total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/attrs/total_weight.html') class DeviceRolePanel(panels.NestedGroupObjectPanel): @@ -155,7 +155,7 @@ class DeviceTypePanel(panels.ObjectAttributesPanel): height = attrs.TemplatedAttr('u_height', template_name='dcim/devicetype/attrs/height.html') exclude_from_utilization = attrs.BooleanAttr('exclude_from_utilization') full_depth = attrs.BooleanAttr('is_full_depth') - weight = attrs.NumericAttr('weight', unit_accessor='get_weight_unit_display') + weight = attrs.WeightAttr('weight') subdevice_role = attrs.ChoiceAttr('subdevice_role', label=_('Parent/child')) airflow = attrs.ChoiceAttr('airflow') front_image = attrs.ImageAttr('front_image') @@ -184,7 +184,7 @@ class ModuleTypePanel(panels.ObjectAttributesPanel): part_number = attrs.TextAttr('part_number') description = attrs.TextAttr('description') airflow = attrs.ChoiceAttr('airflow') - weight = attrs.NumericAttr('weight', unit_accessor='get_weight_unit_display') + weight = attrs.WeightAttr('weight') class PlatformPanel(panels.NestedGroupObjectPanel): diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 63666cce0..cc888597d 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1810,7 +1810,7 @@ class ModuleTypeConsolePortsView(ModuleTypeComponentsView): viewname = 'dcim:moduletype_consoleports' tab = ViewTab( label=_('Console Ports'), - badge=lambda obj: obj.consoleporttemplates.count(), + badge=lambda obj: obj.console_port_template_count, permission='dcim.view_consoleporttemplate', weight=530, hide_if_empty=True @@ -1825,7 +1825,7 @@ class ModuleTypeConsoleServerPortsView(ModuleTypeComponentsView): viewname = 'dcim:moduletype_consoleserverports' tab = ViewTab( label=_('Console Server Ports'), - badge=lambda obj: obj.consoleserverporttemplates.count(), + badge=lambda obj: obj.console_server_port_template_count, permission='dcim.view_consoleserverporttemplate', weight=540, hide_if_empty=True @@ -1840,7 +1840,7 @@ class ModuleTypePowerPortsView(ModuleTypeComponentsView): viewname = 'dcim:moduletype_powerports' tab = ViewTab( label=_('Power Ports'), - badge=lambda obj: obj.powerporttemplates.count(), + badge=lambda obj: obj.power_port_template_count, permission='dcim.view_powerporttemplate', weight=550, hide_if_empty=True @@ -1855,7 +1855,7 @@ class ModuleTypePowerOutletsView(ModuleTypeComponentsView): viewname = 'dcim:moduletype_poweroutlets' tab = ViewTab( label=_('Power Outlets'), - badge=lambda obj: obj.poweroutlettemplates.count(), + badge=lambda obj: obj.power_outlet_template_count, permission='dcim.view_poweroutlettemplate', weight=560, hide_if_empty=True @@ -1870,7 +1870,7 @@ class ModuleTypeInterfacesView(ModuleTypeComponentsView): viewname = 'dcim:moduletype_interfaces' tab = ViewTab( label=_('Interfaces'), - badge=lambda obj: obj.interfacetemplates.count(), + badge=lambda obj: obj.interface_template_count, permission='dcim.view_interfacetemplate', weight=500, hide_if_empty=True @@ -1885,7 +1885,7 @@ class ModuleTypeFrontPortsView(ModuleTypeComponentsView): viewname = 'dcim:moduletype_frontports' tab = ViewTab( label=_('Front Ports'), - badge=lambda obj: obj.frontporttemplates.count(), + badge=lambda obj: obj.front_port_template_count, permission='dcim.view_frontporttemplate', weight=510, hide_if_empty=True @@ -1900,7 +1900,7 @@ class ModuleTypeRearPortsView(ModuleTypeComponentsView): viewname = 'dcim:moduletype_rearports' tab = ViewTab( label=_('Rear Ports'), - badge=lambda obj: obj.rearporttemplates.count(), + badge=lambda obj: obj.rear_port_template_count, permission='dcim.view_rearporttemplate', weight=520, hide_if_empty=True @@ -1915,7 +1915,7 @@ class ModuleTypeModuleBaysView(ModuleTypeComponentsView): viewname = 'dcim:moduletype_modulebays' tab = ViewTab( label=_('Module Bays'), - badge=lambda obj: obj.modulebaytemplates.count(), + badge=lambda obj: obj.module_bay_template_count, permission='dcim.view_modulebaytemplate', weight=570, hide_if_empty=True @@ -2012,6 +2012,7 @@ class ConsolePortTemplateBulkEditView(generic.BulkEditView): @register_model_view(ConsolePortTemplate, 'bulk_rename', path='rename', detail=False) class ConsolePortTemplateBulkRenameView(generic.BulkRenameView): queryset = ConsolePortTemplate.objects.all() + rename_fields = ('name', 'label') @register_model_view(ConsolePortTemplate, 'bulk_delete', path='delete', detail=False) @@ -2052,6 +2053,7 @@ class ConsoleServerPortTemplateBulkEditView(generic.BulkEditView): @register_model_view(ConsoleServerPortTemplate, 'bulk_rename', detail=False) class ConsoleServerPortTemplateBulkRenameView(generic.BulkRenameView): queryset = ConsoleServerPortTemplate.objects.all() + rename_fields = ('name', 'label') @register_model_view(ConsoleServerPortTemplate, 'bulk_delete', path='delete', detail=False) @@ -2092,6 +2094,7 @@ class PowerPortTemplateBulkEditView(generic.BulkEditView): @register_model_view(PowerPortTemplate, 'bulk_rename', path='rename', detail=False) class PowerPortTemplateBulkRenameView(generic.BulkRenameView): queryset = PowerPortTemplate.objects.all() + rename_fields = ('name', 'label') @register_model_view(PowerPortTemplate, 'bulk_delete', path='delete', detail=False) @@ -2132,6 +2135,7 @@ class PowerOutletTemplateBulkEditView(generic.BulkEditView): @register_model_view(PowerOutletTemplate, 'bulk_rename', path='rename', detail=False) class PowerOutletTemplateBulkRenameView(generic.BulkRenameView): queryset = PowerOutletTemplate.objects.all() + rename_fields = ('name', 'label') @register_model_view(PowerOutletTemplate, 'bulk_delete', path='delete', detail=False) @@ -2172,6 +2176,7 @@ class InterfaceTemplateBulkEditView(generic.BulkEditView): @register_model_view(InterfaceTemplate, 'bulk_rename', path='rename', detail=False) class InterfaceTemplateBulkRenameView(generic.BulkRenameView): queryset = InterfaceTemplate.objects.all() + rename_fields = ('name', 'label') @register_model_view(InterfaceTemplate, 'bulk_delete', path='delete', detail=False) @@ -2212,6 +2217,7 @@ class FrontPortTemplateBulkEditView(generic.BulkEditView): @register_model_view(FrontPortTemplate, 'bulk_rename', path='rename', detail=False) class FrontPortTemplateBulkRenameView(generic.BulkRenameView): queryset = FrontPortTemplate.objects.all() + rename_fields = ('name', 'label') @register_model_view(FrontPortTemplate, 'bulk_delete', path='delete', detail=False) @@ -2252,6 +2258,7 @@ class RearPortTemplateBulkEditView(generic.BulkEditView): @register_model_view(RearPortTemplate, 'bulk_rename', path='rename', detail=False) class RearPortTemplateBulkRenameView(generic.BulkRenameView): queryset = RearPortTemplate.objects.all() + rename_fields = ('name', 'label') @register_model_view(RearPortTemplate, 'bulk_delete', path='delete', detail=False) @@ -2292,6 +2299,7 @@ class ModuleBayTemplateBulkEditView(generic.BulkEditView): @register_model_view(ModuleBayTemplate, 'bulk_rename', path='rename', detail=False) class ModuleBayTemplateBulkRenameView(generic.BulkRenameView): queryset = ModuleBayTemplate.objects.all() + rename_fields = ('name', 'label') @register_model_view(ModuleBayTemplate, 'bulk_delete', path='delete', detail=False) @@ -2332,6 +2340,7 @@ class DeviceBayTemplateBulkEditView(generic.BulkEditView): @register_model_view(DeviceBayTemplate, 'bulk_rename', path='rename', detail=False) class DeviceBayTemplateBulkRenameView(generic.BulkRenameView): queryset = DeviceBayTemplate.objects.all() + rename_fields = ('name', 'label') @register_model_view(DeviceBayTemplate, 'bulk_delete', path='delete', detail=False) @@ -2383,6 +2392,7 @@ class InventoryItemTemplateBulkEditView(generic.BulkEditView): @register_model_view(InventoryItemTemplate, 'bulk_rename', path='rename', detail=False) class InventoryItemTemplateBulkRenameView(generic.BulkRenameView): queryset = InventoryItemTemplate.objects.all() + rename_fields = ('name', 'label') @register_model_view(InventoryItemTemplate, 'bulk_delete', path='delete', detail=False) @@ -3067,6 +3077,7 @@ class ConsolePortBulkEditView(generic.BulkEditView): class ConsolePortBulkRenameView(generic.BulkRenameView): queryset = ConsolePort.objects.all() filterset = filtersets.ConsolePortFilterSet + rename_fields = ('name', 'label') @register_model_view(ConsolePort, 'bulk_disconnect', path='disconnect', detail=False) @@ -3156,6 +3167,7 @@ class ConsoleServerPortBulkEditView(generic.BulkEditView): class ConsoleServerPortBulkRenameView(generic.BulkRenameView): queryset = ConsoleServerPort.objects.all() filterset = filtersets.ConsoleServerPortFilterSet + rename_fields = ('name', 'label') @register_model_view(ConsoleServerPort, 'bulk_disconnect', path='disconnect', detail=False) @@ -3244,6 +3256,7 @@ class PowerPortBulkEditView(generic.BulkEditView): class PowerPortBulkRenameView(generic.BulkRenameView): queryset = PowerPort.objects.all() filterset = filtersets.PowerPortFilterSet + rename_fields = ('name', 'label') @register_model_view(PowerPort, 'bulk_disconnect', path='disconnect', detail=False) @@ -3331,6 +3344,7 @@ class PowerOutletBulkEditView(generic.BulkEditView): class PowerOutletBulkRenameView(generic.BulkRenameView): queryset = PowerOutlet.objects.all() filterset = filtersets.PowerOutletFilterSet + rename_fields = ('name', 'label') @register_model_view(PowerOutlet, 'bulk_disconnect', path='disconnect', detail=False) @@ -3507,6 +3521,7 @@ class InterfaceBulkEditView(generic.BulkEditView): class InterfaceBulkRenameView(generic.BulkRenameView): queryset = Interface.objects.all() filterset = filtersets.InterfaceFilterSet + rename_fields = ('name', 'label') @register_model_view(Interface, 'bulk_disconnect', path='disconnect', detail=False) @@ -3611,6 +3626,7 @@ class FrontPortBulkEditView(generic.BulkEditView): class FrontPortBulkRenameView(generic.BulkRenameView): queryset = FrontPort.objects.all() filterset = filtersets.FrontPortFilterSet + rename_fields = ('name', 'label') @register_model_view(FrontPort, 'bulk_disconnect', path='disconnect', detail=False) @@ -3711,6 +3727,7 @@ class RearPortBulkEditView(generic.BulkEditView): @register_model_view(RearPort, 'bulk_rename', path='rename', detail=False) class RearPortBulkRenameView(generic.BulkRenameView): queryset = RearPort.objects.all() + rename_fields = ('name', 'label') @register_model_view(RearPort, 'bulk_disconnect', path='disconnect', detail=False) @@ -3793,6 +3810,7 @@ class ModuleBayBulkEditView(generic.BulkEditView): class ModuleBayBulkRenameView(generic.BulkRenameView): queryset = ModuleBay.objects.all() filterset = filtersets.ModuleBayFilterSet + rename_fields = ('name', 'label') @register_model_view(ModuleBay, 'bulk_delete', path='delete', detail=False) @@ -3946,6 +3964,7 @@ class DeviceBayBulkEditView(generic.BulkEditView): class DeviceBayBulkRenameView(generic.BulkRenameView): queryset = DeviceBay.objects.all() filterset = filtersets.DeviceBayFilterSet + rename_fields = ('name', 'label') @register_model_view(DeviceBay, 'bulk_delete', path='delete', detail=False) @@ -4015,6 +4034,7 @@ class InventoryItemBulkEditView(generic.BulkEditView): class InventoryItemBulkRenameView(generic.BulkRenameView): queryset = InventoryItem.objects.all() filterset = filtersets.InventoryItemFilterSet + rename_fields = ('name', 'label') @register_model_view(InventoryItem, 'bulk_delete', path='delete', detail=False) diff --git a/netbox/extras/api/mixins.py b/netbox/extras/api/mixins.py index 6a3713be4..e8b637ae3 100644 --- a/netbox/extras/api/mixins.py +++ b/netbox/extras/api/mixins.py @@ -1,4 +1,5 @@ -from django.utils.translation import gettext as _ +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import OpenApiResponse, OpenApiTypes, extend_schema from rest_framework.decorators import action from rest_framework.renderers import JSONRenderer from rest_framework.response import Response @@ -8,7 +9,7 @@ from extras.models import ConfigTemplate from netbox.api.authentication import TokenWritePermission from netbox.api.renderers import TextRenderer -from .serializers import ConfigTemplateSerializer +from .serializers import RenderConfigInputSerializer, RenderedConfigSerializer __all__ = ( 'ConfigContextQuerySetMixin', @@ -56,12 +57,11 @@ class ConfigTemplateRenderMixin: if request.accepted_renderer.format == 'txt': return Response(output) - template_serializer = ConfigTemplateSerializer(configtemplate, nested=True, context={'request': request}) - - return Response({ - 'configtemplate': template_serializer.data, - 'content': output - }) + serializer = RenderedConfigSerializer( + instance={'configtemplate': configtemplate, 'content': output}, + context={'request': request}, + ) + return Response(serializer.data) class RenderConfigMixin(ConfigTemplateRenderMixin): @@ -75,6 +75,26 @@ class RenderConfigMixin(ConfigTemplateRenderMixin): return [TokenWritePermission()] return super().get_permissions() + @extend_schema( + request=RenderConfigInputSerializer, + responses={ + 200: OpenApiResponse( + response=RenderedConfigSerializer, + description=_( + "The rendered config template. When the client requests `text/plain`, the raw " + "rendered content is returned in place of the JSON object." + ), + ), + 400: OpenApiResponse( + response=OpenApiTypes.OBJECT, + description=_("No config template could be resolved for this object."), + ), + 500: OpenApiResponse( + response=OpenApiTypes.OBJECT, + description=_("An error occurred while rendering the config template."), + ), + }, + ) @action(detail=True, methods=['post'], url_path='render-config', renderer_classes=[JSONRenderer, TextRenderer]) def render_config(self, request, pk): """ diff --git a/netbox/extras/api/serializers_/configtemplates.py b/netbox/extras/api/serializers_/configtemplates.py index 4bf4608ac..7ac542f5d 100644 --- a/netbox/extras/api/serializers_/configtemplates.py +++ b/netbox/extras/api/serializers_/configtemplates.py @@ -1,3 +1,7 @@ +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.extensions import OpenApiSerializerExtension +from rest_framework import serializers + from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer from extras.models import ConfigTemplate from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer @@ -6,6 +10,8 @@ from users.api.serializers_.mixins import OwnerMixin __all__ = ( 'ConfigTemplateSerializer', + 'RenderConfigInputSerializer', + 'RenderedConfigSerializer', ) @@ -32,3 +38,41 @@ class ConfigTemplateSerializer( 'data_file', 'auto_sync_enabled', 'data_synced', 'owner', 'tags', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class RenderConfigInputSerializer(serializers.Serializer): + """ + Describes the request body for the device/VM /render-config/ endpoints. Any additional keys + supplied are passed through as context variables to the rendered template. + """ + config_template_id = serializers.IntegerField( + required=False, + help_text=_( + "Optional ID of the ConfigTemplate to render. If omitted, the object's assigned " + "config template is used." + ) + ) + + +class RenderConfigInputSerializerExtension(OpenApiSerializerExtension): + """ + Augment the schema for `RenderConfigInputSerializer` to advertise that additional keys + (beyond `config_template_id`) are accepted and forwarded as template context variables. + """ + target_class = 'extras.api.serializers_.configtemplates.RenderConfigInputSerializer' + + def map_serializer(self, auto_schema, direction): + schema = auto_schema._map_serializer(self.target, direction, bypass_extensions=True) + schema['additionalProperties'] = True + return schema + + +class RenderedConfigSerializer(serializers.Serializer): + """ + Describes the JSON response returned by the /render-config/ and /render/ endpoints. + """ + configtemplate = ConfigTemplateSerializer(read_only=True, nested=True) + content = serializers.CharField( + read_only=True, + help_text=_("The rendered template output.") + ) diff --git a/netbox/extras/api/serializers_/tableconfigs.py b/netbox/extras/api/serializers_/tableconfigs.py index 7a4fb7b2a..a23e4e78f 100644 --- a/netbox/extras/api/serializers_/tableconfigs.py +++ b/netbox/extras/api/serializers_/tableconfigs.py @@ -1,14 +1,14 @@ from core.models import ObjectType from extras.models import TableConfig from netbox.api.fields import ContentTypeField -from netbox.api.serializers import ValidatedModelSerializer +from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer __all__ = ( 'TableConfigSerializer', ) -class TableConfigSerializer(ValidatedModelSerializer): +class TableConfigSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer): object_type = ContentTypeField( queryset=ObjectType.objects.all() ) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 6f57f1957..faf8a9bec 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -1,7 +1,7 @@ from django.http import Http404 from django.shortcuts import get_object_or_404 -from django_rq.queues import get_connection -from drf_spectacular.utils import extend_schema, extend_schema_view +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import OpenApiResponse, OpenApiTypes, extend_schema, extend_schema_view from rest_framework import status from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied @@ -11,7 +11,6 @@ from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework.routers import APIRootView from rest_framework.viewsets import ModelViewSet -from rq import Worker from extras import filtersets from extras.jobs import ScriptJob @@ -24,6 +23,7 @@ from netbox.api.viewsets import BaseViewSet, NetBoxModelViewSet from netbox.api.viewsets.mixins import ObjectValidationMixin from utilities.exceptions import RQWorkerNotRunningException from utilities.request import copy_safe_request +from utilities.rqworker import any_workers_for_queue from . import serializers from .mixins import ConfigTemplateRenderMixin @@ -246,11 +246,28 @@ class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxMo return [TokenWritePermission()] return super().get_permissions() + @extend_schema( + request=OpenApiTypes.OBJECT, + responses={ + 200: OpenApiResponse( + response=serializers.RenderedConfigSerializer, + description=_( + "The rendered config template. When the client requests `text/plain`, the raw " + "rendered content is returned in place of the JSON object." + ), + ), + 500: OpenApiResponse( + response=OpenApiTypes.OBJECT, + description=_("An error occurred while rendering the config template."), + ), + }, + ) @action(detail=True, methods=['post'], renderer_classes=[JSONRenderer, TextRenderer]) def render(self, request, pk): """ - Render a ConfigTemplate using the context data provided (if any). If the client requests "text/plain" data, - return the raw rendered content, rather than serialized JSON. + Render a ConfigTemplate using the context data provided (if any). The request body should be a + mapping of context variables to make available to the template. If the client requests "text/plain" + data, return the raw rendered content, rather than serialized JSON. """ # Override restrict() on the default queryset to enforce the render & view actions self.queryset = self.queryset.model.objects.restrict(request.user, 'render').restrict(request.user, 'view') @@ -326,7 +343,7 @@ class ScriptViewSet(ModelViewSet): ) # Check that at least one RQ worker is running - if not Worker.count(get_connection('default')): + if not any_workers_for_queue('default'): raise RQWorkerNotRunningException() if input_serializer.is_valid(): diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index d88fdb3c3..bae808f24 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -1,3 +1,5 @@ +from jinja2 import ChainableUndefined, DebugUndefined, StrictUndefined, Undefined + from core.events import * from extras.choices import LogLevelChoices @@ -32,11 +34,47 @@ WEBHOOK_EVENT_TYPES = { JOB_ERRORED: 'job_ended', } -# Jinja environment parameters which support path imports -JINJA_ENV_PARAMS_WITH_PATH_IMPORT = ( - 'undefined', - 'finalize', -) +# Allowed Jinja2 environment parameters and their permitted values. +# Only keys listed here may appear in a template's environment_params. +# None = any JSON-serializable value accepted (scalars, booleans, etc.) +# dict = only dict keys accepted; dict values are the resolved Python objects +# +# Note: 'finalize' is intentionally absent. It is deprecated and handled as a +# legacy carve-out in RenderTemplateMixin (blocked from new use, but existing +# stored values continue to resolve via import_string at render time). +JINJA_ENV_PARAMS_ALLOWED = { + # Boolean / scalar params (accept any JSON-serializable value) + 'auto_reload': None, + 'autoescape': None, + 'cache_size': None, + 'enable_async': None, + 'keep_trailing_newline': None, + 'lstrip_blocks': None, + 'optimized': None, + 'trim_blocks': None, + # String params (template syntax delimiters) + 'block_start_string': None, + 'block_end_string': None, + 'comment_start_string': None, + 'comment_end_string': None, + 'line_comment_prefix': None, + 'line_statement_prefix': None, + 'newline_sequence': None, + 'variable_start_string': None, + 'variable_end_string': None, + # Mapped params (value must be a key in the dict; resolved to the dict value) + 'undefined': { + 'jinja2.ChainableUndefined': ChainableUndefined, + 'jinja2.DebugUndefined': DebugUndefined, + 'jinja2.StrictUndefined': StrictUndefined, + 'jinja2.Undefined': Undefined, + }, + # Excluded (dangerous — accept callables or trigger imports): + # 'bytecode_cache' — accepts arbitrary object + # 'extensions' — Jinja2 internally calls import_string() on string entries + # 'finalize' — deprecated; legacy carve-out in RenderTemplateMixin + # 'loader' — accepts arbitrary object +} # Dashboard DEFAULT_DASHBOARD = [ diff --git a/netbox/extras/forms/scripts.py b/netbox/extras/forms/scripts.py index 2ef5e7a35..b646c397d 100644 --- a/netbox/extras/forms/scripts.py +++ b/netbox/extras/forms/scripts.py @@ -90,6 +90,15 @@ class ScriptFileForm(ManagedFileForm): raise forms.ValidationError( _("Error loading script: {error}").format(error=e) ) + elif data_file := self.cleaned_data.get('data_file'): + # Validate scripts synced from a data source as well, to avoid creating a broken + # script module that cannot be loaded or corrected + try: + validate_script_content(data_file.data, data_file.path) + except Exception as e: + raise forms.ValidationError( + _("Error loading script: {error}").format(error=e) + ) return self.cleaned_data diff --git a/netbox/extras/graphql/filters.py b/netbox/extras/graphql/filters.py index 30de07f05..afc1ff8f9 100644 --- a/netbox/extras/graphql/filters.py +++ b/netbox/extras/graphql/filters.py @@ -1,15 +1,16 @@ +from datetime import datetime from typing import TYPE_CHECKING, Annotated import strawberry import strawberry_django from django.db.models import Q, QuerySet from strawberry.scalars import ID -from strawberry_django import BaseFilterLookup, FilterLookup, StrFilterLookup +from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup, StrFilterLookup from extras import models from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin from netbox.graphql.filter_mixins import SyncedDataFilterMixin -from netbox.graphql.filters import ChangeLoggedModelFilter, PrimaryModelFilter +from netbox.graphql.filters import BaseModelFilter, ChangeLoggedModelFilter, PrimaryModelFilter if TYPE_CHECKING: from core.graphql.filters import ContentTypeFilter @@ -41,8 +42,10 @@ __all__ = ( 'ExportTemplateFilter', 'ImageAttachmentFilter', 'JournalEntryFilter', + 'NotificationFilter', 'NotificationGroupFilter', 'SavedFilterFilter', + 'SubscriptionFilter', 'TableConfigFilter', 'TagFilter', 'WebhookFilter', @@ -108,8 +111,8 @@ class ConfigContextFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter): @strawberry_django.filter_type(models.ConfigContextProfile, lookups=True) class ConfigContextProfileFilter(SyncedDataFilterMixin, PrimaryModelFilter): - name: StrFilterLookup[str] = strawberry_django.filter_field() - description: StrFilterLookup[str] = strawberry_django.filter_field() + name: StrFilterLookup[str] | None = strawberry_django.filter_field() + description: StrFilterLookup[str] | None = strawberry_django.filter_field() tags: Annotated['TagFilter', strawberry.lazy('extras.graphql.filters')] | None = strawberry_django.filter_field() @@ -291,6 +294,21 @@ class JournalEntryFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedM comments: StrFilterLookup[str] | None = strawberry_django.filter_field() +@strawberry_django.filter_type(models.Notification, lookups=True) +class NotificationFilter(BaseModelFilter): + created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() + read: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() + user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() + user_id: ID | None = strawberry_django.filter_field() + object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + object_type_id: ID | None = strawberry_django.filter_field() + object_id: ID | None = strawberry_django.filter_field() + object_repr: StrFilterLookup[str] | None = strawberry_django.filter_field() + event_type: StrFilterLookup[str] | None = strawberry_django.filter_field() + + @strawberry_django.filter_type(models.NotificationGroup, lookups=True) class NotificationGroupFilter(ChangeLoggedModelFilter): name: StrFilterLookup[str] | None = strawberry_django.filter_field() @@ -316,6 +334,18 @@ class SavedFilterFilter(ChangeLoggedModelFilter): ) +@strawberry_django.filter_type(models.Subscription, lookups=True) +class SubscriptionFilter(BaseModelFilter): + created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() + user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() + user_id: ID | None = strawberry_django.filter_field() + object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + object_type_id: ID | None = strawberry_django.filter_field() + object_id: ID | None = strawberry_django.filter_field() + + @strawberry_django.filter_type(models.TableConfig, lookups=True) class TableConfigFilter(ChangeLoggedModelFilter): name: StrFilterLookup[str] | None = strawberry_django.filter_field() @@ -374,7 +404,9 @@ class EventRuleFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedMode action_type: BaseFilterLookup[Annotated['EventRuleActionEnum', strawberry.lazy('extras.graphql.enums')]] | None = ( strawberry_django.filter_field() ) - action_object_type: StrFilterLookup[str] | None = strawberry_django.filter_field() + action_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) action_object_type_id: ID | None = strawberry_django.filter_field() action_object_id: ID | None = strawberry_django.filter_field() action_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( diff --git a/netbox/extras/graphql/mixins.py b/netbox/extras/graphql/mixins.py index 96f9719a3..3a958d8bc 100644 --- a/netbox/extras/graphql/mixins.py +++ b/netbox/extras/graphql/mixins.py @@ -4,6 +4,9 @@ import strawberry import strawberry_django from strawberry.types import Info +from extras.models import ImageAttachment, JournalEntry +from utilities.querysets import RestrictedPrefetch + __all__ = ( 'ConfigContextMixin', 'ContactsMixin', @@ -50,16 +53,24 @@ class CustomFieldsMixin: @strawberry.type class ImageAttachmentsMixin: - @strawberry_django.field - def image_attachments(self, info: Info) -> list[Annotated['ImageAttachmentType', strawberry.lazy('.types')]]: - return self.images.restrict(info.context.request.user, 'view') + @strawberry_django.field( + prefetch_related=lambda info: RestrictedPrefetch( + 'images', info.context.request.user, 'view', queryset=ImageAttachment.objects.all() + ), + ) + def image_attachments(self) -> list[Annotated['ImageAttachmentType', strawberry.lazy('.types')]]: + return self.images.all() @strawberry.type class JournalEntriesMixin: - @strawberry_django.field - def journal_entries(self, info: Info) -> list[Annotated['JournalEntryType', strawberry.lazy('.types')]]: + @strawberry_django.field( + prefetch_related=lambda info: RestrictedPrefetch( + 'journal_entries', info.context.request.user, 'view', queryset=JournalEntry.objects.all() + ), + ) + def journal_entries(self) -> list[Annotated['JournalEntryType', strawberry.lazy('.types')]]: return self.journal_entries.all() diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index bed77aad0..5943e938e 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -161,7 +161,7 @@ class JournalEntryType(CustomFieldsMixin, TagsMixin, ObjectType): @strawberry_django.type( models.Notification, - # filters=NotificationFilter + filters=NotificationFilter, pagination=True ) class NotificationType(ObjectType): @@ -190,7 +190,7 @@ class SavedFilterType(OwnerMixin, ObjectType): @strawberry_django.type( models.Subscription, - # filters=NotificationFilter + filters=SubscriptionFilter, pagination=True ) class SubscriptionType(ObjectType): @@ -204,6 +204,7 @@ class SubscriptionType(ObjectType): pagination=True ) class TableConfigType(ObjectType): + object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None diff --git a/netbox/extras/management/commands/runscript.py b/netbox/extras/management/commands/runscript.py index 71f398c18..ef3a46fac 100644 --- a/netbox/extras/management/commands/runscript.py +++ b/netbox/extras/management/commands/runscript.py @@ -68,7 +68,7 @@ class Command(BaseCommand): 'info': logging.INFO, 'warning': logging.WARNING, }[loglevel]) - except KeyError: + except KeyError: # pragma: no cover raise CommandError(f"Invalid log level: {loglevel}") # Initialize the script form diff --git a/netbox/extras/managers.py b/netbox/extras/managers.py index bd8ccf1ca..450af466e 100644 --- a/netbox/extras/managers.py +++ b/netbox/extras/managers.py @@ -1,10 +1,11 @@ from django.db import router from django.db.models import signals -from taggit.managers import _TaggableManager +from taggit.managers import TaggableManager, _TaggableManager from taggit.utils import require_instance_manager __all__ = ( 'NetBoxTaggableManager', + 'NetBoxTaggableManagerField', ) @@ -65,3 +66,29 @@ class NetBoxTaggableManager(_TaggableManager): pk_set=new_ids, using=db, ) + + +class NetBoxTaggableManagerField(TaggableManager): + """ + Subclass of taggit's TaggableManager that interpolates `%(app_label)s` and `%(class)s` in + `related_name`. taggit's contribute_to_class() bypasses Django's RelatedField, which is what + normally performs this substitution, so without this two taggable models that share a class + name (e.g. from different plugins) collide on Tag's reverse accessor. + """ + def contribute_to_class(self, cls, name): + super().contribute_to_class(cls, name) + if not cls._meta.abstract and self.remote_field.related_name: + self.remote_field.related_name = self.remote_field.related_name % { + 'class': cls.__name__.lower(), + 'app_label': cls._meta.app_label.lower(), + } + + def deconstruct(self): + # Emit the upstream taggit path and omit related_name so existing migrations remain + # equivalent and no AlterField is produced for every TagsMixin consumer. related_name + # has no effect on the database schema; it is reapplied on model load. Only safe while + # this subclass adds no field attributes that affect schema — if that changes, restore + # the real path so migrations capture the diff. + name, _path, args, kwargs = super().deconstruct() + kwargs.pop('related_name', None) + return name, 'taggit.managers.TaggableManager', args, kwargs diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index e7e8d2b84..011afa42d 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -972,7 +972,7 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, OwnerMixin, Chang extra_choice_values = set() if self.base_choices: - valid_choice_values.update(CHOICE_SETS.get(self.base_choices).values()) + valid_choice_values.update(value for value, _ in CHOICE_SETS.get(self.base_choices)) if self.extra_choices: for value, _label in self.extra_choices: @@ -1031,7 +1031,7 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, OwnerMixin, Chang def save(self, *args, **kwargs): # Sort choices if alphabetical ordering is enforced - if self.order_alphabetically: + if self.order_alphabetically and self.extra_choices: self.extra_choices = sorted(self.extra_choices, key=lambda x: x[0]) return super().save(*args, **kwargs) diff --git a/netbox/extras/models/mixins.py b/netbox/extras/models/mixins.py index 805916d9f..7eee9ac3c 100644 --- a/netbox/extras/models/mixins.py +++ b/netbox/extras/models/mixins.py @@ -4,6 +4,7 @@ import os import sys from collections import defaultdict +from django.core.exceptions import ValidationError from django.core.files.storage import storages from django.db import models from django.http import HttpResponse @@ -11,7 +12,7 @@ from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ from core.models import ObjectType -from extras.constants import DEFAULT_MIME_TYPE, JINJA_ENV_PARAMS_WITH_PATH_IMPORT +from extras.constants import DEFAULT_MIME_TYPE, JINJA_ENV_PARAMS_ALLOWED from extras.utils import filename_from_model, filename_from_object from utilities.jinja2 import render_jinja2 @@ -134,14 +135,84 @@ class RenderTemplateMixin(models.Model): return _context + def clean(self): + super().clean() + + params = self.environment_params or {} + for key, value in params.items(): + # finalize is deprecated: block new use but preserve existing stored values + if key == 'finalize': + raise ValidationError({ + 'environment_params': _( + 'The "{key}" parameter is deprecated and may not be set on new or modified templates.' + ).format(key=key) + }) + if key not in JINJA_ENV_PARAMS_ALLOWED: + raise ValidationError({ + 'environment_params': _( + '"{key}" is not a permitted Jinja2 environment parameter.' + ).format(key=key) + }) + allowed = JINJA_ENV_PARAMS_ALLOWED[key] + if type(allowed) is dict: + if value not in allowed: + raise ValidationError({ + 'environment_params': _( + 'Invalid value "{value}" for parameter "{key}". ' + 'Allowed values are: {allowed}' + ).format( + value=value, + key=key, + allowed=', '.join(sorted(allowed.keys())) + ) + }) + + @staticmethod + def _filter_environment_params(params): + """ + Return a copy of params with only permitted keys. Keys not in the allowlist are + stripped, except 'finalize' which is a deprecated legacy carve-out. + """ + return { + key: value for key, value in params.items() + if key in JINJA_ENV_PARAMS_ALLOWED or key == 'finalize' + } + + @staticmethod + def _resolve_mapped_params(params): + """ + Resolve allowlisted params that have a value mapping (e.g. undefined class names) + to their Python objects via direct dict lookup. Returns a new dict with resolved values; + unresolved params are passed through unchanged. + """ + resolved = {} + for name, value in params.items(): + allowed = JINJA_ENV_PARAMS_ALLOWED.get(name) + if type(allowed) is dict and value in allowed: + resolved[name] = allowed[value] + else: + resolved[name] = value + return resolved + + @staticmethod + def _resolve_finalize(params): + """ + Legacy carve-out: resolve the deprecated 'finalize' parameter via import_string(). + Existing templates with finalize continue to work; new use is blocked by clean(). + """ + if 'finalize' in params and type(params['finalize']) is str: + return {**params, 'finalize': import_string(params['finalize'])} + return params + def get_environment_params(self): """ - Pre-processing of any defined Jinja environment parameters (e.g. to support path resolution). + Pre-processing of any defined Jinja environment parameters. """ - params = self.environment_params or {} - for name, value in params.items(): - if name in JINJA_ENV_PARAMS_WITH_PATH_IMPORT and type(value) is str: - params[name] = import_string(value) + # Shallow-copy so resolved values don't replace the strings on the model field. + params = dict(self.environment_params or {}) + params = self._filter_environment_params(params) + params = self._resolve_mapped_params(params) + params = self._resolve_finalize(params) return params def render(self, context=None, queryset=None): diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index f3b8fae61..14cae8d9d 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -658,7 +658,7 @@ class TableConfig(CloningMixin, ChangeLoggedModel): table = self.table_class([]) # Validate ordering columns - for name in self.ordering: + for name in self.ordering or []: if name.startswith('-'): name = name[1:] # Strip leading hyphen if name not in table.columns: diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index f286670da..95ca00d56 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -234,10 +234,12 @@ class ObjectVar(ScriptVariable): :param null_option: The label to use as a "null" selection option (optional) :param selector: Include an advanced object selection widget to assist the user in identifying the desired object (optional) + :param quick_add: Include a widget to quickly create a new related object for assignment. (optional) """ form_field = DynamicModelChoiceField - def __init__(self, model, query_params=None, context=None, null_option=None, selector=False, *args, **kwargs): + def __init__(self, model, query_params=None, context=None, null_option=None, selector=False, quick_add=False, + *args, **kwargs): super().__init__(*args, **kwargs) self.field_attrs.update({ @@ -246,6 +248,7 @@ class ObjectVar(ScriptVariable): 'context': context, 'null_option': null_option, 'selector': selector, + 'quick_add': quick_add, }) diff --git a/netbox/extras/tests/query_counts.json b/netbox/extras/tests/query_counts.json new file mode 100644 index 000000000..a6807bd29 --- /dev/null +++ b/netbox/extras/tests/query_counts.json @@ -0,0 +1,37 @@ +{ + "bookmark:api_list_objects": 12, + "bookmark:list_objects_with_permission": 17, + "configcontext:api_list_objects": 22, + "configcontext:list_objects_with_permission": 16, + "configcontextprofile:api_list_objects": 12, + "configcontextprofile:list_objects_with_permission": 19, + "configtemplate:api_list_objects": 10, + "configtemplate:list_objects_with_permission": 17, + "customfield:api_list_objects": 10, + "customfield:list_objects_with_permission": 19, + "customfieldchoiceset:api_list_objects": 9, + "customfieldchoiceset:list_objects_with_permission": 16, + "customlink:api_list_objects": 10, + "customlink:list_objects_with_permission": 18, + "eventrule:api_list_objects": 16, + "eventrule:list_objects_with_permission": 23, + "exporttemplate:api_list_objects": 10, + "exporttemplate:list_objects_with_permission": 19, + "imageattachment:api_list_objects": 11, + "imageattachment:list_objects_with_permission": 21, + "journalentry:api_list_objects": 15, + "journalentry:list_objects_with_permission": 24, + "notification:api_list_objects": 12, + "notificationgroup:api_list_objects": 11, + "notificationgroup:list_objects_with_permission": 18, + "savedfilter:api_list_objects": 10, + "savedfilter:list_objects_with_permission": 19, + "subscription:api_list_objects": 12, + "tableconfig:api_list_objects": 11, + "tableconfig:list_objects_with_permission": 19, + "tag:api_list_objects": 10, + "tag:list_objects_with_permission": 18, + "taggeditem:api_list_objects": 12, + "webhook:api_list_objects": 13, + "webhook:list_objects_with_permission": 20 +} diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 22fea092c..df94cbe0d 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -19,10 +19,11 @@ from extras.scripts import BooleanVar, IntegerVar, StringVar from extras.scripts import Script as PythonClass from users.constants import TOKEN_PREFIX from users.models import Group, Token, User +from utilities.tables import get_table_for_model from utilities.testing import APITestCase, APIViewTestCases -class AppTest(APITestCase): +class AppTestCase(APITestCase): def test_root(self): @@ -32,7 +33,7 @@ class AppTest(APITestCase): self.assertEqual(response.status_code, 200) -class WebhookTest(APIViewTestCases.APIViewTestCase): +class WebhookTestCase(APIViewTestCases.APIViewTestCase): model = Webhook brief_fields = ['description', 'display', 'id', 'name', 'url'] create_data = [ @@ -74,7 +75,7 @@ class WebhookTest(APIViewTestCases.APIViewTestCase): Webhook.objects.bulk_create(webhooks) -class EventRuleTest(APIViewTestCases.APIViewTestCase): +class EventRuleTestCase(APIViewTestCases.APIViewTestCase): model = EventRule brief_fields = ['description', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -152,7 +153,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase): ] -class CustomFieldTest(APIViewTestCases.APIViewTestCase): +class CustomFieldTestCase(APIViewTestCases.APIViewTestCase): model = CustomField brief_fields = ['description', 'display', 'id', 'name', 'url'] create_data = [ @@ -204,7 +205,7 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase): cf.object_types.add(site_ct) -class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase): +class CustomFieldChoiceSetTestCase(APIViewTestCases.APIViewTestCase): model = CustomFieldChoiceSet brief_fields = ['choices_count', 'description', 'display', 'id', 'name', 'url'] create_data = [ @@ -325,7 +326,7 @@ class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase): self.assertEqual(response.status_code, 400) -class CustomLinkTest(APIViewTestCases.APIViewTestCase): +class CustomLinkTestCase(APIViewTestCases.APIViewTestCase): model = CustomLink brief_fields = ['display', 'id', 'name', 'url'] create_data = [ @@ -385,7 +386,7 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase): custom_link.object_types.set([site_type]) -class SavedFilterTest(APIViewTestCases.APIViewTestCase): +class SavedFilterTestCase(APIViewTestCases.APIViewTestCase): model = SavedFilter brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url'] create_data = [ @@ -458,7 +459,78 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase): savedfilter.object_types.set([site_type]) -class BookmarkTest( +class TableConfigTestCase(APIViewTestCases.APIViewTestCase): + model = TableConfig + brief_fields = ['description', 'display', 'id', 'name', 'object_type', 'table', 'url'] + bulk_update_data = { + 'description': 'New description', + 'weight': 999, + 'enabled': False, + 'shared': False, + } + + @classmethod + def setUpTestData(cls): + site_type = ObjectType.objects.get_for_model(Site) + site_table_name = get_table_for_model(Site).__name__ + + users = ( + User(username='User 1'), + User(username='User 2'), + User(username='User 3'), + ) + User.objects.bulk_create(users) + + table_configs = ( + TableConfig( + name='Table Config 1', + object_type=site_type, + table=site_table_name, + user=users[0], + columns=['name', 'status'], + ), + TableConfig( + name='Table Config 2', + object_type=site_type, + table=site_table_name, + user=users[1], + columns=['name', 'region'], + ), + TableConfig( + name='Table Config 3', + object_type=site_type, + table=site_table_name, + user=users[2], + columns=['name', 'tenant'], + ), + ) + TableConfig.objects.bulk_create(table_configs) + + cls.create_data = [ + { + 'object_type': 'dcim.site', + 'table': site_table_name, + 'name': 'Table Config 4', + 'columns': ['name', 'status'], + 'ordering': ['name'], + }, + { + 'object_type': 'dcim.site', + 'table': site_table_name, + 'name': 'Table Config 5', + 'columns': ['name', 'region'], + 'ordering': ['-name'], + }, + { + 'object_type': 'dcim.site', + 'table': site_table_name, + 'name': 'Table Config 6', + 'columns': ['name', 'tenant'], + }, + ] + + +class BookmarkTestCase( APIViewTestCases.GetObjectViewTestCase, APIViewTestCases.ListObjectsViewTestCase, APIViewTestCases.CreateObjectViewTestCase, @@ -510,7 +582,7 @@ class BookmarkTest( ] -class ExportTemplateTest(APIViewTestCases.APIViewTestCase): +class ExportTemplateTestCase(APIViewTestCases.APIViewTestCase): model = ExportTemplate brief_fields = ['description', 'display', 'id', 'name', 'url'] create_data = [ @@ -560,7 +632,7 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase): et.object_types.set([device_object_type]) -class TagTest(APIViewTestCases.APIViewTestCase): +class TagTestCase(APIViewTestCases.APIViewTestCase): model = Tag brief_fields = ['color', 'description', 'display', 'id', 'name', 'slug', 'url'] create_data = [ @@ -593,7 +665,7 @@ class TagTest(APIViewTestCases.APIViewTestCase): Tag.objects.bulk_create(tags) -class TaggedItemTest( +class TaggedItemTestCase( APIViewTestCases.GetObjectViewTestCase, APIViewTestCases.ListObjectsViewTestCase ): @@ -622,7 +694,7 @@ class TaggedItemTest( # TODO: Standardize to APIViewTestCase (needs create & update tests) -class ImageAttachmentTest( +class ImageAttachmentTestCase( APIViewTestCases.GetObjectViewTestCase, APIViewTestCases.ListObjectsViewTestCase, APIViewTestCases.DeleteObjectViewTestCase, @@ -666,7 +738,7 @@ class ImageAttachmentTest( ImageAttachment.objects.bulk_create(image_attachments) -class JournalEntryTest(APIViewTestCases.APIViewTestCase): +class JournalEntryTestCase(APIViewTestCases.APIViewTestCase): model = JournalEntry brief_fields = ['created', 'display', 'id', 'url'] bulk_update_data = { @@ -716,7 +788,7 @@ class JournalEntryTest(APIViewTestCases.APIViewTestCase): ] -class ConfigContextProfileTest(APIViewTestCases.APIViewTestCase): +class ConfigContextProfileTestCase(APIViewTestCases.APIViewTestCase): model = ConfigContextProfile brief_fields = ['description', 'display', 'id', 'name', 'url'] create_data = [ @@ -825,7 +897,7 @@ class ConfigContextProfileTest(APIViewTestCases.APIViewTestCase): self.assertEqual(response.data['data_file']['id'], datafile.pk) -class ConfigContextTest(APIViewTestCases.APIViewTestCase): +class ConfigContextTestCase(APIViewTestCases.APIViewTestCase): model = ConfigContext brief_fields = ['description', 'display', 'id', 'name', 'url'] create_data = [ @@ -951,7 +1023,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase): self.assertEqual(response.data['data_file']['id'], datafile.pk) -class ConfigTemplateTest(APIViewTestCases.APIViewTestCase): +class ConfigTemplateTestCase(APIViewTestCases.APIViewTestCase): model = ConfigTemplate brief_fields = ['description', 'display', 'id', 'name', 'url'] create_data = [ @@ -1036,7 +1108,7 @@ class ConfigTemplateTest(APIViewTestCases.APIViewTestCase): self.assertHttpStatus(response, status.HTTP_200_OK) -class ScriptTest(APITestCase): +class ScriptTestCase(APITestCase): class TestScriptClass(PythonClass): class Meta: @@ -1083,6 +1155,12 @@ class ScriptTest(APITestCase): # Monkey-patch the Script model to return our TestScriptClass above Script.python_class = self.python_class + # The script-run endpoint gates on a live RQ worker. Tests run without + # one, so bypass the check to exercise validation and the enqueue path. + worker_patch = patch('extras.api.views.any_workers_for_queue', return_value=True) + worker_patch.start() + self.addCleanup(worker_patch.stop) + def test_get_script(self): response = self.client.get(self.url, **self.header) @@ -1162,7 +1240,7 @@ class ScriptTest(APITestCase): self.TestScriptClass.Meta.scheduling_enabled = original -class CreatedUpdatedFilterTest(APITestCase): +class CreatedUpdatedFilterTestCase(APITestCase): @classmethod def setUpTestData(cls): @@ -1236,9 +1314,12 @@ class CreatedUpdatedFilterTest(APITestCase): self.assertEqual(response.data['results'][0]['id'], rack2.pk) -class SubscriptionTest(APIViewTestCases.APIViewTestCase): +class SubscriptionTestCase(APIViewTestCases.APIViewTestCase): model = Subscription brief_fields = ['display', 'id', 'object_id', 'object_type', 'url', 'user'] + graphql_filter = { + 'id': {'lookup': 'gt', 'value': '0'}, + } @classmethod def setUpTestData(cls): @@ -1295,7 +1376,7 @@ class SubscriptionTest(APIViewTestCases.APIViewTestCase): } -class NotificationGroupTest(APIViewTestCases.APIViewTestCase): +class NotificationGroupTestCase(APIViewTestCases.APIViewTestCase): model = NotificationGroup brief_fields = ['description', 'display', 'id', 'name', 'url'] create_data = [ @@ -1372,12 +1453,15 @@ class NotificationGroupTest(APIViewTestCases.APIViewTestCase): ] -class NotificationTest(APIViewTestCases.APIViewTestCase): +class NotificationTestCase(APIViewTestCases.APIViewTestCase): model = Notification brief_fields = ['display', 'event_type', 'id', 'object_id', 'object_type', 'read', 'url', 'user'] bulk_update_data = { 'read': now(), } + graphql_filter = { + 'event_type': {'lookup': 'exact', 'value': OBJECT_CREATED}, + } @classmethod def setUpTestData(cls): @@ -1436,7 +1520,7 @@ class NotificationTest(APIViewTestCases.APIViewTestCase): ] -class ScriptModuleTest(APITestCase): +class ScriptModuleTestCase(APITestCase): """ Tests for the POST /api/extras/scripts/upload/ endpoint. diff --git a/netbox/extras/tests/test_conditions.py b/netbox/extras/tests/test_conditions.py index d705ba6ae..53b3c6ac3 100644 --- a/netbox/extras/tests/test_conditions.py +++ b/netbox/extras/tests/test_conditions.py @@ -134,7 +134,7 @@ class ConditionTestCase(TestCase): self.assertTrue(c.eval({'x': '123'})) -class ConditionSetTest(TestCase): +class ConditionSetTestCase(TestCase): def test_empty(self): with self.assertRaises(ValueError): diff --git a/netbox/extras/tests/test_custom_validation.py b/netbox/extras/tests/test_custom_validation.py index f76864a7d..d6fa0afd4 100644 --- a/netbox/extras/tests/test_custom_validation.py +++ b/netbox/extras/tests/test_custom_validation.py @@ -8,7 +8,7 @@ from netbox.choices import CSVDelimiterChoices, ImportFormatChoices from utilities.testing import APITestCase, ModelViewTestCase, create_tags, post_data -class ModelFormCustomValidationTest(TestCase): +class ModelFormCustomValidationTestCase(TestCase): @override_settings(CUSTOM_VALIDATORS={ 'circuits.provider': [ @@ -58,7 +58,7 @@ class ModelFormCustomValidationTest(TestCase): self.assertTrue(form.is_valid()) -class BulkEditCustomValidationTest(ModelViewTestCase): +class BulkEditCustomValidationTestCase(ModelViewTestCase): model = Provider @classmethod @@ -155,7 +155,7 @@ class BulkEditCustomValidationTest(ModelViewTestCase): self.assertTrue(provider.asns.exists()) -class BulkImportCustomValidationTest(ModelViewTestCase): +class BulkImportCustomValidationTestCase(ModelViewTestCase): model = Provider @classmethod @@ -214,7 +214,7 @@ class BulkImportCustomValidationTest(ModelViewTestCase): self.assertTrue(Provider.objects.exists()) -class APISerializerCustomValidationTest(APITestCase): +class APISerializerCustomValidationTestCase(APITestCase): @override_settings(CUSTOM_VALIDATORS={ 'circuits.provider': [ diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 0b9382701..c7c633991 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -19,7 +19,7 @@ from utilities.testing import APITestCase, TestCase from virtualization.models import VirtualMachine -class CustomFieldTest(TestCase): +class CustomFieldTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -466,6 +466,15 @@ class CustomFieldTest(TestCase): self.assertIn('choice_colors', cm.exception.message_dict) + @tag('regression') + def test_choice_set_with_base_choices_validates_without_error(self): + """Regression test for #22325: base-only choice sets must validate.""" + for base in ('IATA', 'ISO_3166', 'UN_LOCODE'): + with self.subTest(base=base): + choice_set = CustomFieldChoiceSet(name=f'Test {base}', base_choices=base, order_alphabetically=True) + choice_set.full_clean() # must not raise + choice_set.save() # must not raise (extra_choices is None) + def test_remove_selected_choice(self): """ Removing a ChoiceSet choice that is referenced by an object should raise @@ -762,7 +771,7 @@ class CustomFieldTest(TestCase): ).full_clean() -class CustomFieldManagerTest(TestCase): +class CustomFieldManagerTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -776,7 +785,7 @@ class CustomFieldManagerTest(TestCase): self.assertEqual(CustomField.objects.get_for_model(VirtualMachine).count(), 0) -class CustomFieldAPITest(APITestCase): +class CustomFieldAPITestCase(APITestCase): @classmethod def setUpTestData(cls): @@ -1564,7 +1573,7 @@ class CustomFieldAPITest(APITestCase): self.assertHttpStatus(response, status.HTTP_200_OK) -class CustomFieldImportTest(TestCase): +class CustomFieldImportTestCase(TestCase): user_permissions = ( 'dcim.view_site', 'dcim.add_site', @@ -1694,7 +1703,7 @@ class CustomFieldImportTest(TestCase): self.assertIn('cf_select', form.errors) -class CustomFieldModelTest(TestCase): +class CustomFieldModelTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -1755,7 +1764,7 @@ class CustomFieldModelTest(TestCase): site.clean() -class CustomFieldModelFilterTest(TestCase): +class CustomFieldModelFilterTestCase(TestCase): queryset = Site.objects.all() filterset = SiteFilterSet diff --git a/netbox/extras/tests/test_customvalidators.py b/netbox/extras/tests/test_customvalidators.py index f7aafb61a..8fc22a6c3 100644 --- a/netbox/extras/tests/test_customvalidators.py +++ b/netbox/extras/tests/test_customvalidators.py @@ -98,7 +98,7 @@ request_validator = CustomValidator({ custom_validator = MyValidator() -class CustomValidatorTest(TestCase): +class CustomValidatorTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -207,7 +207,7 @@ class CustomValidatorTest(TestCase): request_validator(site, request) -class CustomValidatorConfigTest(TestCase): +class CustomValidatorConfigTestCase(TestCase): @override_settings( CUSTOM_VALIDATORS={ @@ -242,7 +242,7 @@ class CustomValidatorConfigTest(TestCase): Site(name='bar', slug='bar').clean() -class ProtectionRulesConfigTest(TestCase): +class ProtectionRulesConfigTestCase(TestCase): @override_settings( PROTECTION_RULES={ diff --git a/netbox/extras/tests/test_dashboard.py b/netbox/extras/tests/test_dashboard.py index ac3a1afe5..866b46477 100644 --- a/netbox/extras/tests/test_dashboard.py +++ b/netbox/extras/tests/test_dashboard.py @@ -3,7 +3,7 @@ from django.test import TestCase, tag from extras.dashboard.widgets import ObjectListWidget -class ObjectListWidgetTests(TestCase): +class ObjectListWidgetTestCase(TestCase): def test_widget_config_form_validates_model(self): model_info = 'extras.notification' form = ObjectListWidget.ConfigForm({'model': model_info}) diff --git a/netbox/extras/tests/test_event_rules.py b/netbox/extras/tests/test_event_rules.py index 50095df22..ddd1b05b8 100644 --- a/netbox/extras/tests/test_event_rules.py +++ b/netbox/extras/tests/test_event_rules.py @@ -24,7 +24,7 @@ from netbox.context_managers import event_tracking from utilities.testing import APITestCase -class EventRuleTest(APITestCase): +class EventRuleTestCase(APITestCase): def setUp(self): super().setUp() @@ -33,6 +33,12 @@ class EventRuleTest(APITestCase): self.queue = django_rq.get_queue('default') self.queue.empty() + def tearDown(self): + super().tearDown() + + # Clear the queue so leftover jobs do not leak to the next test suite + self.queue.empty() + @classmethod def setUpTestData(cls): diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index c9727ad2f..24d3d02ef 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -790,6 +790,62 @@ class ImageAttachmentTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) +class TableConfigTestCase(TestCase, ChangeLoggedFilterSetTests): + queryset = TableConfig.objects.all() + filterset = TableConfigFilterSet + ignore_fields = ('columns', 'ordering') + + @classmethod + def setUpTestData(cls): + site_ct = ContentType.objects.get_by_natural_key('dcim', 'site') + rack_ct = ContentType.objects.get_by_natural_key('dcim', 'rack') + + users = ( + User(username='user1'), + User(username='user2'), + User(username='user3'), + ) + User.objects.bulk_create(users) + + TableConfig.objects.bulk_create( + [ + TableConfig( + object_type=site_ct, + table='SiteTable', + name='Table Config 1', + user=users[0], + weight=100, + enabled=True, + shared=True, + columns=['name', 'status'], + ordering=[], + ), + TableConfig( + object_type=site_ct, + table='SiteTable', + name='Table Config 2', + user=users[1], + weight=200, + enabled=True, + shared=False, + columns=['name', 'region'], + ordering=[], + ), + TableConfig( + object_type=rack_ct, + table='RackTable', + name='Table Config 3', + user=users[2], + weight=300, + enabled=False, + shared=True, + columns=['name', 'site'], + ordering=[], + ), + ] + ) + + class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = JournalEntry.objects.all() filterset = JournalEntryFilterSet diff --git a/netbox/extras/tests/test_forms.py b/netbox/extras/tests/test_forms.py index aea84e92c..26de1de80 100644 --- a/netbox/extras/tests/test_forms.py +++ b/netbox/extras/tests/test_forms.py @@ -1,15 +1,22 @@ +import tempfile +from pathlib import Path + +from django.core.exceptions import NON_FIELD_ERRORS +from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase -from core.models import ObjectType +from core.choices import ManagedFileRootPathChoices +from core.models import DataSource, ObjectType from dcim.forms import SiteForm from dcim.models import Site from extras.choices import CustomFieldTypeChoices from extras.forms import SavedFilterForm from extras.forms.model_forms import CustomFieldChoiceSetForm -from extras.models import CustomField, CustomFieldChoiceSet +from extras.forms.scripts import ScriptFileForm +from extras.models import CustomField, CustomFieldChoiceSet, ScriptModule -class CustomFieldModelFormTest(TestCase): +class CustomFieldModelFormTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -91,7 +98,7 @@ class CustomFieldModelFormTest(TestCase): self.assertIsNone(instance.custom_field_data[field_type]) -class CustomFieldChoiceSetFormTest(TestCase): +class CustomFieldChoiceSetFormTestCase(TestCase): def test_escaped_colons_preserved_on_edit(self): choice_set = CustomFieldChoiceSet.objects.create( @@ -142,7 +149,7 @@ class CustomFieldChoiceSetFormTest(TestCase): self.assertEqual(updated.choice_colors, {'choice2': 'green', 'foo:bar': 'red'}) -class SavedFilterFormTest(TestCase): +class SavedFilterFormTestCase(TestCase): def test_basic_submit(self): """ @@ -161,3 +168,123 @@ class SavedFilterFormTest(TestCase): }) self.assertTrue(form.is_valid()) form.save() + + +class ScriptFileFormTestCase(TestCase): + """ + Scripts added via a Data Source must be validated the same way uploaded scripts are (see #22180). + """ + BROKEN_SCRIPT = ( + "from extras.scripts import Script\n" + "import imnotarealmoduleicreateerrors\n\n\n" + "class BrokenScript(Script):\n" + " def run(self, data, commit):\n" + " pass\n" + ) + VALID_SCRIPT = ( + "from extras.scripts import Script\n\n\n" + "class FirstScript(Script):\n" + " def run(self, data, commit):\n" + " pass\n\n\n" + "class SecondScript(Script):\n" + " def run(self, data, commit):\n" + " pass\n" + ) + + @staticmethod + def _write(scripts_dir, filename, content): + with open(scripts_dir / filename, 'w') as f: + f.write(content) + + @staticmethod + def _new_module(): + # Mirror ScriptModuleCreateView.alter_object(), which sets file_root before validation. + return ScriptModule(file_root=ManagedFileRootPathChoices.SCRIPTS) + + def _sync_source(self, name, **files): + """ + Create a local DataSource over a temp dir populated with the given {filename: content} files, + sync it, and return the DataSource. + """ + temp_dir = tempfile.TemporaryDirectory() + self.addCleanup(temp_dir.cleanup) + scripts_dir = Path(temp_dir.name) / "scripts" + scripts_dir.mkdir(parents=True, exist_ok=True) + for filename, content in files.items(): + self._write(scripts_dir, filename, content) + + data_source = DataSource(name=name, type="local", source_url=str(scripts_dir)) + data_source.save() + data_source.sync() + return data_source + + def test_broken_script_via_data_file_is_rejected(self): + """A script that fails to import via a data_file must be rejected, and no ScriptModule created.""" + data_source = self._sync_source("Broken", **{'broken.py': self.BROKEN_SCRIPT}) + data_file = data_source.datafiles.get(path__endswith='broken.py') + + form = ScriptFileForm(data={'data_file': data_file.pk}, instance=self._new_module()) + + self.assertFalse(form.is_valid()) + self.assertIn(NON_FIELD_ERRORS, form.errors) + self.assertEqual(ScriptModule.objects.count(), 0) + + def test_valid_script_via_data_file_is_accepted(self): + """A valid script via a data_file passes validation and its Script classes are discovered on save.""" + data_source = self._sync_source("Valid", **{'valid.py': self.VALID_SCRIPT}) + data_file = data_source.datafiles.get(path__endswith='valid.py') + + form = ScriptFileForm(data={'data_file': data_file.pk}, instance=self._new_module()) + self.assertTrue(form.is_valid()) + module = form.save() + + self.assertEqual(ScriptModule.objects.count(), 1) + self.assertEqual( + {script.name for script in module.scripts.all()}, + {'FirstScript', 'SecondScript'}, + ) + + def test_corrected_script_recovers(self): + """After a broken script is rejected, syncing a corrected version succeeds without a uniqueness deadlock.""" + temp_dir = tempfile.TemporaryDirectory() + self.addCleanup(temp_dir.cleanup) + scripts_dir = Path(temp_dir.name) / "scripts" + scripts_dir.mkdir(parents=True, exist_ok=True) + + data_source = DataSource(name="Recovery", type="local", source_url=str(scripts_dir)) + data_source.save() + + # First sync: broken script is rejected, nothing created + self._write(scripts_dir, 'myscript.py', self.BROKEN_SCRIPT) + data_source.sync() + data_file = data_source.datafiles.get(path__endswith='myscript.py') + form = ScriptFileForm(data={'data_file': data_file.pk}, instance=self._new_module()) + self.assertFalse(form.is_valid()) + self.assertEqual(ScriptModule.objects.count(), 0) + + # Correct the script and re-sync: now it should be accepted + self._write(scripts_dir, 'myscript.py', self.VALID_SCRIPT) + data_source.sync() + data_file = data_source.datafiles.get(path__endswith='myscript.py') + form = ScriptFileForm(data={'data_file': data_file.pk}, instance=self._new_module()) + self.assertTrue(form.is_valid()) + module = form.save() + self.assertEqual( + {script.name for script in module.scripts.all()}, + {'FirstScript', 'SecondScript'}, + ) + + def test_broken_script_via_upload_is_rejected(self): + """Regression guard: the upload_file path still validates content.""" + upload_file = SimpleUploadedFile(name='broken.py', content=self.BROKEN_SCRIPT.encode()) + form = ScriptFileForm(files={'upload_file': upload_file}, instance=self._new_module()) + + self.assertFalse(form.is_valid()) + self.assertIn(NON_FIELD_ERRORS, form.errors) + + def test_valid_script_via_upload_is_accepted(self): + """Regression guard: a valid uploaded script still validates.""" + upload_file = SimpleUploadedFile(name='valid.py', content=self.VALID_SCRIPT.encode()) + form = ScriptFileForm(files={'upload_file': upload_file}, instance=self._new_module()) + + self.assertTrue(form.is_valid()) diff --git a/netbox/extras/tests/test_jobs.py b/netbox/extras/tests/test_jobs.py new file mode 100644 index 000000000..6906ebb69 --- /dev/null +++ b/netbox/extras/tests/test_jobs.py @@ -0,0 +1,329 @@ +from contextlib import contextmanager +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +from django.db import DEFAULT_DB_ALIAS +from django.test import TestCase + +from extras.jobs import ScriptJob +from utilities.exceptions import AbortScript + + +def _make_runner(**job_attrs): + """ + Build a ScriptJob without going through ``__init__``. + + ``JobRunner.__init__`` attaches a ``JobLogHandler`` to a module-level + singleton logger; instantiating one per test would accumulate handlers. + """ + runner = ScriptJob.__new__(ScriptJob) + runner.job = MagicMock(**job_attrs) + runner.logger = MagicMock() + return runner + + +class DummyScript: + """A minimal stand-in for a Script implementation.""" + + full_name = 'tests.DummyScript' + + def __init__(self, run_result=None, run_exception=None, failed=False): + self._run_result = run_result + self._run_exception = run_exception + self.failed = failed + self.output = None + self.request = None + self.run = MagicMock(side_effect=self._run_impl) + self.log_info = MagicMock() + self.log_failure = MagicMock() + self.get_job_data = MagicMock(return_value={'status': 'done'}) + + def _run_impl(self, data, commit): + if self._run_exception is not None: + raise self._run_exception + return self._run_result + + +class RunScriptTestCase(TestCase): + def test_run_script_success_commit_true_sets_output_and_job_data(self): + runner = _make_runner() + script = DummyScript(run_result='hello') + + runner.run_script(script, request=None, data={'k': 'v'}, commit=True) + + self.assertEqual(script.output, 'hello') + script.run.assert_called_once_with({'k': 'v'}, True) + self.assertEqual(runner.job.data, {'status': 'done'}) + + def test_run_script_commit_false_rolls_back_without_raising(self): + runner = _make_runner() + script = DummyScript(run_result='hello') + + runner.run_script(script, request=None, data={}, commit=False) + + script.run.assert_called_once_with({}, False) + # Rollback path: log_info() is called (with the translated revert message); + # job.data is still populated. Don't assert exact wording. + script.log_info.assert_called_once() + self.assertEqual(runner.job.data, {'status': 'done'}) + + def test_run_script_commit_false_logs_warning_when_script_failed(self): + runner = _make_runner() + script = DummyScript(run_result='hello', failed=True) + + with patch('extras.jobs.logging.getLogger') as get_logger: + logger = get_logger.return_value + runner.run_script(script, request=None, data={}, commit=False) + + logger.warning.assert_any_call('Script failed') + + def test_run_script_abort_script_logs_failure_and_reraises(self): + # Non-report scripts call `script.log_failure(msg)` positionally; report-style + # scripts use `log_failure(message=msg)`. See `test_run_script_abort_script_uses_report_log_failure_signature`. + runner = _make_runner() + script = DummyScript(run_exception=AbortScript('nope')) + + with self.assertRaises(AbortScript): + runner.run_script(script, request=None, data={}, commit=True) + + # Outcome assertions: AbortScript re-raised, failure logged on the script, + # rollback path traversed (log_info called), job.data populated. + script.log_failure.assert_called_once() + # Non-report path uses the positional signature; verify both that there is + # a positional arg and no `message=` kwarg. + self.assertTrue(script.log_failure.call_args.args) + self.assertFalse(script.log_failure.call_args.kwargs) + script.log_info.assert_called_once() + self.assertEqual(runner.job.data, {'status': 'done'}) + + def test_run_script_abort_script_uses_report_log_failure_signature(self): + runner = _make_runner() + script = DummyScript(run_exception=AbortScript('reportfail')) + + with ( + patch('extras.jobs.is_report', return_value=True), + self.assertRaises(AbortScript), + ): + runner.run_script(script, request=None, data={}, commit=True) + + # For reports, log_failure is called with the keyword-form signature. + script.log_failure.assert_called_once() + self.assertIn('message', script.log_failure.call_args.kwargs) + + def test_run_script_general_exception_logs_traceback_and_reraises(self): + runner = _make_runner() + script = DummyScript(run_exception=RuntimeError('boom')) + + with self.assertRaisesMessage(RuntimeError, 'boom'): + runner.run_script(script, request=None, data={}, commit=True) + + script.log_failure.assert_called_once() + # The failure message wraps a traceback; assert on the structural marker + # (Traceback header) rather than translated copy. + message = script.log_failure.call_args.kwargs['message'] + self.assertIn('Traceback', message) + self.assertEqual(runner.job.data, {'status': 'done'}) + + def test_run_script_sends_clear_events_when_request_is_present_and_error_occurs(self): + runner = _make_runner() + script = DummyScript(run_exception=RuntimeError('boom')) + request = MagicMock() + + with patch('extras.jobs.clear_events.send') as send: + with self.assertRaises(RuntimeError): + runner.run_script(script, request=request, data={}, commit=True) + + send.assert_called_once_with(request) + + def test_run_script_default_db_uses_single_atomic(self): + # Symmetric with test_run_script_enters_secondary_atomic_when_changelog_db_differs: + # when the changelog DB is the default, only a single atomic() block is opened. + runner = _make_runner() + script = DummyScript(run_result='ok') + atomic_calls = [] + + @contextmanager + def fake_atomic(using): + atomic_calls.append(using) + yield + + with ( + patch('extras.jobs.router.db_for_write', return_value=DEFAULT_DB_ALIAS), + patch('extras.jobs.transaction.atomic', side_effect=fake_atomic), + ): + runner.run_script(script, request=None, data={}, commit=True) + + self.assertEqual(atomic_calls, [DEFAULT_DB_ALIAS]) + script.run.assert_called_once_with({}, True) + self.assertEqual(runner.job.data, {'status': 'done'}) + + def test_run_script_enters_secondary_atomic_when_changelog_db_differs(self): + # Verifies that the code enters two nested atomic() context managers when + # the changelog DB differs from the default. Does not exercise real rollback + # semantics (transaction.atomic is patched). + runner = _make_runner() + script = DummyScript(run_result='ok') + + atomic_calls = [] + + @contextmanager + def fake_atomic(using): + atomic_calls.append(using) + yield + + with ( + patch('extras.jobs.router.db_for_write', return_value='changelog_db'), + patch('extras.jobs.transaction.atomic', side_effect=fake_atomic), + ): + runner.run_script(script, request=None, data={}, commit=True) + + self.assertEqual(atomic_calls, [DEFAULT_DB_ALIAS, 'changelog_db']) + script.run.assert_called_once_with({}, True) + self.assertEqual(runner.job.data, {'status': 'done'}) + + def test_run_script_enters_secondary_atomic_when_commit_false(self): + # Mirror of the previous test for the commit=False branch on the secondary + # DB path. Verifies both atomic blocks are entered; does not exercise true + # rollback semantics (transaction.atomic is patched). + runner = _make_runner() + script = DummyScript(run_result='ok') + atomic_calls = [] + + @contextmanager + def fake_atomic(using): + atomic_calls.append(using) + yield + + with ( + patch('extras.jobs.router.db_for_write', return_value='changelog_db'), + patch('extras.jobs.transaction.atomic', side_effect=fake_atomic), + ): + runner.run_script(script, request=None, data={}, commit=False) + + self.assertEqual(atomic_calls, [DEFAULT_DB_ALIAS, 'changelog_db']) + script.run.assert_called_once_with({}, False) + script.log_info.assert_called_once() + + +class ScriptJobRunTestCase(TestCase): + @staticmethod + def _runner(): + return _make_runner(object_id=1) + + @staticmethod + def _script_model(script_instance): + return SimpleNamespace(pk=1, python_class=MagicMock(return_value=script_instance)) + + def test_run_loads_script_model_and_instantiates_python_class(self): + runner = self._runner() + script_instance = DummyScript() + script_model = self._script_model(script_instance) + + with ( + patch('extras.jobs.ScriptModel.objects.get', return_value=script_model) as get_script_model, + patch.object(ScriptJob, 'run_script') as run_script, + patch.dict('netbox.registry.registry', {'request_processors': []}, clear=False), + ): + runner.run(data={'k': 'v'}, commit=True) + + get_script_model.assert_called_once_with(pk=1) + script_model.python_class.assert_called_once_with() + run_script.assert_called_once() + args, _ = run_script.call_args + self.assertIs(args[0], script_instance) + self.assertEqual(args[2], {'k': 'v'}) # data + self.assertIs(args[3], True) # commit + + def test_run_merges_request_files_into_data(self): + runner = self._runner() + script_instance = DummyScript() + script_model = self._script_model(script_instance) + request = MagicMock(FILES={'upload': 'fileobj'}, id='req-1') + data = {'k': 'v'} + + with ( + patch('extras.jobs.ScriptModel.objects.get', return_value=script_model), + patch.object(ScriptJob, 'run_script') as run_script, + patch.dict('netbox.registry.registry', {'request_processors': []}, clear=False), + ): + runner.run(data=data, request=request, commit=True) + + passed_data = run_script.call_args.args[2] + self.assertEqual(passed_data['k'], 'v') + self.assertEqual(passed_data['upload'], 'fileobj') + + def test_run_sets_script_request_when_request_is_present(self): + runner = self._runner() + script_instance = DummyScript() + script_model = self._script_model(script_instance) + request = MagicMock(FILES={}, id='req-1') + + with ( + patch('extras.jobs.ScriptModel.objects.get', return_value=script_model), + patch.object(ScriptJob, 'run_script'), + patch.dict('netbox.registry.registry', {'request_processors': []}, clear=False), + ): + runner.run(data={}, request=request, commit=True) + + self.assertIs(script_instance.request, request) + + def _processor_factories(self, entered): + def ctx_a(request): + @contextmanager + def _ctx(): + entered.append('proc_a') + yield + + return _ctx() + + def ctx_event(request): + @contextmanager + def _ctx(): + entered.append('event_tracking') + yield + + return _ctx() + + return ctx_a, ctx_event + + def test_run_uses_request_processors_when_commit_true(self): + runner = self._runner() + script_instance = DummyScript() + script_model = self._script_model(script_instance) + entered = [] + ctx_a, ctx_event = self._processor_factories(entered) + + with ( + patch('extras.jobs.ScriptModel.objects.get', return_value=script_model), + patch.object(ScriptJob, 'run_script'), + patch('extras.jobs.event_tracking', new=ctx_event), + patch.dict( + 'netbox.registry.registry', + {'request_processors': [ctx_a, ctx_event]}, + clear=False, + ), + ): + runner.run(data={}, commit=True) + + self.assertEqual(entered, ['proc_a', 'event_tracking']) + + def test_run_skips_event_tracking_when_commit_false(self): + runner = self._runner() + script_instance = DummyScript() + script_model = self._script_model(script_instance) + entered = [] + ctx_a, ctx_event = self._processor_factories(entered) + + with ( + patch('extras.jobs.ScriptModel.objects.get', return_value=script_model), + patch.object(ScriptJob, 'run_script'), + patch('extras.jobs.event_tracking', new=ctx_event), + patch.dict( + 'netbox.registry.registry', + {'request_processors': [ctx_a, ctx_event]}, + clear=False, + ), + ): + runner.run(data={}, commit=False) + + self.assertEqual(entered, ['proc_a']) diff --git a/netbox/extras/tests/test_management_commands.py b/netbox/extras/tests/test_management_commands.py new file mode 100644 index 000000000..1a6e2bfe8 --- /dev/null +++ b/netbox/extras/tests/test_management_commands.py @@ -0,0 +1,504 @@ +from io import BytesIO, StringIO +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +from django.contrib.contenttypes.models import ContentType +from django.core.management import call_command +from django.core.management.base import CommandError +from django.test import TestCase + +from dcim.choices import InterfaceTypeChoices +from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site +from extras.management.commands import renaturalize, webhook_receiver +from extras.management.commands.webhook_receiver import WebhookHandler +from users.models import User +from utilities.fields import NaturalOrderingField + + +class ReindexTestCase(TestCase): + def test_reindex_all_registered_indexers(self): + class DummyObjects: + @staticmethod + def iterator(): + return iter(()) + + class DummyModel: + objects = DummyObjects() + _meta = SimpleNamespace(app_label='extras', model_name='dummy') + + indexer = SimpleNamespace(model=DummyModel) + out = StringIO() + + with ( + patch('extras.management.commands.reindex.registry', {'search': {'extras.dummy': indexer}}), + patch('extras.management.commands.reindex.search_backend') as search_backend, + ): + search_backend.clear.return_value = 0 + search_backend.cache.return_value = 0 + search_backend.size = 0 + + call_command('reindex', stdout=out) + + search_backend.clear.assert_called_once_with(object_types=None) + search_backend.cache.assert_called_once() + self.assertIn('Completed.', out.getvalue()) + + def test_reindex_lazy_skips_models_with_existing_cache(self): + class DummyObjects: + @staticmethod + def iterator(): + return iter(()) + + class DummyModel: + objects = DummyObjects() + _meta = SimpleNamespace(app_label='extras', model_name='dummy') + + content_type = object() + indexer = SimpleNamespace(model=DummyModel) + out = StringIO() + + with ( + patch('extras.management.commands.reindex.registry', {'search': {'extras.dummy': indexer}}), + patch('extras.management.commands.reindex.search_backend') as search_backend, + patch.object(ContentType.objects, 'get_for_model', return_value=content_type), + ): + search_backend.count.return_value = 1 + search_backend.size = 1 + + call_command('reindex', lazy=True, stdout=out) + + search_backend.clear.assert_not_called() + search_backend.count.assert_called_once_with(object_types=[content_type]) + search_backend.cache.assert_not_called() + self.assertIn('Skipping', out.getvalue()) + + def test_reindex_specific_model_caches_objects_and_reports_total_count(self): + iterator = iter([object()]) + + class DummyObjects: + @staticmethod + def iterator(): + return iterator + + class DummyModel: + objects = DummyObjects() + _meta = SimpleNamespace(app_label='extras', model_name='dummy') + + content_type = object() + indexer = SimpleNamespace(model=DummyModel) + out = StringIO() + + with ( + patch('extras.management.commands.reindex.registry', {'search': {'extras.dummy': indexer}}), + patch('extras.management.commands.reindex.search_backend') as search_backend, + patch.object(ContentType.objects, 'get_for_model', return_value=content_type), + ): + search_backend.clear.return_value = 2 + search_backend.cache.return_value = 1 + search_backend.size = 1 + + call_command('reindex', 'extras.dummy', stdout=out) + + search_backend.clear.assert_called_once_with(object_types=[content_type]) + search_backend.cache.assert_called_once_with(iterator, remove_existing=False) + self.assertIn('1 entries cached.', out.getvalue()) + self.assertIn('Total entries: 1', out.getvalue()) + + def test_reindex_app_label_uses_matching_indexers(self): + class DummyObjects: + @staticmethod + def iterator(): + return iter(()) + + class DummyModel: + objects = DummyObjects() + _meta = SimpleNamespace(app_label='extras', model_name='dummy') + + class OtherModel: + objects = DummyObjects() + _meta = SimpleNamespace(app_label='dcim', model_name='device') + + content_type = object() + indexer = SimpleNamespace(model=DummyModel) + other_indexer = SimpleNamespace(model=OtherModel) + + with ( + patch( + 'extras.management.commands.reindex.registry', + {'search': {'extras.dummy': indexer, 'dcim.device': other_indexer}}, + ), + patch('extras.management.commands.reindex.search_backend') as search_backend, + patch.object(ContentType.objects, 'get_for_model', return_value=content_type), + ): + search_backend.clear.return_value = 0 + search_backend.cache.return_value = 0 + search_backend.size = 0 + + call_command('reindex', 'extras', stdout=StringIO()) + + search_backend.clear.assert_called_once_with(object_types=[content_type]) + search_backend.cache.assert_called_once() + + def test_reindex_unknown_registered_model(self): + with ( + patch('extras.management.commands.reindex.registry', {'search': {}}), + self.assertRaisesMessage(CommandError, 'No indexer registered for extras.dummy'), + ): + call_command('reindex', 'extras.dummy', stdout=StringIO()) + + def test_reindex_app_with_no_registered_indexers(self): + with ( + patch('extras.management.commands.reindex.registry', {'search': {}}), + self.assertRaisesMessage(CommandError, 'No indexers found'), + ): + call_command('reindex', 'extras', stdout=StringIO()) + + def test_invalid_model_label(self): + with self.assertRaisesMessage(CommandError, 'Invalid model'): + call_command('reindex', 'dcim.rack.extra', stdout=StringIO()) + + +class RenaturalizeTestCase(TestCase): + @classmethod + def setUpTestData(cls): + site = Site.objects.create(name='Test Site', slug='test-site') + manufacturer = Manufacturer.objects.create(name='Test Manufacturer', slug='test-manufacturer') + device_type = DeviceType.objects.create( + manufacturer=manufacturer, + model='Test Device Type', + slug='test-device-type', + ) + device_role = DeviceRole.objects.create( + name='Test Device Role', + slug='test-device-role', + color='ff0000', + ) + cls.device = Device.objects.create( + device_type=device_type, + role=device_role, + name='Test Device', + site=site, + ) + + def test_recalculates_natural_ordering_fields(self): + interface = Interface.objects.create( + device=self.device, + name='Ethernet10', + type=InterfaceTypeChoices.TYPE_1GE_FIXED, + ) + field = next(field for field in Interface._meta.concrete_fields if type(field) is NaturalOrderingField) + Interface.objects.filter(pk=interface.pk).update(**{field.name: 'incorrect'}) + + out = StringIO() + call_command('renaturalize', 'dcim.Interface', verbosity=2, stdout=out) + + interface.refresh_from_db() + expected = field.naturalize_function(interface.name, max_length=field.max_length) + self.assertEqual(getattr(interface, field.name), expected) + self.assertIn('Ethernet10 ->', out.getvalue()) + self.assertIn('updated', out.getvalue()) + + def test_recalculates_with_default_verbosity(self): + interface = Interface.objects.create( + device=self.device, + name='Ethernet11', + type=InterfaceTypeChoices.TYPE_1GE_FIXED, + ) + field = next(field for field in Interface._meta.concrete_fields if type(field) is NaturalOrderingField) + Interface.objects.filter(pk=interface.pk).update(**{field.name: 'incorrect'}) + + out = StringIO() + call_command('renaturalize', 'dcim.Interface', verbosity=1, stdout=out) + + interface.refresh_from_db() + expected = field.naturalize_function(interface.name, max_length=field.max_length) + self.assertEqual(getattr(interface, field.name), expected) + self.assertIn('Renaturalizing 1 models.', out.getvalue()) + self.assertIn('Done.', out.getvalue()) + + def test_invalid_format(self): + with self.assertRaisesMessage(CommandError, 'Invalid format'): + call_command('renaturalize', 'dcim', stdout=StringIO()) + + def test_model_without_natural_ordering(self): + with self.assertRaisesMessage(CommandError, 'does not employ natural ordering'): + call_command('renaturalize', 'extras.Tag', stdout=StringIO()) + + def test_unknown_app_label(self): + with self.assertRaises(CommandError): + call_command('renaturalize', 'invalid.Interface', stdout=StringIO()) + + def test_unknown_model_name(self): + with self.assertRaisesMessage(CommandError, 'Unknown model: dcim.UnknownModel'): + call_command('renaturalize', 'dcim.UnknownModel', stdout=StringIO()) + + def test_get_models_discovers_all_models_with_natural_ordering_fields(self): + field = next(field for field in Interface._meta.concrete_fields if type(field) is NaturalOrderingField) + model = SimpleNamespace(_meta=SimpleNamespace(concrete_fields=[field])) + app_config = SimpleNamespace(models={'interface': model}) + + with patch('extras.management.commands.renaturalize.apps.get_app_configs', return_value=[app_config]): + models = renaturalize.Command()._get_models(()) + + self.assertEqual(models, [(model, [field])]) + + +class RunScriptTestCase(TestCase): + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_superuser( + username='admin', + email='admin@example.com', + password='password', + ) + + def test_enqueues_script_job(self): + class TestScript: + full_name = 'test.Script' + + def as_form(self, data, files): + form = MagicMock() + form.is_valid.return_value = True + form.cleaned_data = { + '_schedule_at': None, + '_interval': None, + '_commit': None, + 'name': data['name'], + } + form.errors.get_json_data.return_value = {} + return form + + script_obj = SimpleNamespace(python_class=TestScript) + job = SimpleNamespace(duration='0 seconds') + + with ( + patch( + 'extras.management.commands.runscript.get_module_and_script', + return_value=(None, script_obj), + ) as get_module_and_script, + patch( + 'extras.management.commands.runscript.ScriptJob.enqueue', + return_value=job, + ) as enqueue, + patch('extras.management.commands.runscript.logging.getLogger'), + ): + call_command( + 'runscript', + 'test.Script', + user='admin', + data='{"name": "test"}', + stdout=StringIO(), + ) + + get_module_and_script.assert_called_once_with('test', 'Script') + enqueue.assert_called_once() + kwargs = enqueue.call_args.kwargs + self.assertEqual(kwargs['instance'], script_obj) + self.assertEqual(kwargs['user'], self.user) + self.assertTrue(kwargs['immediate']) + self.assertEqual(kwargs['data'], {'name': 'test'}) + self.assertFalse(kwargs['commit']) + + def test_invalid_script_data_raises_error_without_enqueueing_job(self): + class TestScript: + full_name = 'test.Script' + + def as_form(self, data, files): + form = MagicMock() + form.is_valid.return_value = False + form.errors.get_json_data.return_value = { + 'name': [ + {'message': 'This field is required.'}, + ], + } + return form + + script_obj = SimpleNamespace(python_class=TestScript) + logger = MagicMock() + + with ( + patch( + 'extras.management.commands.runscript.get_module_and_script', + return_value=(None, script_obj), + ) as get_module_and_script, + patch('extras.management.commands.runscript.ScriptJob.enqueue') as enqueue, + patch('extras.management.commands.runscript.logging.getLogger', return_value=logger), + ): + with self.assertRaises(CommandError): + call_command( + 'runscript', + 'test.Script', + user='admin', + data='{}', + stdout=StringIO(), + ) + + get_module_and_script.assert_called_once_with('test', 'Script') + enqueue.assert_not_called() + logger.error.assert_any_call('Data is not valid:') + logger.error.assert_any_call('\tname: This field is required.') + + def test_missing_user_falls_back_to_superuser_and_empty_data(self): + class TestScript: + full_name = 'test.Script' + + def as_form(self, data, files): + form = MagicMock() + form.is_valid.return_value = True + form.cleaned_data = { + '_schedule_at': None, + '_interval': None, + '_commit': None, + } + form.errors.get_json_data.return_value = {} + return form + + script_obj = SimpleNamespace(python_class=TestScript) + job = SimpleNamespace(duration='0 seconds') + + with ( + patch( + 'extras.management.commands.runscript.get_module_and_script', + return_value=(None, script_obj), + ), + patch( + 'extras.management.commands.runscript.ScriptJob.enqueue', + return_value=job, + ) as enqueue, + patch('extras.management.commands.runscript.logging.getLogger'), + ): + call_command( + 'runscript', + 'test.Script', + user='missing-user', + stdout=StringIO(), + ) + + kwargs = enqueue.call_args.kwargs + self.assertEqual(kwargs['user'], self.user) + self.assertEqual(kwargs['data'], {}) + + def test_no_user_argument_falls_back_to_first_superuser(self): + class TestScript: + full_name = 'test.Script' + + def as_form(self, data, files): + form = MagicMock() + form.is_valid.return_value = True + form.cleaned_data = { + '_schedule_at': None, + '_interval': None, + '_commit': None, + } + form.errors.get_json_data.return_value = {} + return form + + script_obj = SimpleNamespace(python_class=TestScript) + job = SimpleNamespace(duration='0 seconds') + + with ( + patch( + 'extras.management.commands.runscript.get_module_and_script', + return_value=(None, script_obj), + ), + patch( + 'extras.management.commands.runscript.ScriptJob.enqueue', + return_value=job, + ) as enqueue, + patch('extras.management.commands.runscript.logging.getLogger'), + ): + call_command('runscript', 'test.Script', stdout=StringIO()) + + self.assertEqual(enqueue.call_args.kwargs['user'], self.user) + + +class WebhookReceiverTestCase(TestCase): + def test_starts_http_server(self): + out = StringIO() + + with ( + patch('extras.management.commands.webhook_receiver.HTTPServer') as http_server, + patch.object(WebhookHandler, 'show_headers', True), + ): + server = http_server.return_value + server.serve_forever.side_effect = KeyboardInterrupt + + call_command( + 'webhook_receiver', + port=9999, + no_headers=True, + stdout=out, + ) + + self.assertFalse(WebhookHandler.show_headers) + + http_server.assert_called_once_with(('localhost', 9999), WebhookHandler) + server.serve_forever.assert_called_once_with() + self.assertIn('Listening on port http://localhost:9999', out.getvalue()) + self.assertIn('Exiting', out.getvalue()) + + def test_handler_routes_arbitrary_http_methods(self): + handler = object.__new__(WebhookHandler) + + self.assertEqual(handler.__getattr__('do_PATCH').__func__, WebhookHandler.do_ANY) + with self.assertRaises(AttributeError): + handler.__getattr__('missing') + + def test_handler_logs_request_message(self): + handler = object.__new__(WebhookHandler) + handler.date_time_string = MagicMock(return_value='now') + handler.address_string = MagicMock(return_value='127.0.0.1') + + with ( + patch('extras.management.commands.webhook_receiver.request_counter', 7), + patch('builtins.print') as print_, + ): + handler.log_message('%s', 'message') + + print_.assert_called_once_with('[7] now 127.0.0.1 message') + + def test_handler_accepts_json_request_body(self): + handler = object.__new__(WebhookHandler) + body = b'{"ok": true}' + handler.headers = { + 'Content-Length': str(len(body)), + 'Content-Type': 'application/json', + 'X-Test': 'value', + } + handler.rfile = BytesIO(body) + handler.wfile = BytesIO() + handler.send_response = MagicMock() + handler.end_headers = MagicMock() + handler.show_headers = True + + with ( + patch('extras.management.commands.webhook_receiver.request_counter', 1), + patch('builtins.print') as print_, + ): + handler.do_ANY() + self.assertEqual(webhook_receiver.request_counter, 2) + + handler.send_response.assert_called_once_with(200) + handler.end_headers.assert_called_once_with() + self.assertEqual(handler.wfile.getvalue(), b'Webhook received!\n') + print_.assert_any_call('X-Test: value') + print_.assert_any_call('{\n "ok": true\n}') + print_.assert_any_call('Completed request #1') + + def test_handler_prints_no_body_when_content_length_is_missing(self): + handler = object.__new__(WebhookHandler) + handler.headers = {} + handler.rfile = BytesIO() + handler.wfile = BytesIO() + handler.send_response = MagicMock() + handler.end_headers = MagicMock() + handler.show_headers = False + + with ( + patch('extras.management.commands.webhook_receiver.request_counter', 1), + patch('builtins.print') as print_, + ): + handler.do_ANY() + + print_.assert_any_call('(No body)') + print_.assert_any_call('Completed request #1') diff --git a/netbox/extras/tests/test_models.py b/netbox/extras/tests/test_models.py index a58bf0bca..d827c641b 100644 --- a/netbox/extras/tests/test_models.py +++ b/netbox/extras/tests/test_models.py @@ -1,6 +1,7 @@ import io import tempfile from pathlib import Path +from types import SimpleNamespace from unittest.mock import patch from django.contrib.contenttypes.models import ContentType @@ -9,11 +10,13 @@ from django.core.files.storage import Storage from django.core.files.uploadedfile import SimpleUploadedFile from django.forms import ValidationError from django.test import TestCase, tag +from jinja2 import DebugUndefined, StrictUndefined, TemplateError, TemplateSyntaxError, UndefinedError from PIL import Image from core.events import OBJECT_CREATED from core.models import AutoSyncRecord, DataSource, ObjectType from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup +from extras.constants import DEFAULT_MIME_TYPE from extras.models import ( ConfigContext, ConfigContextProfile, @@ -21,11 +24,15 @@ from extras.models import ( EventRule, ExportTemplate, ImageAttachment, + TableConfig, Tag, TaggedItem, ) +from extras.models.mixins import RenderTemplateMixin from tenancy.models import Tenant, TenantGroup from utilities.exceptions import AbortRequest +from utilities.jinja2 import env_filter, render_jinja2 +from utilities.tables import get_table_for_model from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -67,7 +74,7 @@ class OverwriteStyleMemoryStorage(Storage): return f'https://example.invalid/{name}' -class ImageAttachmentTests(TestCase): +class ImageAttachmentTestCase(TestCase): @classmethod def setUpTestData(cls): cls.ct_rack = ContentType.objects.get_by_natural_key('dcim', 'rack') @@ -185,7 +192,26 @@ class ImageAttachmentTests(TestCase): self.assertCountEqual(storage.files.keys(), {base_name, suffixed_name}) -class TagTest(TestCase): +class TableConfigTestCase(TestCase): + @classmethod + def setUpTestData(cls): + cls.site_ct = ContentType.objects.get_for_model(Site) + cls.table_name = get_table_for_model(Site).__name__ + + def test_clean_accepts_ordering_none(self): + """clean() must accept ordering=None (field is null=True).""" + tc = TableConfig( + object_type=self.site_ct, + table=self.table_name, + name='No ordering', + columns=['name'], + # ordering left unset (defaults to None) + ) + # Must not raise TypeError: 'NoneType' object is not iterable + tc.full_clean() + + +class TagTestCase(TestCase): def test_default_ordering_weight_then_name_is_set(self): Tag.objects.create(name='Tag 1', slug='tag-1', weight=3000) @@ -244,7 +270,7 @@ class TagTest(TestCase): sitegroup.tags.add(tag) -class ConfigContextTest(TestCase): +class ConfigContextTestCase(TestCase): """ These test cases deal with the weighting, ordering, and deep merge logic of config context data. @@ -792,7 +818,7 @@ class ConfigContextTest(TestCase): self.assertTrue(distinct_subqueries[0].distinct) -class ConfigTemplateTest(TestCase): +class ConfigTemplateTestCase(TestCase): """ TODO: These test cases deal with the weighting, ordering, and deep merge logic of config context data. """ @@ -905,7 +931,7 @@ class ConfigTemplateTest(TestCase): self.assertEqual(autosync_records.count(), 0, "AutoSyncRecord should be deleted after detaching") -class ConfigTemplateDebugTest(TestCase): +class ConfigTemplateDebugTestCase(TestCase): """ Tests for the ConfigTemplate debug field and its effect on template rendering error output. """ @@ -925,35 +951,71 @@ class ConfigTemplateDebugTest(TestCase): def test_template_error_non_debug_no_traceback(self): """In non-debug mode, a TemplateError raises with no traceback exposure.""" - from jinja2 import TemplateError t = self._make_template("{{ unclosed", debug=False) with self.assertRaises(TemplateError): t.render({}) def test_template_error_debug_mode_raises(self): """In debug mode, a TemplateError still raises (callers handle display).""" - from jinja2 import TemplateError t = self._make_template("{{ unclosed", debug=True) with self.assertRaises(TemplateError): t.render({}) def test_render_jinja2_debug_extension_enabled(self): """When debug=True, the Jinja2 debug extension is loaded in the environment.""" - from utilities.jinja2 import render_jinja2 # The {% debug %} tag is only available when the debug extension is loaded. output = render_jinja2("{% debug %}", {}, debug=True) self.assertIsInstance(output, str) def test_render_jinja2_debug_extension_not_loaded_by_default(self): """When debug=False, the {% debug %} tag is not available.""" - from jinja2 import TemplateSyntaxError - - from utilities.jinja2 import render_jinja2 with self.assertRaises(TemplateSyntaxError): render_jinja2("{% debug %}", {}, debug=False) -class ExportTemplateContextTest(TestCase): +class JinjaEnvFilterTestCase(TestCase): + """ + Tests for the env() Jinja2 filter and the JINJA_ENVIRONMENT_PARAMS configuration parameter. + """ + + def test_env_filter_returns_value_for_matching_name(self): + with patch.dict('os.environ', {'NETBOX_TEST_TOKEN': 'secret'}, clear=False), \ + self.settings(JINJA_ENVIRONMENT_PARAMS=['NETBOX_TEST_TOKEN']): + self.assertEqual(env_filter('NETBOX_TEST_TOKEN'), 'secret') + + def test_env_filter_returns_none_for_unmatched_name(self): + with patch.dict('os.environ', {'NETBOX_OTHER_TOKEN': 'secret'}, clear=False), \ + self.settings(JINJA_ENVIRONMENT_PARAMS=['NETBOX_TEST_TOKEN']): + self.assertIsNone(env_filter('NETBOX_OTHER_TOKEN')) + + def test_env_filter_wildcard_match(self): + with patch.dict('os.environ', {'NETBOX_TEST_TOKEN_1': 'one', 'NETBOX_TEST_TOKEN_2': 'two'}, clear=False), \ + self.settings(JINJA_ENVIRONMENT_PARAMS=['NETBOX_TEST_TOKEN_*']): + self.assertEqual(env_filter('NETBOX_TEST_TOKEN_1'), 'one') + self.assertEqual(env_filter('NETBOX_TEST_TOKEN_2'), 'two') + + def test_env_filter_returns_none_for_missing_env_var(self): + with self.settings(JINJA_ENVIRONMENT_PARAMS=['NETBOX_MISSING_VAR']): + self.assertIsNone(env_filter('NETBOX_MISSING_VAR')) + + def test_env_filter_empty_whitelist_returns_none(self): + with patch.dict('os.environ', {'NETBOX_TEST_TOKEN': 'secret'}, clear=False), \ + self.settings(JINJA_ENVIRONMENT_PARAMS=[]): + self.assertIsNone(env_filter('NETBOX_TEST_TOKEN')) + + def test_env_filter_registered_by_default(self): + with patch.dict('os.environ', {'NETBOX_TEST_TOKEN': 'secret'}, clear=False), \ + self.settings(JINJA_ENVIRONMENT_PARAMS=['NETBOX_TEST_TOKEN']): + output = render_jinja2("{{ 'NETBOX_TEST_TOKEN' | env }}", {}) + self.assertEqual(output, 'secret') + + def test_user_defined_filter_overrides_default(self): + with self.settings(JINJA2_FILTERS={'env': lambda name: 'overridden'}): + output = render_jinja2("{{ 'NETBOX_TEST_TOKEN' | env }}", {}) + self.assertEqual(output, 'overridden') + + +class ExportTemplateContextTestCase(TestCase): """ Tests for ExportTemplate.get_context() including public model population. """ @@ -986,7 +1048,188 @@ class ExportTemplateContextTest(TestCase): self.assertIs(ctx['dcim']['Site'], Site) -class EventRuleTest(TestCase): +def finalize_none_to_dash(value): + """ + Module-level helper used by RenderTemplateMixinRenderTestCase.test_environment_params_finalize_path_import. + Exported so it can be referenced by dotted path from a Jinja environment_params value. + """ + return '-' if value is None else value + + +class RenderTemplateMixinRenderTestCase(TestCase): + """ + Tests for RenderTemplateMixin.render() and get_environment_params(), exercised via ConfigTemplate. + """ + + def test_render_basic_context(self): + t = ConfigTemplate(name='basic', template_code='Hello {{ name }}') + self.assertEqual(t.render({'name': 'world'}), 'Hello world') + + def test_render_normalizes_crlf(self): + t = ConfigTemplate(name='crlf', template_code='line1\r\nline2\r\nline3') + self.assertEqual(t.render({}), 'line1\nline2\nline3') + + def test_render_passes_environment_params(self): + # With trim_blocks + lstrip_blocks, block tags don't emit their surrounding whitespace. + template_code = '{% if x %}\n {% if y %}\n VALUE\n {% endif %}\n{% endif %}' + plain = ConfigTemplate(name='plain', template_code=template_code) + trimmed = ConfigTemplate( + name='trimmed', + template_code=template_code, + environment_params={'trim_blocks': True, 'lstrip_blocks': True}, + ) + ctx = {'x': True, 'y': True} + self.assertNotEqual(plain.render(ctx), trimmed.render(ctx)) + self.assertEqual(trimmed.render(ctx).strip(), 'VALUE') + + def test_environment_params_undefined_path_import(self): + # Default Undefined renders nothing for a missing variable. + default = ConfigTemplate(name='default', template_code='{{ missing }}') + self.assertEqual(default.render({}), '') + + # StrictUndefined (resolved from its dotted path) raises on access. + strict = ConfigTemplate( + name='strict', + template_code='{{ missing }}', + environment_params={'undefined': 'jinja2.StrictUndefined'}, + ) + with self.assertRaises(UndefinedError): + strict.render({}) + + def test_environment_params_finalize_legacy_resolution(self): + """ + Existing finalize values continue to resolve via import_string() as a + legacy carve-out (CVE-2026-29514). New use is blocked by clean(). + """ + t = ConfigTemplate( + name='finalize', + template_code='{{ v }}', + environment_params={'finalize': 'extras.tests.test_models.finalize_none_to_dash'}, + ) + self.assertEqual(t.render({'v': None}), '-') + self.assertEqual(t.render({'v': 'abc'}), 'abc') + + def test_get_environment_params_handles_none(self): + # The environment_params field may be cleared; ensure the mixin returns a dict (not None). + t = ConfigTemplate(name='empty', template_code='ok', environment_params=None) + self.assertEqual(t.get_environment_params(), {}) + + def test_get_environment_params_resolves_path_imports(self): + t = ConfigTemplate( + name='resolve', + template_code='ok', + environment_params={'undefined': 'jinja2.StrictUndefined', 'trim_blocks': True}, + ) + params = t.get_environment_params() + self.assertIs(params['undefined'], StrictUndefined) + self.assertIs(params['trim_blocks'], True) + + def test_get_environment_params_does_not_mutate_field(self): + # Resolving path imports must not replace the string values stored on the model field. + t = ConfigTemplate( + name='no-mutate', + template_code='ok', + environment_params={'undefined': 'jinja2.StrictUndefined'}, + ) + t.get_environment_params() + t.get_environment_params() + self.assertEqual(t.environment_params, {'undefined': 'jinja2.StrictUndefined'}) + + +class RenderTemplateMixinResponseTestCase(TestCase): + """ + Tests for RenderTemplateMixin.render_to_response() HTTP behavior. + """ + + def test_response_default_mime_type(self): + t = ConfigTemplate(name='t', template_code='ok') + response = t.render_to_response({}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], DEFAULT_MIME_TYPE) + + def test_response_custom_mime_type(self): + t = ConfigTemplate(name='t', template_code='{}', mime_type='application/json') + response = t.render_to_response({}) + self.assertEqual(response['Content-Type'], 'application/json') + + def test_response_attachment_with_file_name(self): + t = ConfigTemplate( + name='t', template_code='ok', file_name='router1', file_extension='cfg', as_attachment=True, + ) + response = t.render_to_response({}) + self.assertEqual(response['Content-Disposition'], 'attachment; filename="router1.cfg"') + + def test_response_attachment_filename_from_queryset(self): + Site.objects.create(name='Site 1', slug='site-1') + t = ExportTemplate( + name='t', + template_code='{% for obj in queryset %}{{ obj.name }}{% endfor %}', + file_extension='txt', + as_attachment=True, + ) + response = t.render_to_response(queryset=Site.objects.all()) + self.assertEqual(response['Content-Disposition'], 'attachment; filename="netbox_sites.txt"') + + def test_response_attachment_filename_from_device_context(self): + t = ConfigTemplate(name='t', template_code='ok', as_attachment=True) + device = SimpleNamespace(name='router1') + response = t.render_to_response(context={'device': device}) + self.assertEqual(response['Content-Disposition'], 'attachment; filename="router1"') + + def test_response_attachment_fallback_filename(self): + # No file_name, no queryset, no device/vm key in context: filename falls back to "output". + t = ConfigTemplate(name='t', template_code='ok', as_attachment=True) + response = t.render_to_response({}) + self.assertEqual(response['Content-Disposition'], 'attachment; filename="output"') + + def test_response_as_attachment_false_omits_disposition(self): + t = ConfigTemplate(name='t', template_code='ok', file_name='router1', as_attachment=False) + response = t.render_to_response({}) + self.assertNotIn('Content-Disposition', response) + + def test_response_body_matches_render(self): + t = ConfigTemplate(name='t', template_code='Hello {{ name }}') + rendered = t.render({'name': 'world'}) + response = t.render_to_response({'name': 'world'}) + self.assertEqual(response.content.decode(), rendered) + + +class ExportTemplateRenderTestCase(TestCase): + """ + Tests for ExportTemplate.render() with a queryset bound into the template context. + """ + + @classmethod + def setUpTestData(cls): + Site.objects.bulk_create([ + Site(name='Site A', slug='site-a'), + Site(name='Site B', slug='site-b'), + Site(name='Site C', slug='site-c'), + ]) + + def test_render_iterates_queryset(self): + t = ExportTemplate( + name='sites', + template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}', + ) + queryset = Site.objects.order_by('name') + output = t.render(queryset=queryset) + self.assertEqual(output, 'Site A\nSite B\nSite C\n') + + def test_render_to_response_for_queryset(self): + t = ExportTemplate( + name='sites', + template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}', + file_extension='txt', + ) + response = t.render_to_response(queryset=Site.objects.order_by('name')) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], DEFAULT_MIME_TYPE) + self.assertEqual(response['Content-Disposition'], 'attachment; filename="netbox_sites.txt"') + self.assertEqual(response.content.decode(), 'Site A\nSite B\nSite C\n') + + +class EventRuleTestCase(TestCase): def test_action_data_clean_accepts_dict(self): """ @@ -1005,3 +1248,199 @@ class EventRuleTest(TestCase): with self.assertRaises(ValidationError) as cm: rule.clean() self.assertIn('action_data', cm.exception.message_dict) + + +class JinjaEnvironmentParamsCleanTestCase(TestCase): + """Tests for RenderTemplateMixin.clean() validation of environment_params.""" + + def _make_template(self, environment_params): + return ConfigTemplate( + name='test', + template_code='{{ "test" }}', + environment_params=environment_params, + ) + + def test_allowed_scalar_params_pass(self): + template = self._make_template({'trim_blocks': True, 'lstrip_blocks': True}) + template.clean() + + def test_autoescape_boolean_passes(self): + template = self._make_template({'autoescape': True}) + template.clean() + + def test_valid_undefined_passes(self): + for value in ( + 'jinja2.Undefined', + 'jinja2.ChainableUndefined', + 'jinja2.DebugUndefined', + 'jinja2.StrictUndefined', + ): + template = self._make_template({'undefined': value}) + template.clean() + + def test_invalid_undefined_rejected(self): + template = self._make_template({'undefined': 'subprocess.getoutput'}) + with self.assertRaises(ValidationError) as cm: + template.clean() + self.assertIn('environment_params', cm.exception.message_dict) + + def test_unknown_key_rejected(self): + template = self._make_template({'extensions': ['os']}) + with self.assertRaises(ValidationError) as cm: + template.clean() + self.assertIn('environment_params', cm.exception.message_dict) + + def test_finalize_blocked_from_new_use(self): + template = self._make_template({'finalize': 'subprocess.getoutput'}) + with self.assertRaises(ValidationError) as cm: + template.clean() + self.assertIn('environment_params', cm.exception.message_dict) + + def test_empty_params_pass(self): + template = self._make_template({}) + template.clean() + + def test_none_params_pass(self): + template = self._make_template(None) + template.clean() + + def test_exporttemplate_clean_rejects_unknown_key(self): + """MRO smoke test: ExportTemplate.clean() reaches RenderTemplateMixin.clean().""" + obj = ExportTemplate( + name='test', + template_code='{{ "test" }}', + environment_params={'loader': 'some.loader'}, + ) + with self.assertRaises(ValidationError) as cm: + obj.clean() + self.assertIn('environment_params', cm.exception.message_dict) + + def test_configtemplate_clean_rejects_finalize(self): + """MRO smoke test: ConfigTemplate.clean() reaches RenderTemplateMixin.clean().""" + obj = ConfigTemplate( + name='test', + template_code='{{ "test" }}', + environment_params={'finalize': 'subprocess.getoutput'}, + ) + with self.assertRaises(ValidationError) as cm: + obj.clean() + self.assertIn('environment_params', cm.exception.message_dict) + + +class JinjaEnvironmentParamsFilterTestCase(TestCase): + """Tests for RenderTemplateMixin._filter_environment_params().""" + + def test_allowed_keys_pass_through(self): + params = {'trim_blocks': True, 'autoescape': False} + result = RenderTemplateMixin._filter_environment_params(params) + self.assertEqual(result, params) + + def test_unknown_keys_stripped(self): + params = {'extensions': ['os'], 'loader': 'x', 'trim_blocks': True} + result = RenderTemplateMixin._filter_environment_params(params) + self.assertEqual(result, {'trim_blocks': True}) + + def test_finalize_preserved_as_legacy(self): + params = {'finalize': 'some.module.func', 'trim_blocks': True} + result = RenderTemplateMixin._filter_environment_params(params) + self.assertEqual(result, params) + + def test_empty_params(self): + self.assertEqual(RenderTemplateMixin._filter_environment_params({}), {}) + + +class JinjaEnvironmentParamsResolveTestCase(TestCase): + """Tests for RenderTemplateMixin._resolve_mapped_params().""" + + def test_undefined_resolved_to_class(self): + params = {'undefined': 'jinja2.StrictUndefined'} + result = RenderTemplateMixin._resolve_mapped_params(params) + self.assertIs(result['undefined'], StrictUndefined) + + def test_unrecognized_undefined_value_passed_through(self): + params = {'undefined': 'not.a.real.class'} + result = RenderTemplateMixin._resolve_mapped_params(params) + self.assertEqual(result['undefined'], 'not.a.real.class') + + def test_scalar_params_passed_through(self): + params = {'trim_blocks': True, 'autoescape': False} + result = RenderTemplateMixin._resolve_mapped_params(params) + self.assertEqual(result, params) + + def test_empty_params(self): + self.assertEqual(RenderTemplateMixin._resolve_mapped_params({}), {}) + + +class JinjaEnvironmentParamsFinalizeTestCase(TestCase): + """Tests for RenderTemplateMixin._resolve_finalize() legacy carve-out.""" + + def test_finalize_string_resolved_via_import_string(self): + params = {'finalize': 'extras.tests.test_models.finalize_none_to_dash'} + result = RenderTemplateMixin._resolve_finalize(params) + self.assertIs(result['finalize'], finalize_none_to_dash) + + def test_finalize_non_string_passed_through(self): + params = {'finalize': 42} + result = RenderTemplateMixin._resolve_finalize(params) + self.assertEqual(result['finalize'], 42) + + def test_no_finalize_key_unchanged(self): + params = {'trim_blocks': True} + result = RenderTemplateMixin._resolve_finalize(params) + self.assertEqual(result, {'trim_blocks': True}) + + def test_invalid_import_path_raises_import_error(self): + params = {'finalize': 'nonexistent.module.func'} + with self.assertRaises(ImportError): + RenderTemplateMixin._resolve_finalize(params) + + def test_empty_params(self): + self.assertEqual(RenderTemplateMixin._resolve_finalize({}), {}) + + +class JinjaEnvironmentParamsIntegrationTestCase(TestCase): + """Integration tests for get_environment_params() end-to-end.""" + + def _make_template(self, environment_params): + return ConfigTemplate( + name='test', + template_code='{{ "test" }}', + environment_params=environment_params, + ) + + def test_full_pipeline_with_undefined(self): + template = self._make_template({'undefined': 'jinja2.StrictUndefined', 'trim_blocks': True}) + params = template.get_environment_params() + self.assertIs(params['undefined'], StrictUndefined) + self.assertIs(params['trim_blocks'], True) + + def test_full_pipeline_strips_unknown_and_resolves(self): + template = self._make_template({ + 'extensions': ['os'], + 'undefined': 'jinja2.DebugUndefined', + 'trim_blocks': True, + }) + params = template.get_environment_params() + self.assertNotIn('extensions', params) + self.assertIs(params['undefined'], DebugUndefined) + self.assertIs(params['trim_blocks'], True) + + def test_full_pipeline_finalize_resolves(self): + template = self._make_template({ + 'finalize': 'extras.tests.test_models.finalize_none_to_dash', + }) + params = template.get_environment_params() + self.assertIs(params['finalize'], finalize_none_to_dash) + + def test_does_not_mutate_stored_value(self): + template = self._make_template({'undefined': 'jinja2.StrictUndefined'}) + template.get_environment_params() + self.assertEqual(template.environment_params['undefined'], 'jinja2.StrictUndefined') + + def test_none_environment_params(self): + template = self._make_template(None) + self.assertEqual(template.get_environment_params(), {}) + + def test_empty_environment_params(self): + template = self._make_template({}) + self.assertEqual(template.get_environment_params(), {}) diff --git a/netbox/extras/tests/test_scripts.py b/netbox/extras/tests/test_scripts.py index bd4033e92..fab047484 100644 --- a/netbox/extras/tests/test_scripts.py +++ b/netbox/extras/tests/test_scripts.py @@ -32,7 +32,7 @@ JSON_DATA = """ """ -class ScriptVariablesTest(TestCase): +class ScriptVariablesTestCase(TestCase): def test_stringvar(self): diff --git a/netbox/extras/tests/test_signals.py b/netbox/extras/tests/test_signals.py new file mode 100644 index 000000000..2a8fb29e6 --- /dev/null +++ b/netbox/extras/tests/test_signals.py @@ -0,0 +1,297 @@ +import uuid +from unittest.mock import patch + +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError +from django.test import RequestFactory, TestCase, override_settings + +from core.events import JOB_COMPLETED, JOB_STARTED, OBJECT_DELETED, OBJECT_UPDATED +from core.models import Job, ObjectType +from core.signals import job_end, job_start +from dcim.choices import SiteStatusChoices +from dcim.models import Region, Site +from extras import signals +from extras.choices import CustomFieldTypeChoices, EventRuleActionChoices +from extras.models import CustomField, EventRule, Notification, Subscription, Tag, Webhook +from extras.validators import CustomValidator +from netbox.context_managers import event_tracking +from users.models import User +from utilities.exceptions import AbortRequest + + +def _build_request(user=None): + request = RequestFactory().get('/') + request.id = uuid.uuid4() + request.user = user + return request + + +class CustomFieldRenameSignalTestCase(TestCase): + """ + Verify extras.signals.handle_cf_renamed migrates stored custom-field data when a + CustomField is renamed. + """ + + def test_renaming_custom_field_moves_object_data(self): + site_type = ContentType.objects.get_for_model(Site) + cf = CustomField.objects.create( + name='asset_tag', + type=CustomFieldTypeChoices.TYPE_TEXT, + ) + cf.object_types.set([site_type]) + site = Site.objects.create(name='Site 1', slug='site-1', custom_field_data={'asset_tag': 'A123'}) + + cf.name = 'inventory_id' + cf.save() + + site.refresh_from_db() + self.assertNotIn('asset_tag', site.custom_field_data) + self.assertEqual(site.custom_field_data['inventory_id'], 'A123') + + def test_creating_custom_field_does_not_attempt_rename(self): + # Should not raise — newly-created custom fields have no prior name to migrate. + site_type = ContentType.objects.get_for_model(Site) + cf = CustomField.objects.create( + name='cf1', + type=CustomFieldTypeChoices.TYPE_TEXT, + ) + cf.object_types.set([site_type]) + + +class CustomFieldDeletedSignalTestCase(TestCase): + """ + Verify extras.signals.handle_cf_deleted strips stale data from associated objects + when a CustomField is deleted. + """ + + def test_deleting_custom_field_clears_object_data(self): + site_type = ContentType.objects.get_for_model(Site) + cf = CustomField.objects.create( + name='asset_tag', + type=CustomFieldTypeChoices.TYPE_TEXT, + ) + cf.object_types.set([site_type]) + site = Site.objects.create(name='Site 1', slug='site-1', custom_field_data={'asset_tag': 'A123'}) + + cf.delete() + + site.refresh_from_db() + self.assertNotIn('asset_tag', site.custom_field_data) + + +class CustomFieldObjectTypeSignalTestCase(TestCase): + """ + Verify extras.signals.handle_cf_added_obj_types and handle_cf_removed_obj_types + populate or strip default values when a CustomField's object_types m2m changes. + """ + + def test_adding_object_type_populates_default_value(self): + site_type = ContentType.objects.get_for_model(Site) + Site.objects.create(name='Site 1', slug='site-1') + cf = CustomField.objects.create( + name='asset_tag', + type=CustomFieldTypeChoices.TYPE_TEXT, + default='UNTAGGED', + ) + + cf.object_types.set([site_type]) + + site = Site.objects.get(slug='site-1') + self.assertEqual(site.custom_field_data.get('asset_tag'), 'UNTAGGED') + + def test_removing_object_type_clears_stored_data(self): + site_type = ContentType.objects.get_for_model(Site) + cf = CustomField.objects.create( + name='asset_tag', + type=CustomFieldTypeChoices.TYPE_TEXT, + ) + cf.object_types.set([site_type]) + site = Site.objects.create(name='Site 1', slug='site-1', custom_field_data={'asset_tag': 'A123'}) + + cf.object_types.remove(site_type) + + site.refresh_from_db() + self.assertNotIn('asset_tag', site.custom_field_data) + + +class RunSaveValidatorsSignalTestCase(TestCase): + """ + Verify extras.signals.run_save_validators invokes any configured CUSTOM_VALIDATORS + when a model emits the post_clean signal. + """ + + @override_settings(CUSTOM_VALIDATORS={'dcim.site': [CustomValidator({'name': {'eq': 'allowed'}})]}) + def test_validation_failure_raises_validation_error(self): + with self.assertRaises(ValidationError): + Site(name='blocked', slug='blocked', status=SiteStatusChoices.STATUS_ACTIVE).clean() + + @override_settings(CUSTOM_VALIDATORS={'dcim.site': [CustomValidator({'name': {'eq': 'allowed'}})]}) + def test_validation_success_does_not_raise(self): + Site(name='allowed', slug='allowed', status=SiteStatusChoices.STATUS_ACTIVE).clean() + + +class ValidateAssignedTagsSignalTestCase(TestCase): + """ + Verify extras.signals.validate_assigned_tags rejects Tags that are restricted to + object types incompatible with the target object. + """ + + def test_restricted_tag_blocks_incompatible_object(self): + region_type = ContentType.objects.get_for_model(Region) + tag = Tag.objects.create(name='RegionOnly', slug='regiononly') + tag.object_types.set([region_type]) + site = Site.objects.create(name='Site 1', slug='site-1') + + with self.assertRaises(AbortRequest): + site.tags.add(tag) + + def test_restricted_tag_allows_compatible_object(self): + site_type = ContentType.objects.get_for_model(Site) + tag = Tag.objects.create(name='SiteOnly', slug='siteonly') + tag.object_types.set([site_type]) + site = Site.objects.create(name='Site 1', slug='site-1') + + site.tags.add(tag) + self.assertEqual(list(site.tags.all()), [tag]) + + def test_unrestricted_tag_is_always_permitted(self): + tag = Tag.objects.create(name='Anywhere', slug='anywhere') + site = Site.objects.create(name='Site 1', slug='site-1') + + site.tags.add(tag) + self.assertEqual(list(site.tags.all()), [tag]) + + +class JobEventRulesSignalTestCase(TestCase): + """ + Verify extras.signals.process_job_start_event_rules and process_job_end_event_rules + invoke any EventRule registered for JOB_STARTED / JOB_COMPLETED on the sender's + object_type. + """ + + @classmethod + def setUpTestData(cls): + cls.site_type = ObjectType.objects.get_for_model(Site) + webhook = Webhook.objects.create( + name='Webhook', + payload_url='http://localhost/', + secret='secret', + ) + webhook_type = ObjectType.objects.get_for_model(Webhook) + cls.start_rule = EventRule.objects.create( + name='Job Start Rule', + event_types=[JOB_STARTED], + action_type=EventRuleActionChoices.WEBHOOK, + action_object_type=webhook_type, + action_object_id=webhook.pk, + ) + cls.start_rule.object_types.set([cls.site_type]) + cls.end_rule = EventRule.objects.create( + name='Job End Rule', + event_types=[JOB_COMPLETED], + action_type=EventRuleActionChoices.WEBHOOK, + action_object_type=webhook_type, + action_object_id=webhook.pk, + ) + cls.end_rule.object_types.set([cls.site_type]) + + def _create_job(self): + return Job.objects.create( + object_type=self.site_type, + name='test-job', + job_id=uuid.uuid4(), + data={'foo': 1}, + ) + + def test_job_start_filters_to_matching_event_rules(self): + sender = self._create_job() + + with patch('extras.signals.process_event_rules') as process_event_rules: + job_start.send(sender=sender) + + process_event_rules.assert_called_once() + rules_qs = process_event_rules.call_args.args[0] + self.assertEqual(list(rules_qs.values_list('pk', flat=True)), [self.start_rule.pk]) + + def test_job_end_filters_to_matching_event_rules(self): + sender = self._create_job() + + with patch('extras.signals.process_event_rules') as process_event_rules: + job_end.send(sender=sender) + + process_event_rules.assert_called_once() + rules_qs = process_event_rules.call_args.args[0] + self.assertEqual(list(rules_qs.values_list('pk', flat=True)), [self.end_rule.pk]) + + +class NotifyObjectChangedSignalTestCase(TestCase): + """ + Verify extras.signals.notify_object_changed creates Notifications for subscribed + users on object update and deletion, and skips creation otherwise. + """ + + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user(username='alice', password='pw') + + def test_creating_object_does_not_create_notifications(self): + # Subscribe a user to the object BEFORE invoking the handler so the + # discriminating branch (created=True early return) is genuinely + # exercised. Without the subscription this assertion passes trivially. + site = Site.objects.create(name='Site 1', slug='site-1') + site_type = ContentType.objects.get_for_model(Site) + Subscription.objects.create(user=self.user, object_type=site_type, object_id=site.pk) + Notification.objects.all().delete() + + signals.notify_object_changed(sender=Site, instance=site, created=True) + + self.assertFalse(Notification.objects.exists()) + + def test_updating_subscribed_object_creates_update_notification(self): + site = Site.objects.create(name='Site 1', slug='site-1') + site_type = ContentType.objects.get_for_model(Site) + Subscription.objects.create(user=self.user, object_type=site_type, object_id=site.pk) + + site.description = 'updated' + site.save() + + notification = Notification.objects.get(user=self.user) + self.assertEqual(notification.object_type, site_type) + self.assertEqual(notification.object_id, site.pk) + self.assertEqual(notification.event_type, OBJECT_UPDATED) + + def test_deleting_subscribed_object_creates_delete_notification(self): + site = Site.objects.create(name='Site 1', slug='site-1') + site_type = ContentType.objects.get_for_model(Site) + Subscription.objects.create(user=self.user, object_type=site_type, object_id=site.pk) + + site.delete() + + notification = Notification.objects.get(user=self.user) + self.assertEqual(notification.event_type, OBJECT_DELETED) + + def test_updating_unsubscribed_object_creates_no_notification(self): + site = Site.objects.create(name='Site 1', slug='site-1') + + site.description = 'updated' + site.save() + + self.assertEqual(Notification.objects.count(), 0) + + def test_updating_object_replaces_existing_notification(self): + site = Site.objects.create(name='Site 1', slug='site-1') + site_type = ContentType.objects.get_for_model(Site) + Subscription.objects.create(user=self.user, object_type=site_type, object_id=site.pk) + + # Trigger two updates within a single request; only one Notification should remain. + request = _build_request(user=self.user) + with event_tracking(request): + site.description = 'first' + site.save() + site.description = 'second' + site.save() + + self.assertEqual( + Notification.objects.filter(user=self.user, object_type=site_type, object_id=site.pk).count(), + 1, + ) diff --git a/netbox/extras/tests/test_tables.py b/netbox/extras/tests/test_tables.py index fb20a7e2e..1fbd1fe2b 100644 --- a/netbox/extras/tests/test_tables.py +++ b/netbox/extras/tests/test_tables.py @@ -3,31 +3,31 @@ from extras.tables import * from utilities.testing import TableTestCases -class CustomFieldTableTest(TableTestCases.StandardTableTestCase): +class CustomFieldTableTestCase(TableTestCases.StandardTableTestCase): table = CustomFieldTable -class CustomFieldChoiceSetTableTest(TableTestCases.StandardTableTestCase): +class CustomFieldChoiceSetTableTestCase(TableTestCases.StandardTableTestCase): table = CustomFieldChoiceSetTable -class CustomLinkTableTest(TableTestCases.StandardTableTestCase): +class CustomLinkTableTestCase(TableTestCases.StandardTableTestCase): table = CustomLinkTable -class ExportTemplateTableTest(TableTestCases.StandardTableTestCase): +class ExportTemplateTableTestCase(TableTestCases.StandardTableTestCase): table = ExportTemplateTable -class SavedFilterTableTest(TableTestCases.StandardTableTestCase): +class SavedFilterTableTestCase(TableTestCases.StandardTableTestCase): table = SavedFilterTable -class TableConfigTableTest(TableTestCases.StandardTableTestCase): +class TableConfigTableTestCase(TableTestCases.StandardTableTestCase): table = TableConfigTable -class BookmarkTableTest(TableTestCases.StandardTableTestCase): +class BookmarkTableTestCase(TableTestCases.StandardTableTestCase): table = BookmarkTable # The list view for this table lives in account.views (not extras.views), @@ -37,11 +37,11 @@ class BookmarkTableTest(TableTestCases.StandardTableTestCase): ] -class NotificationGroupTableTest(TableTestCases.StandardTableTestCase): +class NotificationGroupTableTestCase(TableTestCases.StandardTableTestCase): table = NotificationGroupTable -class NotificationTableTest(TableTestCases.StandardTableTestCase): +class NotificationTableTestCase(TableTestCases.StandardTableTestCase): table = NotificationTable # The list view for this table lives in account.views (not extras.views), @@ -51,7 +51,7 @@ class NotificationTableTest(TableTestCases.StandardTableTestCase): ] -class SubscriptionTableTest(TableTestCases.StandardTableTestCase): +class SubscriptionTableTestCase(TableTestCases.StandardTableTestCase): table = SubscriptionTable # The list view for this table lives in account.views (not extras.views), @@ -61,33 +61,33 @@ class SubscriptionTableTest(TableTestCases.StandardTableTestCase): ] -class WebhookTableTest(TableTestCases.StandardTableTestCase): +class WebhookTableTestCase(TableTestCases.StandardTableTestCase): table = WebhookTable -class EventRuleTableTest(TableTestCases.StandardTableTestCase): +class EventRuleTableTestCase(TableTestCases.StandardTableTestCase): table = EventRuleTable -class TagTableTest(TableTestCases.StandardTableTestCase): +class TagTableTestCase(TableTestCases.StandardTableTestCase): table = TagTable -class ConfigContextProfileTableTest(TableTestCases.StandardTableTestCase): +class ConfigContextProfileTableTestCase(TableTestCases.StandardTableTestCase): table = ConfigContextProfileTable -class ConfigContextTableTest(TableTestCases.StandardTableTestCase): +class ConfigContextTableTestCase(TableTestCases.StandardTableTestCase): table = ConfigContextTable -class ConfigTemplateTableTest(TableTestCases.StandardTableTestCase): +class ConfigTemplateTableTestCase(TableTestCases.StandardTableTestCase): table = ConfigTemplateTable -class ImageAttachmentTableTest(TableTestCases.StandardTableTestCase): +class ImageAttachmentTableTestCase(TableTestCases.StandardTableTestCase): table = ImageAttachmentTable -class JournalEntryTableTest(TableTestCases.StandardTableTestCase): +class JournalEntryTableTestCase(TableTestCases.StandardTableTestCase): table = JournalEntryTable diff --git a/netbox/extras/tests/test_tags.py b/netbox/extras/tests/test_tags.py index b827b28d9..f2f61f4ec 100644 --- a/netbox/extras/tests/test_tags.py +++ b/netbox/extras/tests/test_tags.py @@ -1,11 +1,16 @@ +from django.apps import apps +from django.db import models +from django.test import SimpleTestCase from django.urls import reverse from rest_framework import status from dcim.models import Site +from extras.managers import NetBoxTaggableManagerField +from netbox.models.features import TagsMixin from utilities.testing import APITestCase, create_tags -class TaggedItemTest(APITestCase): +class TaggedItemTestCase(APITestCase): """ Test the application of Tags to and item (a Site, for example) upon creation (POST) and modification (PATCH). """ @@ -77,3 +82,47 @@ class TaggedItemTest(APITestCase): self.assertEqual(len(response.data['tags']), 0) site = Site.objects.get(pk=response.data['id']) self.assertEqual(len(site.tags.all()), 0) + + +class TagsMixinCollisionTestCase(SimpleTestCase): + """ + Two TagsMixin-derived models that share a class name (e.g. introduced by separate plugins) + must not collide on Tag's reverse accessor. Regression test for #22301. + """ + def _make_taggable_model(self, app_label, name='Foo'): + # Build a model dynamically and register a cleanup to remove it from the global app + # registry so subsequent test runs (and other tests in this process) don't see ghost + # models leak in via apps.get_models(). + model = type(name, (TagsMixin, models.Model), { + '__module__': self.__class__.__module__, + 'Meta': type('Meta', (), {'app_label': app_label}), + }) + self.addCleanup(apps.all_models[app_label].pop, name.lower(), None) + self.addCleanup(apps.clear_cache) + return model + + def test_same_named_taggable_models_in_different_apps(self): + foo_a = self._make_taggable_model('dcim') + foo_b = self._make_taggable_model('ipam') + + # Without the fix, Django's system checks raise fields.E304 on these two models. + self.assertEqual(foo_a.check(), []) + self.assertEqual(foo_b.check(), []) + + # Each model should resolve to its own unique related_name on the Tag relation. + rn_a = foo_a._meta.get_field('tags').remote_field.related_name + rn_b = foo_b._meta.get_field('tags').remote_field.related_name + self.assertNotEqual(rn_a, rn_b) + + def test_deconstruct_emits_upstream_path(self): + """ + NetBoxTaggableManagerField.deconstruct() must emit the upstream taggit path and drop + related_name, so existing migrations remain valid and no AlterField is produced for + every TagsMixin consumer. + """ + model = self._make_taggable_model('dcim', name='DeconstructProbe') + field = model._meta.get_field('tags') + self.assertIsInstance(field, NetBoxTaggableManagerField) + _name, path, _args, kwargs = field.deconstruct() + self.assertEqual(path, 'taggit.managers.TaggableManager') + self.assertNotIn('related_name', kwargs) diff --git a/netbox/extras/tests/test_utils.py b/netbox/extras/tests/test_utils.py index 1a80c8121..b4ff32563 100644 --- a/netbox/extras/tests/test_utils.py +++ b/netbox/extras/tests/test_utils.py @@ -11,7 +11,7 @@ from tenancy.models import ContactGroup, TenantGroup from wireless.models import WirelessLANGroup -class FilenameFromModelTests(TestCase): +class FilenameFromModelTestCase(TestCase): def test_expected_output(self): cases = ( (ExportTemplate, 'netbox_export_templates'), @@ -43,7 +43,7 @@ class OverwriteStyleStorage(Storage): return f'{file_root}_sdmmer4{file_ext}' -class ImageUploadTests(TestCase): +class ImageUploadTestCase(TestCase): @classmethod def setUpTestData(cls): # We only need a ContentType with model="rack" for the prefix; diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 8d28f4602..74c88cea8 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -1,18 +1,19 @@ +import uuid from unittest.mock import PropertyMock, patch from django.contrib.contenttypes.models import ContentType from django.test import tag from django.urls import reverse -from core.choices import ManagedFileRootPathChoices +from core.choices import JobStatusChoices, ManagedFileRootPathChoices from core.events import * -from core.models import ObjectType +from core.models import Job, ObjectType from dcim.models import DeviceType, Manufacturer, Site from extras.choices import * from extras.models import * from extras.scripts import BooleanVar, IntegerVar from extras.scripts import Script as PythonClass -from users.models import Group, User +from users.models import Group, ObjectPermission, User from utilities.testing import TestCase, ViewTestCases @@ -181,6 +182,27 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase): } +class CustomLinkRenderingTestCase(TestCase): + user_permissions = ['dcim.view_site'] + + def test_view_object_with_custom_link(self): + customlink = CustomLink( + name='Test', + link_text='FOO {{ object.name }} BAR', + link_url='http://example.com/?site={{ object.slug }}', + new_window=False + ) + customlink.save() + customlink.object_types.set([ObjectType.objects.get_for_model(Site)]) + + site = Site(name='Test Site', slug='test-site') + site.save() + + response = self.client.get(site.get_absolute_url(), follow=True) + self.assertEqual(response.status_code, 200) + self.assertIn(f'FOO {site.name} BAR', str(response.content)) + + class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = SavedFilter @@ -252,6 +274,53 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase): } +class TableConfigTestCase( + ViewTestCases.GetObjectViewTestCase, + ViewTestCases.GetObjectChangelogViewTestCase, + ViewTestCases.ListObjectsViewTestCase, + ViewTestCases.DeleteObjectViewTestCase, + ViewTestCases.BulkDeleteObjectsViewTestCase, +): + # Add/Edit/BulkEdit views require an object_type pre-context from the source + # table view, so they are not exercised here. + model = TableConfig + + @classmethod + def setUpTestData(cls): + site_type = ObjectType.objects.get_for_model(Site) + users = ( + User(username='User 1'), + User(username='User 2'), + User(username='User 3'), + ) + User.objects.bulk_create(users) + + table_configs = ( + TableConfig( + name='Table Config 1', + object_type=site_type, + table='SiteTable', + user=users[0], + columns=['name', 'status'], + ), + TableConfig( + name='Table Config 2', + object_type=site_type, + table='SiteTable', + user=users[1], + columns=['name', 'region'], + ), + TableConfig( + name='Table Config 3', + object_type=site_type, + table='SiteTable', + user=users[2], + columns=['name', 'tenant'], + ), + ) + TableConfig.objects.bulk_create(table_configs) + + class BookmarkTestCase( ViewTestCases.DeleteObjectViewTestCase, ViewTestCases.ListObjectsViewTestCase, @@ -300,6 +369,52 @@ class BookmarkTestCase( return +class ImageAttachmentTestCase( + ViewTestCases.GetObjectViewTestCase, + ViewTestCases.ListObjectsViewTestCase, + ViewTestCases.DeleteObjectViewTestCase, + ViewTestCases.BulkDeleteObjectsViewTestCase, +): + # Add/Edit/BulkEdit are omitted: ImageField.save() re-reads the file to + # populate image_height / image_width, which fails when fixtures use + # placeholder URLs instead of real images on disk. + model = ImageAttachment + + @classmethod + def setUpTestData(cls): + ct = ContentType.objects.get_for_model(Site) + site = Site.objects.create(name='Site 1', slug='site-1') + + ImageAttachment.objects.bulk_create( + [ + ImageAttachment( + object_type=ct, + object_id=site.pk, + name='Image Attachment 1', + image='http://example.com/image1.png', + image_height=100, + image_width=100, + ), + ImageAttachment( + object_type=ct, + object_id=site.pk, + name='Image Attachment 2', + image='http://example.com/image2.png', + image_height=100, + image_width=100, + ), + ImageAttachment( + object_type=ct, + object_id=site.pk, + name='Image Attachment 3', + image='http://example.com/image3.png', + image_height=100, + image_width=100, + ), + ] + ) + + class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = ExportTemplate @@ -349,6 +464,59 @@ class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase): } +class ExportTemplateExportFlowTestCase(TestCase): + """ + End-to-end test for ExportTemplate invocation via a list view's ?export= query param. + """ + + @classmethod + def setUpTestData(cls): + Site.objects.bulk_create([ + Site(name='Site A', slug='site-a'), + Site(name='Site B', slug='site-b'), + ]) + + site_type = ObjectType.objects.get_for_model(Site) + + ok_template = ExportTemplate.objects.create( + name='Sites Export', + template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}', + mime_type='text/plain', + file_extension='txt', + ) + ok_template.object_types.set([site_type]) + + broken_template = ExportTemplate.objects.create( + name='Broken Export', + template_code='{% for obj in queryset %}{{ obj.name ', # unterminated expression + ) + broken_template.object_types.set([site_type]) + + def test_export_template_invocation(self): + self.add_permissions('dcim.view_site', 'extras.view_exporttemplate') + url = reverse('dcim:site_list') + + response = self.client.get(f'{url}?export=Sites Export') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/plain') + self.assertEqual(response['Content-Disposition'], 'attachment; filename="netbox_sites.txt"') + # The rendered queryset reflects whatever ordering the list view applies. Assert on set + # membership rather than line order so the test isn't coupled to Site's natural ordering. + rendered_names = set(filter(None, response.content.decode().split('\n'))) + self.assertEqual(rendered_names, {'Site A', 'Site B'}) + + def test_export_template_render_error_redirects(self): + self.add_permissions('dcim.view_site', 'extras.view_exporttemplate') + url = reverse('dcim:site_list') + + # A broken template surfaces an exception during render; the view catches it and redirects + # back to the (filtered) list view rather than returning a 500. + response = self.client.get(f'{url}?export=Broken Export') + self.assertEqual(response.status_code, 302) + self.assertTrue(response['Location'].startswith(url)) + self.assertNotIn('export=', response['Location']) + + class WebhookTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = Webhook @@ -703,27 +871,6 @@ class JournalEntryTestCase( } -class CustomLinkTest(TestCase): - user_permissions = ['dcim.view_site'] - - def test_view_object_with_custom_link(self): - customlink = CustomLink( - name='Test', - link_text='FOO {{ object.name }} BAR', - link_url='http://example.com/?site={{ object.slug }}', - new_window=False - ) - customlink.save() - customlink.object_types.set([ObjectType.objects.get_for_model(Site)]) - - site = Site(name='Test Site', slug='test-site') - site.save() - - response = self.client.get(site.get_absolute_url(), follow=True) - self.assertEqual(response.status_code, 200) - self.assertIn(f'FOO {site.name} BAR', str(response.content)) - - class SubscriptionTestCase( ViewTestCases.CreateObjectViewTestCase, ViewTestCases.DeleteObjectViewTestCase, @@ -887,7 +1034,7 @@ class NotificationTestCase( return -class ScriptListViewTest(TestCase): +class ScriptListViewTestCase(TestCase): user_permissions = ['extras.view_script'] def test_script_list_embedded_parameter(self): @@ -905,7 +1052,7 @@ class ScriptListViewTest(TestCase): self.assertTemplateUsed(response, 'extras/inc/script_list_content.html') -class ScriptValidationErrorTest(TestCase): +class ScriptValidationErrorTestCase(TestCase): user_permissions = ['extras.view_script', 'extras.run_script'] class TestScriptMixin: @@ -936,13 +1083,13 @@ class ScriptValidationErrorTest(TestCase): def setUp(self): super().setUp() - Script.python_class = property(lambda self: ScriptValidationErrorTest.TestScriptClass) + Script.python_class = property(lambda self: ScriptValidationErrorTestCase.TestScriptClass) @tag('regression') def test_script_validation_error_displays_message(self): url = reverse('extras:script', kwargs={'pk': self.script.pk}) - with patch('extras.views.get_workers_for_queue', return_value=['worker']): + with patch('extras.views.any_workers_for_queue', return_value=True): response = self.client.post(url, {'debug_mode': 'true', '_commit': 'true'}) self.assertEqual(response.status_code, 200) @@ -967,7 +1114,7 @@ class ScriptValidationErrorTest(TestCase): with patch.object(Script, 'python_class', new_callable=PropertyMock) as mock_python_class: mock_python_class.return_value = FieldsetScript - with patch('extras.views.get_workers_for_queue', return_value=['worker']): + with patch('extras.views.any_workers_for_queue', return_value=True): response = self.client.post(url, {'required_field': '5', '_commit': 'true'}) self.assertEqual(response.status_code, 200) @@ -975,7 +1122,7 @@ class ScriptValidationErrorTest(TestCase): self.assertEqual(len(messages), 0) -class ScriptDefaultValuesTest(TestCase): +class ScriptDefaultValuesTestCase(TestCase): user_permissions = ['extras.view_script', 'extras.run_script'] class TestScriptClass(PythonClass): @@ -1005,12 +1152,12 @@ class ScriptDefaultValuesTest(TestCase): def setUp(self): super().setUp() - Script.python_class = property(lambda self: ScriptDefaultValuesTest.TestScriptClass) + Script.python_class = property(lambda self: ScriptDefaultValuesTestCase.TestScriptClass) def test_default_values_are_used(self): url = reverse('extras:script', kwargs={'pk': self.script.pk}) - with patch('extras.views.get_workers_for_queue', return_value=['worker']): + with patch('extras.views.any_workers_for_queue', return_value=True): with patch('extras.jobs.ScriptJob.enqueue') as mock_enqueue: mock_enqueue.return_value.pk = 1 self.client.post(url, {}) @@ -1019,3 +1166,88 @@ class ScriptDefaultValuesTest(TestCase): self.assertEqual(call_kwargs['data']['bool_default_false'], False) self.assertEqual(call_kwargs['data']['int_with_default'], 0) self.assertIsNone(call_kwargs['data']['int_without_default']) + + +class ScriptResultViewTestCase(TestCase): + SECRET_OUTPUT = 'my secret script output' + + @classmethod + def setUpTestData(cls): + with patch.object(ScriptModule, 'sync_classes'): + module = ScriptModule.objects.create( + file_root=ManagedFileRootPathChoices.SCRIPTS, + file_path='test_script.py', + ) + cls.allowed_script = Script.objects.create( + module=module, name='Allowed script', is_executable=True + ) + cls.secret_script = Script.objects.create( + module=module, name='Secret script', is_executable=True + ) + + script_type = ObjectType.objects.get_for_model(Script) + cls.allowed_job = Job.objects.create( + object_type=script_type, + object_id=cls.allowed_script.pk, + name='allowed-job', + job_id=uuid.uuid4(), + status=JobStatusChoices.STATUS_COMPLETED, + data={'log': [], 'output': 'benign output'}, + ) + cls.secret_job = Job.objects.create( + object_type=script_type, + object_id=cls.secret_script.pk, + name='secret-job', + job_id=uuid.uuid4(), + status=JobStatusChoices.STATUS_COMPLETED, + data={'log': [], 'output': cls.SECRET_OUTPUT}, + ) + + def test_get_without_view_job_permission_returns_404(self): + """ + A user with extras.view_script but no core.view_job must not retrieve any job result + via ScriptResultView, even for the script whose object-level permission they hold. + """ + self.add_permissions('extras.view_script') + + url = reverse('extras:script_result', kwargs={'job_pk': self.allowed_job.pk}) + self.assertHttpStatus(self.client.get(url), 404) + + url = reverse('extras:script_result', kwargs={'job_pk': self.secret_job.pk}) + self.assertHttpStatus(self.client.get(url), 404) + + def test_get_export_output_without_view_job_permission_returns_404(self): + """ + Regression for the PoC: the ?export=output path must not leak job.data['output'] + when the user lacks core.view_job. + """ + self.add_permissions('extras.view_script') + + url = reverse('extras:script_result', kwargs={'job_pk': self.secret_job.pk}) + response = self.client.get(url, {'export': 'output'}) + + self.assertHttpStatus(response, 404) + self.assertNotIn(self.SECRET_OUTPUT.encode(), response.content) + + def test_get_with_constrained_view_job_permission(self): + """ + With core.view_job constrained to the allowed job only, the user can fetch the allowed + result but the secret result is hidden (404). + """ + self.add_permissions('extras.view_script') + obj_perm = ObjectPermission( + name='View allowed job only', + constraints={'pk': self.allowed_job.pk}, + actions=['view'], + ) + obj_perm.save() + obj_perm.users.add(self.user) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Job)) + + url = reverse('extras:script_result', kwargs={'job_pk': self.allowed_job.pk}) + self.assertHttpStatus(self.client.get(url), 200) + + url = reverse('extras:script_result', kwargs={'job_pk': self.secret_job.pk}) + response = self.client.get(url, {'export': 'output'}) + self.assertHttpStatus(response, 404) + self.assertNotIn(self.SECRET_OUTPUT.encode(), response.content) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 080613950..344830e67 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -38,7 +38,7 @@ from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.query import count_related from utilities.querydict import normalize_querydict from utilities.request import copy_safe_request -from utilities.rqworker import get_workers_for_queue +from utilities.rqworker import any_workers_for_queue from utilities.templatetags.builtins.filters import render_markdown from utilities.views import ContentTypePermissionRequiredMixin, get_action_url, register_model_view from virtualization.models import VirtualMachine @@ -1729,7 +1729,7 @@ class ScriptView(BaseScriptView): form = script_class.as_form(post_data, request.FILES) # Allow execution only if RQ worker process is running - if not get_workers_for_queue('default'): + if not any_workers_for_queue('default'): messages.error(request, _("Unable to run script: RQ worker process not running.")) elif form.is_valid(): ScriptJob = import_string("extras.jobs.ScriptJob") @@ -1803,6 +1803,9 @@ class ScriptResultView(TableMixin, generic.ObjectView): def get_required_permission(self): return 'extras.view_script' + def get_queryset(self, request): + return Job.objects.restrict(request.user, 'view') + def get_table(self, job, request, bulk_actions=True): data = [] tests = None @@ -1863,7 +1866,7 @@ class ScriptResultView(TableMixin, generic.ObjectView): def get(self, request, **kwargs): table = None - job = get_object_or_404(Job.objects.all(), pk=kwargs.get('job_pk')) + job = get_object_or_404(self.queryset, pk=kwargs.get('job_pk')) # If a direct export output has been requested, return the job data content as a # downloadable file. diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index 9433f3cb3..0bd60ef4f 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -352,6 +352,7 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode ), FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')), FieldSet('device_id', 'virtual_machine_id', name=_('Device/VM')), + FieldSet('nat_inside_id', name=_('NAT')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), @@ -397,6 +398,11 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode required=False, label=_('Assigned VM'), ) + nat_inside_id = DynamicModelMultipleChoiceField( + queryset=IPAddress.objects.all(), + required=False, + label=_('NAT inside'), + ) status = forms.MultipleChoiceField( label=_('Status'), choices=IPAddressStatusChoices, diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index 2bfdc7d65..31b93b419 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -128,7 +128,7 @@ class FHRPGroupType(IPAddressesMixin, PrimaryObjectType): class FHRPGroupAssignmentType(BaseObjectType): group: Annotated['FHRPGroupType', strawberry.lazy('ipam.graphql.types')] - @strawberry_django.field + @strawberry_django.field(prefetch_related='interface') def interface(self) -> Annotated[ Annotated['InterfaceType', strawberry.lazy('dcim.graphql.types')] | Annotated['VMInterfaceType', strawberry.lazy('virtualization.graphql.types')], @@ -153,7 +153,7 @@ class IPAddressType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType): tunnel_terminations: list[Annotated['TunnelTerminationType', strawberry.lazy('vpn.graphql.types')]] services: list[Annotated['ServiceType', strawberry.lazy('ipam.graphql.types')]] - @strawberry_django.field + @strawberry_django.field(prefetch_related='assigned_object') def assigned_object(self) -> Annotated[ Annotated['InterfaceType', strawberry.lazy('dcim.graphql.types')] | Annotated['FHRPGroupType', strawberry.lazy('ipam.graphql.types')] @@ -190,7 +190,7 @@ class PrefixType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType): vlan: Annotated['VLANType', strawberry.lazy('ipam.graphql.types')] | None role: Annotated['RoleType', strawberry.lazy('ipam.graphql.types')] | None - @strawberry_django.field + @strawberry_django.field(prefetch_related='scope') def scope(self) -> Annotated[ Annotated['LocationType', strawberry.lazy('dcim.graphql.types')] | Annotated['RegionType', strawberry.lazy('dcim.graphql.types')] @@ -252,7 +252,7 @@ class ServiceType(ContactsMixin, PrimaryObjectType): ports: list[int] ipaddresses: list[Annotated['IPAddressType', strawberry.lazy('ipam.graphql.types')]] - @strawberry_django.field + @strawberry_django.field(prefetch_related='parent') def parent(self) -> Annotated[ Annotated['DeviceType', strawberry.lazy('dcim.graphql.types')] | Annotated['VirtualMachineType', strawberry.lazy('virtualization.graphql.types')] @@ -291,7 +291,7 @@ class VLANType(PrimaryObjectType): interfaces_as_tagged: list[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]] vminterfaces_as_tagged: list[Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')]] - @strawberry_django.field + @strawberry_django.field(prefetch_related='qinq_svlan') def qinq_svlan(self) -> Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None: return self.qinq_svlan @@ -309,7 +309,7 @@ class VLANGroupType(OrganizationalObjectType): total_vlan_ids: BigInt tenant: Annotated['TenantType', strawberry.lazy('tenancy.graphql.types')] | None - @strawberry_django.field + @strawberry_django.field(prefetch_related='scope') def scope(self) -> Annotated[ Annotated['ClusterType', strawberry.lazy('virtualization.graphql.types')] | Annotated['ClusterGroupType', strawberry.lazy('virtualization.graphql.types')] diff --git a/netbox/ipam/migrations/0090_vlangroup_recompute_total_vlan_ids.py b/netbox/ipam/migrations/0090_vlangroup_recompute_total_vlan_ids.py new file mode 100644 index 000000000..d4153ddce --- /dev/null +++ b/netbox/ipam/migrations/0090_vlangroup_recompute_total_vlan_ids.py @@ -0,0 +1,32 @@ +from django.db import migrations + + +def recompute_vlangroup_total_vlan_ids(apps, schema_editor): + # PostgreSQL canonicalizes int4range values to '[)' on insert, so vid_ranges + # at rest is already canonical. Only total_vlan_ids needs fixing: the + # pre-fix VLANGroup.save() miscounted it for ranges supplied with + # non-canonical bounds (e.g. NumericRange(lo, hi, bounds='[]')). + VLANGroup = apps.get_model('ipam', 'VLANGroup') + db_alias = schema_editor.connection.alias + + stale_groups = [] + for group in VLANGroup.objects.using(db_alias).only('id', 'vid_ranges', 'total_vlan_ids').iterator(chunk_size=500): + total_vlan_ids = sum( + r.upper - r.lower for r in (group.vid_ranges or []) if r.lower is not None and r.upper is not None + ) + if group.total_vlan_ids != total_vlan_ids: + group.total_vlan_ids = total_vlan_ids + stale_groups.append(group) + + if stale_groups: + VLANGroup.objects.using(db_alias).bulk_update(stale_groups, ['total_vlan_ids'], batch_size=100) + + +class Migration(migrations.Migration): + dependencies = [ + ('ipam', '0089_default_ordering_indexes'), + ] + + operations = [ + migrations.RunPython(recompute_vlangroup_total_vlan_ids, migrations.RunPython.noop), + ] diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index a776aa9b3..cd4554257 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -605,11 +605,12 @@ class IPRange(ContactsMixin, PrimaryModel): 'end_address': _("Starting and ending IP address masks must match") }) - # Check that the ending address is greater than the starting address - if not self.end_address > self.start_address: + # Equal start/end addresses are permitted for single-address ranges. + # Use .ip (host portion) rather than the IPNetwork object to avoid comparing prefix lengths again. + if self.end_address.ip < self.start_address.ip: raise ValidationError({ 'end_address': _( - "Ending address must be greater than the starting address ({start_address})" + "Ending address must be greater than or equal to the starting address ({start_address})" ).format(start_address=self.start_address) }) @@ -677,6 +678,10 @@ class IPRange(ContactsMixin, PrimaryModel): """ Return an efficient string representation of the IP range. """ + # Single-address ranges render both endpoints to stay distinct from IPAddress rows. + if self.start_address.ip == self.end_address.ip: + return f'{self.start_address.ip}-{self.end_address.ip}/{self.start_address.prefixlen}' + separator = ':' if self.family == 6 else '.' start_chunks = str(self.start_address.ip).split(separator) end_chunks = str(self.end_address.ip).split(separator) diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index 554e7487a..2ed3512df 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -12,7 +12,13 @@ from ipam.choices import * from ipam.constants import * from ipam.querysets import VLANGroupQuerySet, VLANQuerySet from netbox.models import NetBoxModel, OrganizationalModel, PrimaryModel -from utilities.data import check_ranges_overlap, ranges_to_string, ranges_to_string_list +from utilities.data import ( + check_ranges_overlap, + get_inclusive_integer_range_bounds, + normalize_integer_range, + ranges_to_string, + ranges_to_string_list, +) from virtualization.models import VMInterface __all__ = ( @@ -55,6 +61,7 @@ class VLANGroup(OrganizationalModel): ct_field='scope_type', fk_field='scope_id' ) + # Normalized to canonical '[)' bounds on save() to match PostgreSQL storage. vid_ranges = ArrayField( IntegerRangeField(), verbose_name=_('VLAN ID ranges'), @@ -103,8 +110,7 @@ class VLANGroup(OrganizationalModel): # Validate VID ranges for vid_range in self.vid_ranges: - lower_vid = vid_range.lower if vid_range.lower_inc else vid_range.lower + 1 - upper_vid = vid_range.upper if vid_range.upper_inc else vid_range.upper - 1 + lower_vid, upper_vid = get_inclusive_integer_range_bounds(vid_range) if lower_vid < VLAN_VID_MIN: raise ValidationError({ 'vid_ranges': _("Starting VLAN ID in range ({value}) cannot be less than {minimum}").format( @@ -129,10 +135,20 @@ class VLANGroup(OrganizationalModel): raise ValidationError({'vid_ranges': _("Ranges cannot overlap.")}) def save(self, *args, **kwargs): + # Canonicalize vid_ranges on the instance so callers (e.g. Custom Scripts) + # see the same value in memory that PostgreSQL stores. self.total_vlan_ids = 0 + vid_ranges = [] for vid_range in self.vid_ranges: - # VID range is inclusive on lower-bound, exclusive on upper-bound + vid_range = normalize_integer_range(vid_range) + vid_ranges.append(vid_range) self.total_vlan_ids += vid_range.upper - vid_range.lower + self.vid_ranges = vid_ranges + + update_fields = kwargs.get('update_fields') + if update_fields is not None and 'vid_ranges' in update_fields: + # total_vlan_ids is a denormalized cache of vid_ranges; persist them together. + kwargs['update_fields'] = list(set(update_fields) | {'total_vlan_ids'}) super().save(*args, **kwargs) @@ -142,9 +158,8 @@ class VLANGroup(OrganizationalModel): """ available_vlans = set() for vlan_range in self.vid_ranges: - available_vlans = available_vlans.union({ - vid for vid in range(vlan_range.lower, vlan_range.upper) - }) + lower_vid, upper_vid = get_inclusive_integer_range_bounds(vlan_range) + available_vlans.update(range(lower_vid, upper_vid + 1)) available_vlans -= set(VLAN.objects.filter(group=self).values_list('vid', flat=True)) return sorted(available_vlans) diff --git a/netbox/ipam/querysets.py b/netbox/ipam/querysets.py index ae02b4922..12bd8328c 100644 --- a/netbox/ipam/querysets.py +++ b/netbox/ipam/querysets.py @@ -1,7 +1,7 @@ from django.contrib.contenttypes.models import ContentType from django.db.models import Count, F, OuterRef, Q, Subquery, Value from django.db.models.expressions import RawSQL -from django.db.models.functions import Round +from django.db.models.functions import NullIf, Round from utilities.query import count_related from utilities.querysets import RestrictedQuerySet @@ -62,9 +62,11 @@ class VLANGroupQuerySet(RestrictedQuerySet): def annotate_utilization(self): from .models import VLAN + # NullIf guards against legacy rows where total_vlan_ids was miscounted to + # 0 by the pre-fix VLANGroup.save(); without it, the annotation 500s. return self.annotate( vlan_count=count_related(VLAN, 'group'), - utilization=Round(F('vlan_count') * 100.0 / F('total_vlan_ids'), 2), + utilization=Round(F('vlan_count') * 100.0 / NullIf(F('total_vlan_ids'), Value(0)), 2), ) diff --git a/netbox/ipam/tests/query_counts.json b/netbox/ipam/tests/query_counts.json new file mode 100644 index 000000000..f0863dc09 --- /dev/null +++ b/netbox/ipam/tests/query_counts.json @@ -0,0 +1,37 @@ +{ + "aggregate:api_list_objects": 14, + "aggregate:list_objects_with_permission": 24, + "asn:api_list_objects": 18, + "asn:list_objects_with_permission": 31, + "asnrange:api_list_objects": 15, + "asnrange:list_objects_with_permission": 22, + "fhrpgroup:api_list_objects": 14, + "fhrpgroup:list_objects_with_permission": 21, + "fhrpgroupassignment:api_list_objects": 18, + "ipaddress:api_list_objects": 14, + "ipaddress:list_objects_with_permission": 21, + "iprange:api_list_objects": 13, + "iprange:list_objects_with_permission": 20, + "prefix:api_list_objects": 13, + "prefix:list_objects_with_permission": 29, + "rir:api_list_objects": 13, + "rir:list_objects_with_permission": 20, + "role:api_list_objects": 13, + "role:list_objects_with_permission": 20, + "routetarget:api_list_objects": 13, + "routetarget:list_objects_with_permission": 21, + "service:api_list_objects": 16, + "service:list_objects_with_permission": 21, + "servicetemplate:api_list_objects": 13, + "servicetemplate:list_objects_with_permission": 20, + "vlan:api_list_objects": 15, + "vlan:list_objects_with_permission": 24, + "vlangroup:api_list_objects": 13, + "vlangroup:list_objects_with_permission": 25, + "vlantranslationpolicy:api_list_objects": 12, + "vlantranslationpolicy:list_objects_with_permission": 20, + "vlantranslationrule:api_list_objects": 12, + "vlantranslationrule:list_objects_with_permission": 21, + "vrf:api_list_objects": 15, + "vrf:list_objects_with_permission": 20 +} diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 87bfa54fb..70d884b5e 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -14,7 +14,7 @@ from utilities.data import string_to_ranges from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_logging -class AppTest(APITestCase): +class AppTestCase(APITestCase): def test_root(self): @@ -24,7 +24,7 @@ class AppTest(APITestCase): self.assertEqual(response.status_code, 200) -class ASNRangeTest(APIViewTestCases.APIViewTestCase): +class ASNRangeTestCase(APIViewTestCases.APIViewTestCase): model = ASNRange brief_fields = ['description', 'display', 'id', 'name', 'url'] bulk_update_data = { @@ -136,7 +136,7 @@ class ASNRangeTest(APIViewTestCases.APIViewTestCase): self.assertEqual(len(response.data), 10) -class ASNTest(APIViewTestCases.APIViewTestCase): +class ASNTestCase(APIViewTestCases.APIViewTestCase): model = ASN brief_fields = ['asn', 'description', 'display', 'id', 'url'] bulk_update_data = { @@ -200,7 +200,7 @@ class ASNTest(APIViewTestCases.APIViewTestCase): ] -class VRFTest(APIViewTestCases.APIViewTestCase): +class VRFTestCase(APIViewTestCases.APIViewTestCase): model = VRF brief_fields = ['description', 'display', 'id', 'name', 'prefix_count', 'rd', 'url'] create_data = [ @@ -232,7 +232,7 @@ class VRFTest(APIViewTestCases.APIViewTestCase): VRF.objects.bulk_create(vrfs) -class RouteTargetTest(APIViewTestCases.APIViewTestCase): +class RouteTargetTestCase(APIViewTestCases.APIViewTestCase): model = RouteTarget brief_fields = ['description', 'display', 'id', 'name', 'url'] create_data = [ @@ -261,7 +261,7 @@ class RouteTargetTest(APIViewTestCases.APIViewTestCase): RouteTarget.objects.bulk_create(route_targets) -class RIRTest(APIViewTestCases.APIViewTestCase): +class RIRTestCase(APIViewTestCases.APIViewTestCase): model = RIR brief_fields = ['aggregate_count', 'description', 'display', 'id', 'name', 'slug', 'url'] create_data = [ @@ -293,7 +293,7 @@ class RIRTest(APIViewTestCases.APIViewTestCase): RIR.objects.bulk_create(rirs) -class AggregateTest(APIViewTestCases.APIViewTestCase): +class AggregateTestCase(APIViewTestCases.APIViewTestCase): model = Aggregate brief_fields = ['description', 'display', 'family', 'id', 'prefix', 'url'] bulk_update_data = { @@ -381,7 +381,7 @@ class AggregateTest(APIViewTestCases.APIViewTestCase): # No exception occurred; invalid entries were ignored -class RoleTest(APIViewTestCases.APIViewTestCase): +class RoleTestCase(APIViewTestCases.APIViewTestCase): model = Role brief_fields = ['asn_count', 'description', 'display', 'id', 'name', 'prefix_count', 'slug', 'url', 'vlan_count'] create_data = [ @@ -424,7 +424,7 @@ class RoleTest(APIViewTestCases.APIViewTestCase): ASN.objects.bulk_create(asns) -class PrefixTest(APIViewTestCases.APIViewTestCase): +class PrefixTestCase(APIViewTestCases.APIViewTestCase): model = Prefix brief_fields = ['_depth', 'description', 'display', 'family', 'id', 'prefix', 'url'] create_data = [ @@ -664,7 +664,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase): self.assertTrue(data['data']['tenant_list']) # tenant returned -class IPRangeTest(APIViewTestCases.APIViewTestCase): +class IPRangeTestCase(APIViewTestCases.APIViewTestCase): model = IPRange brief_fields = ['description', 'display', 'end_address', 'family', 'id', 'start_address', 'url'] create_data = [ @@ -680,6 +680,11 @@ class IPRangeTest(APIViewTestCases.APIViewTestCase): 'start_address': '192.168.6.10/24', 'end_address': '192.168.6.50/24', }, + { + # Single-address range (start == end) + 'start_address': '192.168.7.10/24', + 'end_address': '192.168.7.10/24', + }, ] bulk_update_data = { 'description': 'New description', @@ -822,7 +827,7 @@ class IPRangeTest(APIViewTestCases.APIViewTestCase): # No exception occurred; invalid entries were ignored -class IPAddressTest(APIViewTestCases.APIViewTestCase): +class IPAddressTestCase(APIViewTestCases.APIViewTestCase): model = IPAddress brief_fields = ['address', 'description', 'display', 'family', 'id', 'url'] create_data = [ @@ -977,7 +982,7 @@ class IPAddressTest(APIViewTestCases.APIViewTestCase): self.assertIn(str(device1.pk), ids) -class FHRPGroupTest(APIViewTestCases.APIViewTestCase): +class FHRPGroupTestCase(APIViewTestCases.APIViewTestCase): model = FHRPGroup brief_fields = ['description', 'display', 'group_id', 'id', 'protocol', 'url'] bulk_update_data = { @@ -1028,7 +1033,7 @@ class FHRPGroupTest(APIViewTestCases.APIViewTestCase): ] -class FHRPGroupAssignmentTest(APIViewTestCases.APIViewTestCase): +class FHRPGroupAssignmentTestCase(APIViewTestCases.APIViewTestCase): model = FHRPGroupAssignment brief_fields = ['display', 'group', 'id', 'interface_id', 'interface_type', 'priority', 'url'] bulk_update_data = { @@ -1108,7 +1113,7 @@ class FHRPGroupAssignmentTest(APIViewTestCases.APIViewTestCase): ] -class VLANGroupTest(APIViewTestCases.APIViewTestCase): +class VLANGroupTestCase(APIViewTestCases.APIViewTestCase): model = VLANGroup brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url', 'vlan_count'] create_data = [ @@ -1231,7 +1236,7 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase): self.assertEqual(response.data[2]['vid'], 6) -class VLANTest(APIViewTestCases.APIViewTestCase): +class VLANTestCase(APIViewTestCases.APIViewTestCase): model = VLAN brief_fields = ['description', 'display', 'id', 'name', 'url', 'vid'] bulk_update_data = { @@ -1298,7 +1303,7 @@ class VLANTest(APIViewTestCases.APIViewTestCase): self.assertTrue(content['detail'].startswith('Unable to delete object.')) -class VLANTranslationPolicyTest(APIViewTestCases.APIViewTestCase): +class VLANTranslationPolicyTestCase(APIViewTestCases.APIViewTestCase): model = VLANTranslationPolicy brief_fields = ['description', 'display', 'id', 'name', 'url',] bulk_update_data = { @@ -1340,7 +1345,7 @@ class VLANTranslationPolicyTest(APIViewTestCases.APIViewTestCase): ] -class VLANTranslationRuleTest(APIViewTestCases.APIViewTestCase): +class VLANTranslationRuleTestCase(APIViewTestCases.APIViewTestCase): model = VLANTranslationRule brief_fields = ['description', 'display', 'id', 'local_vid', 'policy', 'remote_vid', 'url'] @@ -1409,7 +1414,7 @@ class VLANTranslationRuleTest(APIViewTestCases.APIViewTestCase): } -class ServiceTemplateTest(APIViewTestCases.APIViewTestCase): +class ServiceTemplateTestCase(APIViewTestCases.APIViewTestCase): model = ServiceTemplate brief_fields = ['description', 'display', 'id', 'name', 'ports', 'protocol', 'url'] bulk_update_data = { @@ -1445,7 +1450,7 @@ class ServiceTemplateTest(APIViewTestCases.APIViewTestCase): ] -class ServiceTest(APIViewTestCases.APIViewTestCase): +class ServiceTestCase(APIViewTestCases.APIViewTestCase): model = Service brief_fields = ['description', 'display', 'id', 'name', 'ports', 'protocol', 'url'] bulk_update_data = { diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 856f8d4b5..900436d62 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -1110,6 +1110,25 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'mark_populated': 'false'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) + def test_single_address_range(self): + # A range with start_address == end_address must be discoverable by the + # start, end, and contains filters. + iprange = IPRange( + start_address=IPNetwork('10.0.5.1/24'), + end_address=IPNetwork('10.0.5.1/24'), + ) + iprange.clean() + iprange.save() + + params = {'start_address': ['10.0.5.1']} + self.assertIn(iprange, self.filterset(params, self.queryset).qs) + + params = {'end_address': ['10.0.5.1']} + self.assertIn(iprange, self.filterset(params, self.queryset).qs) + + params = {'contains': '10.0.5.1/24'} + self.assertIn(iprange, self.filterset(params, self.queryset).qs) + class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = IPAddress.objects.all() @@ -1288,6 +1307,10 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests): ) IPAddress.objects.bulk_create(ipaddresses) + IPAddress.objects.filter(pk__in=[ipaddresses[1].pk, ipaddresses[2].pk]).update( + nat_inside=ipaddresses[0] + ) + services = ( Service( parent=devices[0], @@ -1457,6 +1480,11 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'service_id': [services[0].pk, services[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_nat_inside(self): + inside = IPAddress.objects.filter(nat_outside__isnull=False).distinct().first() + params = {'nat_inside_id': [inside.pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class FHRPGroupTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = FHRPGroup.objects.all() diff --git a/netbox/ipam/tests/test_lookups.py b/netbox/ipam/tests/test_lookups.py index 8383cac31..c23c910cf 100644 --- a/netbox/ipam/tests/test_lookups.py +++ b/netbox/ipam/tests/test_lookups.py @@ -4,7 +4,7 @@ from django.test import TestCase from ipam.models import VLANGroup -class VLANGroupRangeContainsLookupTests(TestCase): +class VLANGroupRangeContainsLookupTestCase(TestCase): @classmethod def setUpTestData(cls): # Two ranges: [1,11) and [20,31) diff --git a/netbox/ipam/tests/test_management_commands.py b/netbox/ipam/tests/test_management_commands.py new file mode 100644 index 000000000..bdadf1317 --- /dev/null +++ b/netbox/ipam/tests/test_management_commands.py @@ -0,0 +1,84 @@ +from io import StringIO +from unittest.mock import patch + +from django.core.management import call_command +from django.test import TestCase +from netaddr import IPNetwork + +from ipam.models import Prefix + + +class RebuildPrefixesTestCase(TestCase): + def test_rebuilds_global_prefix_tree(self): + out = StringIO() + + with ( + patch('ipam.management.commands.rebuild_prefixes.Prefix') as prefix_model, + patch('ipam.management.commands.rebuild_prefixes.VRF') as vrf_model, + patch('ipam.management.commands.rebuild_prefixes.rebuild_prefixes') as rebuild_prefixes, + ): + prefix_model.objects.count.return_value = 0 + prefix_model.objects.filter.return_value.count.return_value = 0 + vrf_model.objects.all.return_value = [] + call_command('rebuild_prefixes', stdout=out) + + rebuild_prefixes.assert_called_once_with(None) + prefix_model.objects.update.assert_called_once_with(_depth=0, _children=0) + self.assertIn('Rebuilding 0 prefixes', out.getvalue()) + self.assertIn('Finished.', out.getvalue()) + + def test_hierarchy_is_correct_after_rebuild(self): + Prefix.objects.bulk_create( + [ + Prefix(prefix=IPNetwork('10.0.0.0/8')), + Prefix(prefix=IPNetwork('10.0.0.0/16')), + Prefix(prefix=IPNetwork('10.0.0.0/24')), + ] + ) + + out = StringIO() + call_command('rebuild_prefixes', stdout=out) + + self.assertIn('Finished.', out.getvalue()) + self.assertEqual(Prefix.objects.get(prefix=IPNetwork('10.0.0.0/8'))._depth, 0) + self.assertEqual(Prefix.objects.get(prefix=IPNetwork('10.0.0.0/8'))._children, 2) + self.assertEqual(Prefix.objects.get(prefix=IPNetwork('10.0.0.0/16'))._depth, 1) + self.assertEqual(Prefix.objects.get(prefix=IPNetwork('10.0.0.0/16'))._children, 1) + self.assertEqual(Prefix.objects.get(prefix=IPNetwork('10.0.0.0/24'))._depth, 2) + self.assertEqual(Prefix.objects.get(prefix=IPNetwork('10.0.0.0/24'))._children, 0) + + def test_rebuilds_prefix_tree_for_each_vrf(self): + class FakeVRF: + pk = 123 + + def __str__(self): + return 'Tenant VRF' + + class FakeQuerySet: + def __init__(self, count): + self._count = count + + def count(self): + return self._count + + out = StringIO() + vrf = FakeVRF() + + with ( + patch('ipam.management.commands.rebuild_prefixes.Prefix') as prefix_model, + patch('ipam.management.commands.rebuild_prefixes.VRF') as vrf_model, + patch('ipam.management.commands.rebuild_prefixes.rebuild_prefixes') as rebuild_prefixes, + ): + prefix_model.objects.count.return_value = 3 + prefix_model.objects.filter.side_effect = [ + FakeQuerySet(1), + FakeQuerySet(2), + ] + vrf_model.objects.all.return_value = [vrf] + + call_command('rebuild_prefixes', stdout=out) + + rebuild_prefixes.assert_any_call(None) + rebuild_prefixes.assert_any_call(vrf.pk) + self.assertIn('Global: 1 prefixes', out.getvalue()) + self.assertIn('VRF Tenant VRF: 2 prefixes', out.getvalue()) diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index d7a1a1267..e5637433a 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -1,5 +1,6 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError +from django.db.backends.postgresql.psycopg_any import NumericRange from django.test import TestCase, override_settings from netaddr import IPNetwork, IPSet @@ -9,7 +10,7 @@ from ipam.models import * from utilities.data import string_to_ranges -class TestAggregate(TestCase): +class AggregateTestCase(TestCase): def test_family_string(self): # Test property when prefix is a string @@ -45,7 +46,7 @@ class TestAggregate(TestCase): self.assertEqual(aggregate.get_utilization(), 100) -class TestIPRange(TestCase): +class IPRangeTestCase(TestCase): def test_family_string(self): # Test property when start_address is a string @@ -101,8 +102,71 @@ class TestIPRange(TestCase): ) iprange_4_198_201.clean() + def test_single_address_range(self): + iprange = IPRange( + start_address=IPNetwork('192.0.2.10/24'), + end_address=IPNetwork('192.0.2.10/24'), + ) -class TestPrefix(TestCase): + iprange.clean() + iprange.save() + + self.assertEqual(iprange.size, 1) + self.assertEqual(str(iprange), '192.0.2.10-192.0.2.10/24') + self.assertEqual(iprange.first_available_ip, '192.0.2.10/24') + + def test_first_available_ip_consumed_single_address_range(self): + iprange = IPRange.objects.create( + start_address=IPNetwork('192.0.2.10/24'), + end_address=IPNetwork('192.0.2.10/24'), + ) + IPAddress.objects.create(address=IPNetwork('192.0.2.10/24')) + + # The sole address in the range is now assigned, so no IPs remain available. + self.assertIsNone(iprange.first_available_ip) + + def test_single_address_range_ipv6(self): + # IPRange.name has IPv4/IPv6-specific formatting; exercise the IPv6 branch + # for a single-address range too. + iprange = IPRange( + start_address=IPNetwork('2001:db8::10/64'), + end_address=IPNetwork('2001:db8::10/64'), + ) + + iprange.clean() + iprange.save() + + self.assertEqual(iprange.size, 1) + self.assertEqual(str(iprange), '2001:db8::10-2001:db8::10/64') + self.assertEqual(iprange.first_available_ip, '2001:db8::10/64') + + def test_reversed_range(self): + iprange = IPRange( + start_address=IPNetwork('192.0.2.10/24'), + end_address=IPNetwork('192.0.2.9/24'), + ) + + with self.assertRaises(ValidationError): + iprange.clean() + + def test_overlapping_single_address_range(self): + IPRange.objects.create( + start_address=IPNetwork('192.0.2.10/24'), + end_address=IPNetwork('192.0.2.10/24'), + ) + + iprange = IPRange( + start_address=IPNetwork('192.0.2.10/24'), + end_address=IPNetwork('192.0.2.10/24'), + ) + + # Assert the overlap-specific error message so this test cannot pass on a + # regression where start_address == end_address is rejected earlier. + with self.assertRaisesMessage(ValidationError, 'Defined addresses overlap'): + iprange.clean() + + +class PrefixTestCase(TestCase): def test_family_string(self): # Test property when prefix is a string @@ -361,7 +425,7 @@ class TestPrefix(TestCase): self.assertRaises(ValidationError, duplicate_prefix.clean) -class TestPrefixHierarchy(TestCase): +class PrefixHierarchyTestCase(TestCase): """ Test the automatic updating of depth and child count in response to changes made within the prefix hierarchy. @@ -559,7 +623,7 @@ class TestPrefixHierarchy(TestCase): self.assertEqual(prefixes[3]._children, 0) -class TestIPAddress(TestCase): +class IPAddressTestCase(TestCase): def test_family_string(self): # Test property when address is a string @@ -640,8 +704,21 @@ class TestIPAddress(TestCase): ip = IPAddress(address=IPNetwork('192.0.2.10/24')) self.assertRaises(ValidationError, ip.full_clean) + def test_mark_populated_single_address_range_blocks_ip(self): + # A single-address range with mark_populated=True must still block creation + # of an IPAddress at the same host with the same mask. + IPRange.objects.create( + start_address=IPNetwork('192.0.2.10/24'), + end_address=IPNetwork('192.0.2.10/24'), + mark_populated=True, + ) + ipaddress = IPAddress(address=IPNetwork('192.0.2.10/24')) -class TestVLANGroup(TestCase): + with self.assertRaisesMessage(ValidationError, 'Cannot create IP address'): + ipaddress.clean() + + +class VLANGroupTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -665,6 +742,18 @@ class TestVLANGroup(TestCase): available_vids = vlangroup.get_available_vids() self.assertListEqual(available_vids, list(range(104, 200))) + def test_get_available_vids_with_inclusive_ranges(self): + vlangroup = VLANGroup.objects.create( + name='VLAN Group 2', + slug='vlan-group-2', + vid_ranges=[NumericRange(200, 204, bounds='[]')], + ) + VLAN.objects.create(name='VLAN 200', vid=200, group=vlangroup) + + # Re-assign with non-canonical bounds to exercise the unsaved in-memory path. + vlangroup.vid_ranges = [NumericRange(200, 204, bounds='[]')] + self.assertListEqual(vlangroup.get_available_vids(), [201, 202, 203, 204]) + def test_get_next_available_vid(self): vlangroup = VLANGroup.objects.first() self.assertEqual(vlangroup.get_next_available_vid(), 104) @@ -700,8 +789,74 @@ class TestVLANGroup(TestCase): vlangroup = VLANGroup.objects.first() self.assertEqual(vlangroup.total_vlan_ids, 100) + def test_total_vlan_ids_with_inclusive_ranges(self): + test_cases = ( + ( + NumericRange(100, 199, bounds='[]'), + 100, + NumericRange(100, 200, bounds='[)'), + ), + ( + NumericRange(100, 100, bounds='[]'), + 1, + NumericRange(100, 101, bounds='[)'), + ), + ( + NumericRange(99, 199, bounds='(]'), + 100, + NumericRange(100, 200, bounds='[)'), + ), + ) -class TestVLAN(TestCase): + for i, (vid_range, total_vlan_ids, normalized_range) in enumerate(test_cases, start=1): + vlangroup = VLANGroup( + name=f'VLAN Group Inclusive {i}', + slug=f'vlan-group-inclusive-{i}', + vid_ranges=[vid_range], + ) + + vlangroup.full_clean() + vlangroup.save() + self.assertEqual(vlangroup.vid_ranges, [normalized_range]) + self.assertEqual(vlangroup.total_vlan_ids, total_vlan_ids) + + def test_total_vlan_ids_with_inclusive_ranges_without_full_clean(self): + vlangroup = VLANGroup.objects.create( + name='VLAN Group Inclusive Save', + slug='vlan-group-inclusive-save', + vid_ranges=[NumericRange(100, 100, bounds='[]')], + ) + + self.assertEqual(vlangroup.vid_ranges, [NumericRange(100, 101, bounds='[)')]) + self.assertEqual(vlangroup.total_vlan_ids, 1) + + def test_total_vlan_ids_with_update_fields(self): + vlangroup = VLANGroup.objects.create( + name='VLAN Group Update Fields', + slug='vlan-group-update-fields', + vid_ranges=[NumericRange(100, 200, bounds='[)')], + ) + + vlangroup.vid_ranges = [NumericRange(100, 100, bounds='[]')] + vlangroup.save(update_fields=['vid_ranges']) + vlangroup.refresh_from_db() + + self.assertEqual(vlangroup.vid_ranges, [NumericRange(100, 101, bounds='[)')]) + self.assertEqual(vlangroup.total_vlan_ids, 1) + + def test_annotate_utilization_with_zero_total_vlan_ids(self): + vlangroup = VLANGroup.objects.create( + name='VLAN Group Zero Total', + slug='vlan-group-zero-total', + vid_ranges=[NumericRange(100, 101)], + ) + VLANGroup.objects.filter(pk=vlangroup.pk).update(total_vlan_ids=0) + + vlangroup = VLANGroup.objects.annotate_utilization().get(pk=vlangroup.pk) + self.assertIsNone(vlangroup.utilization) + + +class VLANTestCase(TestCase): @classmethod def setUpTestData(cls): diff --git a/netbox/ipam/tests/test_signals.py b/netbox/ipam/tests/test_signals.py new file mode 100644 index 000000000..0db43c624 --- /dev/null +++ b/netbox/ipam/tests/test_signals.py @@ -0,0 +1,231 @@ +import uuid + +from django.contrib.contenttypes.models import ContentType +from django.test import RequestFactory, TestCase + +from core.choices import ObjectChangeActionChoices +from core.models import ObjectChange +from ipam.models import IPAddress, Prefix +from netbox.context_managers import event_tracking +from users.models import User +from utilities.testing.utils import create_test_device, create_test_virtualmachine + + +def _build_request(user): + request = RequestFactory().get('/') + request.id = uuid.uuid4() + request.user = user + return request + + +class PrefixHierarchySignalTestCase(TestCase): + """ + Verify ipam.signals.handle_prefix_saved / handle_prefix_deleted keep the cached + `_children` and `_depth` counters up to date as prefixes are added, modified, and + removed. + """ + + def _refresh_counters(self, *prefixes): + for prefix in prefixes: + prefix.refresh_from_db() + return prefixes + + def test_creating_prefix_initializes_hierarchy_counters(self): + parent = Prefix.objects.create(prefix='10.0.0.0/16') + child = Prefix.objects.create(prefix='10.0.1.0/24') + + self._refresh_counters(parent, child) + self.assertEqual(parent._children, 1) + self.assertEqual(child._depth, 1) + self.assertEqual(child._children, 0) + + def test_modifying_prefix_recomputes_old_and_new_position(self): + parent_a = Prefix.objects.create(prefix='10.0.0.0/16') + parent_b = Prefix.objects.create(prefix='192.168.0.0/16') + child = Prefix.objects.create(prefix='10.0.1.0/24') + + self._refresh_counters(parent_a, parent_b, child) + self.assertEqual(parent_a._children, 1) + self.assertEqual(parent_b._children, 0) + + # Move the child under parent_b. + child.prefix = '192.168.1.0/24' + child.save() + + self._refresh_counters(parent_a, parent_b, child) + self.assertEqual(parent_a._children, 0) + self.assertEqual(parent_b._children, 1) + self.assertEqual(child._depth, 1) + + def test_unchanged_save_does_not_disturb_counters(self): + parent = Prefix.objects.create(prefix='10.0.0.0/16') + child = Prefix.objects.create(prefix='10.0.1.0/24') + + self._refresh_counters(parent, child) + original_children = parent._children + original_depth = child._depth + + # Save with no field changes. + parent.description = '' + parent.save() + + self._refresh_counters(parent, child) + self.assertEqual(parent._children, original_children) + self.assertEqual(child._depth, original_depth) + + def test_deleting_prefix_recomputes_neighbor_counters(self): + parent = Prefix.objects.create(prefix='10.0.0.0/16') + child = Prefix.objects.create(prefix='10.0.1.0/24') + + self._refresh_counters(parent) + self.assertEqual(parent._children, 1) + + child.delete() + + self._refresh_counters(parent) + self.assertEqual(parent._children, 0) + + +class ClearPrimaryIPSignalTestCase(TestCase): + """ + Verify ipam.signals.clear_primary_ip detaches deleted IPAddresses from the Device or + VirtualMachine they were assigned as primary. The behavior under test is the + signal-driven snapshot+save (and resulting change-log entry), not the FK's + on_delete=SET_NULL fallback. + """ + + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user(username='alice', password='pw') + + def test_device_primary_ip4_delete_records_device_update(self): + device = create_test_device('Device 1') + ip = IPAddress.objects.create(address='192.0.2.1/24') + device.primary_ip4 = ip + device.save() + + request = _build_request(self.user) + with event_tracking(request): + ip.delete() + + device.refresh_from_db() + self.assertIsNone(device.primary_ip4) + + oc = ObjectChange.objects.get( + changed_object_type=ContentType.objects.get_for_model(device), + changed_object_id=device.pk, + action=ObjectChangeActionChoices.ACTION_UPDATE, + ) + self.assertIsNone(oc.postchange_data['primary_ip4']) + + def test_device_primary_ip6_delete_clears_field_and_saves(self): + device = create_test_device('Device 1') + ip = IPAddress.objects.create(address='2001:db8::1/64') + device.primary_ip6 = ip + device.save() + + request = _build_request(self.user) + with event_tracking(request): + ip.delete() + + device.refresh_from_db() + self.assertIsNone(device.primary_ip6) + oc = ObjectChange.objects.get( + changed_object_type=ContentType.objects.get_for_model(device), + changed_object_id=device.pk, + action=ObjectChangeActionChoices.ACTION_UPDATE, + ) + self.assertIsNone(oc.postchange_data['primary_ip6']) + + def test_vm_primary_ip4_delete_records_vm_update(self): + vm = create_test_virtualmachine('VM 1') + ip = IPAddress.objects.create(address='192.0.2.10/24') + vm.primary_ip4 = ip + vm.save() + + request = _build_request(self.user) + with event_tracking(request): + ip.delete() + + vm.refresh_from_db() + self.assertIsNone(vm.primary_ip4) + oc = ObjectChange.objects.get( + changed_object_type=ContentType.objects.get_for_model(vm), + changed_object_id=vm.pk, + action=ObjectChangeActionChoices.ACTION_UPDATE, + ) + self.assertIsNone(oc.postchange_data['primary_ip4']) + + def test_unrelated_ip_delete_records_no_device_change(self): + device = create_test_device('Device 1') + device_type = ContentType.objects.get_for_model(device) + assigned = IPAddress.objects.create(address='192.0.2.1/24') + unrelated = IPAddress.objects.create(address='192.0.2.2/24') + device.primary_ip4 = assigned + device.save() + + request = _build_request(self.user) + with event_tracking(request): + unrelated.delete() + + device.refresh_from_db() + self.assertEqual(device.primary_ip4, assigned) + self.assertFalse( + ObjectChange.objects.filter( + changed_object_type=device_type, + changed_object_id=device.pk, + action=ObjectChangeActionChoices.ACTION_UPDATE, + ).exists() + ) + + +class ClearOOBIPSignalTestCase(TestCase): + """ + Verify ipam.signals.clear_oob_ip detaches a deleted IPAddress from any Device on + which it was set as the OOB IP, and records a Device update change-log entry. + """ + + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user(username='alice', password='pw') + + def test_device_oob_ip_delete_records_device_update(self): + device = create_test_device('Device 1') + ip = IPAddress.objects.create(address='192.0.2.1/24') + device.oob_ip = ip + device.save() + + request = _build_request(self.user) + with event_tracking(request): + ip.delete() + + device.refresh_from_db() + self.assertIsNone(device.oob_ip) + oc = ObjectChange.objects.get( + changed_object_type=ContentType.objects.get_for_model(device), + changed_object_id=device.pk, + action=ObjectChangeActionChoices.ACTION_UPDATE, + ) + self.assertIsNone(oc.postchange_data['oob_ip']) + + def test_unrelated_ip_delete_records_no_device_change(self): + device = create_test_device('Device 1') + device_type = ContentType.objects.get_for_model(device) + oob = IPAddress.objects.create(address='192.0.2.1/24') + unrelated = IPAddress.objects.create(address='192.0.2.2/24') + device.oob_ip = oob + device.save() + + request = _build_request(self.user) + with event_tracking(request): + unrelated.delete() + + device.refresh_from_db() + self.assertEqual(device.oob_ip, oob) + self.assertFalse( + ObjectChange.objects.filter( + changed_object_type=device_type, + changed_object_id=device.pk, + action=ObjectChangeActionChoices.ACTION_UPDATE, + ).exists() + ) diff --git a/netbox/ipam/tests/test_tables.py b/netbox/ipam/tests/test_tables.py index d9d4ffab1..80391e973 100644 --- a/netbox/ipam/tests/test_tables.py +++ b/netbox/ipam/tests/test_tables.py @@ -7,7 +7,7 @@ from ipam.utils import annotate_ip_space from utilities.testing import TableTestCases -class AnnotatedIPAddressTableTest(TestCase): +class AnnotatedIPAddressTableTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -180,43 +180,43 @@ class AnnotatedIPAddressTableTest(TestCase): # Table ordering tests # -class VRFTableTest(TableTestCases.StandardTableTestCase): +class VRFTableTestCase(TableTestCases.StandardTableTestCase): table = VRFTable -class RouteTargetTableTest(TableTestCases.StandardTableTestCase): +class RouteTargetTableTestCase(TableTestCases.StandardTableTestCase): table = RouteTargetTable -class RIRTableTest(TableTestCases.StandardTableTestCase): +class RIRTableTestCase(TableTestCases.StandardTableTestCase): table = RIRTable -class AggregateTableTest(TableTestCases.StandardTableTestCase): +class AggregateTableTestCase(TableTestCases.StandardTableTestCase): table = AggregateTable -class RoleTableTest(TableTestCases.StandardTableTestCase): +class RoleTableTestCase(TableTestCases.StandardTableTestCase): table = RoleTable -class PrefixTableTest(TableTestCases.StandardTableTestCase): +class PrefixTableTestCase(TableTestCases.StandardTableTestCase): table = PrefixTable -class IPRangeTableTest(TableTestCases.StandardTableTestCase): +class IPRangeTableTestCase(TableTestCases.StandardTableTestCase): table = IPRangeTable -class IPAddressTableTest(TableTestCases.StandardTableTestCase): +class IPAddressTableTestCase(TableTestCases.StandardTableTestCase): table = IPAddressTable -class FHRPGroupTableTest(TableTestCases.StandardTableTestCase): +class FHRPGroupTableTestCase(TableTestCases.StandardTableTestCase): table = FHRPGroupTable -class FHRPGroupAssignmentTableTest(TableTestCases.StandardTableTestCase): +class FHRPGroupAssignmentTableTestCase(TableTestCases.StandardTableTestCase): table = FHRPGroupAssignmentTable # No ObjectListView exists for this table; it is only rendered inline on @@ -226,33 +226,33 @@ class FHRPGroupAssignmentTableTest(TableTestCases.StandardTableTestCase): ] -class VLANGroupTableTest(TableTestCases.StandardTableTestCase): +class VLANGroupTableTestCase(TableTestCases.StandardTableTestCase): table = VLANGroupTable -class VLANTableTest(TableTestCases.StandardTableTestCase): +class VLANTableTestCase(TableTestCases.StandardTableTestCase): table = VLANTable -class VLANTranslationPolicyTableTest(TableTestCases.StandardTableTestCase): +class VLANTranslationPolicyTableTestCase(TableTestCases.StandardTableTestCase): table = VLANTranslationPolicyTable -class VLANTranslationRuleTableTest(TableTestCases.StandardTableTestCase): +class VLANTranslationRuleTableTestCase(TableTestCases.StandardTableTestCase): table = VLANTranslationRuleTable -class ASNRangeTableTest(TableTestCases.StandardTableTestCase): +class ASNRangeTableTestCase(TableTestCases.StandardTableTestCase): table = ASNRangeTable -class ASNTableTest(TableTestCases.StandardTableTestCase): +class ASNTableTestCase(TableTestCases.StandardTableTestCase): table = ASNTable -class ServiceTemplateTableTest(TableTestCases.StandardTableTestCase): +class ServiceTemplateTableTestCase(TableTestCases.StandardTableTestCase): table = ServiceTemplateTable -class ServiceTableTest(TableTestCases.StandardTableTestCase): +class ServiceTableTestCase(TableTestCases.StandardTableTestCase): table = ServiceTable diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index e77d88b68..10932ce72 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -1,7 +1,6 @@ import datetime from django.contrib.contenttypes.models import ContentType -from django.test import override_settings from django.urls import reverse from netaddr import IPNetwork @@ -339,8 +338,8 @@ class AggregateTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'description': 'New description', } - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_aggregate_prefixes(self): + self.add_permissions('ipam.view_aggregate', 'ipam.view_prefix') rir = RIR.objects.first() aggregate = Aggregate.objects.create(prefix=IPNetwork('192.168.0.0/16'), rir=rir) prefixes = ( @@ -476,9 +475,9 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'description': 'New description', } - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_bulk_add_ipv4_prefixes(self): """Test bulk creating IPv4 prefixes using a pattern.""" + self.add_permissions('ipam.view_prefix') obj_perm = ObjectPermission(name='Test permission', actions=['add']) obj_perm.save() obj_perm.users.add(self.user) @@ -497,9 +496,9 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): for i in range(3): self.assertTrue(Prefix.objects.filter(prefix=IPNetwork(f'10.0.{i}.0/24')).exists()) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_bulk_add_ipv6_prefixes(self): """Test bulk creating IPv6 prefixes using a pattern.""" + self.add_permissions('ipam.view_prefix') obj_perm = ObjectPermission(name='Test permission', actions=['add']) obj_perm.save() obj_perm.users.add(self.user) @@ -518,9 +517,9 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): for i in range(4): self.assertTrue(Prefix.objects.filter(prefix=IPNetwork(f'fd00:db8:{i}::/48')).exists()) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_bulk_add_ipv6_prefixes_uppercase_hex(self): """Test bulk creating IPv6 prefixes using uppercase hex in the pattern.""" + self.add_permissions('ipam.view_prefix') obj_perm = ObjectPermission(name='Test permission', actions=['add']) obj_perm.save() obj_perm.users.add(self.user) @@ -544,8 +543,8 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): f'Expected prefix {prefix_str} was not created' ) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_bulk_add_prefixes_with_changelog_message(self): + self.add_permissions('ipam.view_prefix') obj_perm = ObjectPermission(name='Test permission', actions=['add']) obj_perm.save() obj_perm.users.add(self.user) @@ -575,8 +574,8 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): for objectchange in objectchanges: self.assertEqual(objectchange.message, changelog_message) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_prefix_prefixes(self): + self.add_permissions('ipam.view_prefix') prefixes = ( Prefix(prefix=IPNetwork('192.168.0.0/16')), Prefix(prefix=IPNetwork('192.168.1.0/24')), @@ -589,8 +588,8 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('ipam:prefix_prefixes', kwargs={'pk': prefixes[0].pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_prefix_ipranges(self): + self.add_permissions('ipam.view_prefix', 'ipam.view_iprange') prefix = Prefix.objects.create(prefix=IPNetwork('192.168.0.0/16')) ip_ranges = ( IPRange(start_address='192.168.0.1/24', end_address='192.168.0.100/24', size=99), @@ -603,8 +602,8 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('ipam:prefix_ipranges', kwargs={'pk': prefix.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_prefix_ipaddresses(self): + self.add_permissions('ipam.view_prefix', 'ipam.view_ipaddress', 'ipam.view_iprange') prefix = Prefix.objects.create(prefix=IPNetwork('192.168.0.0/16')) ip_addresses = ( IPAddress(address=IPNetwork('192.168.0.1/16')), @@ -617,11 +616,33 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('ipam:prefix_ipaddresses', kwargs={'pk': prefix.pk}) self.assertHttpStatus(self.client.get(url), 200) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) + def test_prefix_ipaddresses_with_single_address_range(self): + self.add_permissions('ipam.view_prefix', 'ipam.view_ipaddress', 'ipam.view_iprange') + # The IP Addresses tab annotates child IP addresses alongside any + # mark-populated child IP ranges. Make sure a single-address range + # (start_address == end_address) renders without errors and is shown + # in its range-like display form rather than as a plain IP address. + prefix = Prefix.objects.create(prefix=IPNetwork('192.168.0.0/16')) + IPAddress.objects.create(address=IPNetwork('192.168.0.1/16')) + IPRange.objects.create( + start_address=IPNetwork('192.168.0.50/16'), + end_address=IPNetwork('192.168.0.50/16'), + mark_populated=True, + ) + + url = reverse('ipam:prefix_ipaddresses', kwargs={'pk': prefix.pk}) + response = self.client.get(url) + self.assertHttpStatus(response, 200) + # The single-address range is rendered with both endpoints, not as + # 192.168.0.50/16 (which would make it indistinguishable from an + # IPAddress row in this mixed-record view). + self.assertContains(response, '192.168.0.50-192.168.0.50/16') + def test_prefix_import(self): """ Custom import test for YAML-based imports (versus CSV) """ + self.add_permissions('dcim.view_site', 'ipam.view_vlan') site = Site.objects.get(name='Site 1') IMPORT_DATA = f""" prefix: 10.1.1.0/24 @@ -648,11 +669,11 @@ scope_id: {site.pk} self.assertEqual(prefix.vlan.vid, 101) self.assertEqual(prefix.scope, site) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_prefix_import_with_scope_name(self): """ Test YAML-based import using scope_name instead of scope_id. """ + self.add_permissions('dcim.view_site') site = Site.objects.get(name='Site 1') IMPORT_DATA = """ prefix: 10.1.3.0/24 @@ -674,11 +695,11 @@ scope_name: Site 1 self.assertEqual(prefix.status, PrefixStatusChoices.STATUS_ACTIVE) self.assertEqual(prefix.scope, site) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_prefix_import_with_vlan_group(self): """ This test covers a unique import edge case where VLAN group is specified during the import. """ + self.add_permissions('dcim.view_site', 'ipam.view_vlan', 'ipam.view_vlangroup') site = Site.objects.get(name='Site 1') IMPORT_DATA = f""" prefix: 10.1.2.0/24 @@ -706,12 +727,12 @@ vlan: 102 self.assertEqual(prefix.vlan.vid, 102) self.assertEqual(prefix.scope, site) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_prefix_import_with_vlan_site_multiple_vlans_same_vid(self): """ Test import when multiple VLANs exist with the same vid but different sites. Ref: #20560 """ + self.add_permissions('dcim.view_site', 'ipam.view_vlan') site1 = Site.objects.get(name='Site 1') site2 = Site.objects.get(name='Site 2') @@ -744,13 +765,13 @@ description: LOC02-MGMT prefix = Prefix.objects.get(prefix='10.11.0.0/22') self.assertEqual(prefix.vlan, vlan1) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_prefix_import_with_vlan_site_and_global_vlan(self): """ Test import when a global VLAN (no site) and site-specific VLAN exist with same vid. When vlan_site is specified, should prefer the site-specific VLAN. Ref: #20560 """ + self.add_permissions('dcim.view_site', 'ipam.view_vlan') site1 = Site.objects.get(name='Site 1') # Create a global VLAN (no site) and a site-specific VLAN with the same vid @@ -830,6 +851,8 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase): "VRF 1,10.1.0.1/16,10.1.9.254/16,active", "VRF 1,10.2.0.1/16,10.2.9.254/16,active", "VRF 1,10.3.0.1/16,10.3.9.254/16,active", + # Single-address range (start == end) + "VRF 1,10.4.0.1/16,10.4.0.1/16,active", ) cls.csv_update_data = ( @@ -847,8 +870,8 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'description': 'New description', } - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_iprange_ipaddresses(self): + self.add_permissions('ipam.view_iprange', 'ipam.view_ipaddress') iprange = IPRange.objects.create( start_address=IPNetwork('192.168.0.1/24'), end_address=IPNetwork('192.168.0.100/24'), @@ -865,6 +888,28 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('ipam:iprange_ipaddresses', kwargs={'pk': iprange.pk}) self.assertHttpStatus(self.client.get(url), 200) + def test_create_single_address_range(self): + # Exercise the UI form path with start_address == end_address. The + # generic test_create_object_with_permission covers the multi-address + # case via cls.form_data; this test mirrors that flow for the single- + # address case so both paths stay covered. + self.add_permissions('ipam.add_iprange') + form_data = { + 'start_address': '192.0.6.10/24', + 'end_address': '192.0.6.10/24', + 'status': IPRangeStatusChoices.STATUS_ACTIVE, + } + initial_count = IPRange.objects.count() + + response = self.client.post(reverse('ipam:iprange_add'), data=form_data) + self.assertHttpStatus(response, 302) + self.assertEqual(IPRange.objects.count(), initial_count + 1) + + iprange = IPRange.objects.order_by('pk').last() + self.assertEqual(str(iprange.start_address), '192.0.6.10/24') + self.assertEqual(str(iprange.end_address), '192.0.6.10/24') + self.assertEqual(iprange.size, 1) + class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = IPAddress @@ -940,8 +985,8 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'description': 'New description', } - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_bulk_add_ipaddresses_with_changelog_message(self): + self.add_permissions('ipam.view_ipaddress', 'ipam.view_vrf') obj_perm = ObjectPermission(name='Test permission', actions=['add']) obj_perm.save() obj_perm.users.add(self.user) @@ -1379,8 +1424,8 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'description': 'New description', } - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[]) def test_unassigned_ip_addresses(self): + self.add_permissions('ipam.view_service', 'dcim.view_device', 'ipam.view_ipaddress') device = Device.objects.first() addr = IPAddress.objects.create(address='192.0.2.4/24') csv_data = ( @@ -1409,8 +1454,8 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase): self.assertIn(addr.address, form_errors['__all__'][0]) self.assertEqual(self._get_queryset().count(), initial_count) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[]) def test_alternate_csv_import(self): + self.add_permissions('ipam.view_service', 'dcim.view_device', 'ipam.view_ipaddress') device = Device.objects.first() interface = device.interfaces.first() addr = IPAddress.objects.create(assigned_object=interface, address='192.0.2.3/24') @@ -1439,9 +1484,13 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase): self.assertHttpStatus(response, 302) self.assertEqual(self._get_queryset().count(), initial_count + len(csv_data) - 1) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_create_from_template(self): - self.add_permissions('ipam.add_service') + self.add_permissions( + 'ipam.view_service', + 'ipam.add_service', + 'dcim.view_device', + 'ipam.view_servicetemplate', + ) device = Device.objects.first() service_template = ServiceTemplate.objects.create( diff --git a/netbox/ipam/ui/panels.py b/netbox/ipam/ui/panels.py index c8984352a..6e0f53d34 100644 --- a/netbox/ipam/ui/panels.py +++ b/netbox/ipam/ui/panels.py @@ -144,7 +144,7 @@ class PrefixPanel(panels.ObjectAttributesPanel): template_name='ipam/prefix/attrs/aggregate.html', label=_('Aggregate'), ) - scope = attrs.GenericForeignKeyAttr('scope', linkify=True) + scope = attrs.GenericForeignKeyAttr('scope', linkify=True, nested=True, max_depth=3) vlan = attrs.RelatedObjectAttr('vlan', linkify=True, label=_('VLAN'), grouped_by='group') status = attrs.ChoiceAttr('status') role = attrs.RelatedObjectAttr('role', linkify=True) @@ -155,7 +155,7 @@ class PrefixPanel(panels.ObjectAttributesPanel): class VLANGroupPanel(panels.ObjectAttributesPanel): name = attrs.TextAttr('name') description = attrs.TextAttr('description') - scope = attrs.GenericForeignKeyAttr('scope', linkify=True) + scope = attrs.GenericForeignKeyAttr('scope', linkify=True, nested=True, max_depth=3) vid_ranges = attrs.TemplatedAttr( 'vid_ranges_items', template_name='ipam/vlangroup/attrs/vid_ranges.html', diff --git a/netbox/netbox/api/fields.py b/netbox/netbox/api/fields.py index 1d72d9467..0466bd980 100644 --- a/netbox/netbox/api/fields.py +++ b/netbox/netbox/api/fields.py @@ -9,6 +9,8 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework.relations import PrimaryKeyRelatedField, RelatedField +from utilities.data import get_inclusive_integer_range_bounds + __all__ = ( 'AttributesField', 'ChoiceField', @@ -173,7 +175,7 @@ class IntegerRangeSerializer(serializers.Serializer): return NumericRange(data[0], data[1] + 1, bounds='[)') def to_representation(self, instance): - return instance.lower, instance.upper - 1 + return get_inclusive_integer_range_bounds(instance) class AttributesField(serializers.JSONField): diff --git a/netbox/netbox/api/views.py b/netbox/netbox/api/views.py index d781ea969..23bbec85a 100644 --- a/netbox/netbox/api/views.py +++ b/netbox/netbox/api/views.py @@ -2,19 +2,18 @@ import platform from django import __version__ as DJANGO_VERSION from django.conf import settings -from django_rq.queues import get_connection from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.views import APIView -from rq.worker import Worker from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired from netbox.plugins.utils import get_installed_plugins from users.api.serializers import UserSerializer from utilities.apps import get_installed_apps +from utilities.rqworker import get_all_workers class APIRootView(APIView): @@ -62,7 +61,7 @@ class StatusView(APIView): 'netbox-full-version': settings.RELEASE.full_version, 'plugins': get_installed_plugins(), 'python-version': platform.python_version(), - 'rq-workers-running': Worker.count(get_connection('default')), + 'rq-workers-running': len(get_all_workers()), }) diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py index 7f753240e..fd49bd7af 100644 --- a/netbox/netbox/api/viewsets/mixins.py +++ b/netbox/netbox/api/viewsets/mixins.py @@ -41,7 +41,10 @@ class ExportTemplatesMixin: def list(self, request, *args, **kwargs): if 'export' in request.GET: object_type = ObjectType.objects.get_for_model(self.get_serializer_class().Meta.model) - et = ExportTemplate.objects.filter(object_types=object_type, name=request.GET['export']).first() + et = ExportTemplate.objects.restrict(request.user, 'view').filter( + object_types=object_type, + name=request.GET['export'], + ).first() if et is None: raise Http404 queryset = self.filter_queryset(self.get_queryset()) diff --git a/netbox/netbox/config/__init__.py b/netbox/netbox/config/__init__.py index b6ca4f90c..1d508c5d5 100644 --- a/netbox/netbox/config/__init__.py +++ b/netbox/netbox/config/__init__.py @@ -20,6 +20,10 @@ _thread_locals = threading.local() logger = logging.getLogger('netbox.config') +# Sentinel used to distinguish a cache miss from a cached "empty" config (an empty dict is a +# legitimate cached value when no ConfigRevision exists). +_MISSING = object() + def get_config(): """ @@ -47,7 +51,9 @@ class Config: """ def __init__(self): self._populate_from_cache() - if not self.config or not self.version: + # Only consult the database when the cache has genuinely never been populated. A cached + # empty config (no ConfigRevision) is authoritative and must not trigger a re-query. + if self._cache_miss: self._populate_from_db() self.defaults = {param.name: param.default for param in PARAMS} @@ -69,8 +75,19 @@ class Config: def _populate_from_cache(self): """Populate config data from Redis cache""" - self.config = cache.get('config') or {} - self.version = cache.get('config_version') + cached_config = cache.get('config', _MISSING) + cached_version = cache.get('config_version', _MISSING) + + # Treat the cache as warm only when both keys are present. A missing 'config_version' + # (e.g. evicted or never written) must re-query the database, even if 'config' is cached. + # The no-revision branch writes both keys (config={}, config_version=None), so the + # intentional empty state is still a cache hit. + self._cache_miss = cached_config is _MISSING or cached_version is _MISSING + + # A cached value of None (ConfigRevision.data is nullable) or {} is a legitimate empty + # config and must not crash attribute access; normalize it to an empty dict. + self.config = {} if cached_config is _MISSING else (cached_config or {}) + self.version = None if cached_version is _MISSING else cached_version if self.config: logger.debug("Loaded configuration data from cache") @@ -86,10 +103,17 @@ class Config: revision = ConfigRevision.objects.order_by('-created').first() if revision is None: logger.debug("No configuration found in database; proceeding with default values") + # Cache the empty state so subsequent requests are served from the cache rather than + # re-querying the database on every request (#22158). Creating the first + # ConfigRevision overwrites this via the post_save handler. + cache.set('config', {}, None) + cache.set('config_version', None, None) + self._populate_from_cache() return logger.debug(f"No active configuration revision found; falling back to most recent (#{revision.pk})") except DatabaseError: - # The database may not be available yet (e.g. when running a management command) + # The database may not be available yet (e.g. when running a management command). Do NOT + # cache anything here, so the next instantiation re-queries once the database is reachable. logger.warning("Skipping config initialization (database unavailable)") return diff --git a/netbox/netbox/config/parameters.py b/netbox/netbox/config/parameters.py index 0c9975941..69d62033b 100644 --- a/netbox/netbox/config/parameters.py +++ b/netbox/netbox/config/parameters.py @@ -229,7 +229,11 @@ PARAMS = ( name='MAPS_URL', label=_('Maps URL'), default='https://maps.google.com/?q=', - description=_("Base URL for mapping geographic locations") + description=_( + "URL for mapping geographic locations. For GPS coordinates, include {lat} and/or {lon} placeholders, " + "or omit them to append coordinates as a comma-separated pair. " + "Note: when {lat} or {lon} placeholders are present, address-based map links will be disabled." + ) ), ) diff --git a/netbox/netbox/configuration_example.py b/netbox/netbox/configuration_example.py index ad73b11c7..528d5ed48 100644 --- a/netbox/netbox/configuration_example.py +++ b/netbox/netbox/configuration_example.py @@ -168,7 +168,7 @@ LOGGING = {} LOGIN_PERSISTENCE = False # The length of time (in seconds) for which a user will remain logged into the web UI before being prompted to -# re-authenticate. (Default: 1209600 [14 days]) +# re-authenticate. If set to None (the default), Django's SESSION_COOKIE_AGE is used (two weeks). LOGIN_TIMEOUT = None # Hide the login form. Useful when only allowing SSO authentication. diff --git a/netbox/netbox/forms/__init__.py b/netbox/netbox/forms/__init__.py index fe5e5f7d3..e61824035 100644 --- a/netbox/netbox/forms/__init__.py +++ b/netbox/netbox/forms/__init__.py @@ -1,5 +1,6 @@ from .bulk_edit import * from .bulk_import import * +from .bulk_rename import * from .filtersets import * from .model_forms import * from .search import * diff --git a/netbox/netbox/forms/bulk_rename.py b/netbox/netbox/forms/bulk_rename.py new file mode 100644 index 000000000..975466f6a --- /dev/null +++ b/netbox/netbox/forms/bulk_rename.py @@ -0,0 +1,14 @@ +from utilities.forms import BulkRenameForm + +from .mixins import ChangelogMessageMixin + +__all__ = ( + 'NetBoxModelBulkRenameForm', +) + + +class NetBoxModelBulkRenameForm(ChangelogMessageMixin, BulkRenameForm): + """ + Extends BulkRenameForm with a changelog message field for NetBox models that support change logging. + """ + pass diff --git a/netbox/netbox/forms/mixins.py b/netbox/netbox/forms/mixins.py index 681047f56..2f031c70f 100644 --- a/netbox/netbox/forms/mixins.py +++ b/netbox/netbox/forms/mixins.py @@ -109,6 +109,10 @@ class SavedFiltersMixin(forms.Form): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # Ensure the underlying ') + return format_html( + '', + title_text, title_text, + ) class BooleanColumn(tables.Column): @@ -716,7 +721,7 @@ class DistanceColumn(TemplateColumn): """ template_code = """ {% load helpers %} - {% if record.distance %}{{ record.distance|floatformat:"-2" }} {{ record.distance_unit }}{% endif %} + {% display_distance record.distance record.distance_unit record.abs_distance %} """ def __init__(self, template_code=template_code, order_by='_abs_distance', **kwargs): diff --git a/netbox/netbox/tests/dummy_plugin/__init__.py b/netbox/netbox/tests/dummy_plugin/__init__.py index 01eb0baa5..4c2be48ad 100644 --- a/netbox/netbox/tests/dummy_plugin/__init__.py +++ b/netbox/netbox/tests/dummy_plugin/__init__.py @@ -24,7 +24,7 @@ class DummyPluginConfig(PluginConfig): def ready(self): super().ready() - from . import jobs, webhook_callbacks # noqa: F401 + from . import jobs, webhook_callbacks, widgets # noqa: F401 config = DummyPluginConfig diff --git a/netbox/netbox/tests/dummy_plugin/widgets.py b/netbox/netbox/tests/dummy_plugin/widgets.py new file mode 100644 index 000000000..95d186842 --- /dev/null +++ b/netbox/netbox/tests/dummy_plugin/widgets.py @@ -0,0 +1,11 @@ +from extras.dashboard.utils import register_widget +from extras.dashboard.widgets import DashboardWidget + + +@register_widget +class DummyDashboardWidget(DashboardWidget): + default_title = 'Dummy Dashboard Widget' + description = 'A dummy dashboard widget for testing plugin registration.' + + def render(self, request): + return 'Dummy dashboard widget content' diff --git a/netbox/netbox/tests/test_api.py b/netbox/netbox/tests/test_api.py index 2529e56cf..9be138d82 100644 --- a/netbox/netbox/tests/test_api.py +++ b/netbox/netbox/tests/test_api.py @@ -1,17 +1,19 @@ import uuid +from django.db.backends.postgresql.psycopg_any import NumericRange from django.test import RequestFactory, TestCase from django.urls import reverse from rest_framework.exceptions import ValidationError from rest_framework.request import Request from netbox.api.exceptions import QuerySetNotOrdered +from netbox.api.fields import IntegerRangeSerializer from netbox.api.pagination import NetBoxPagination from users.models import Token from utilities.testing import APITestCase -class AppTest(APITestCase): +class AppTestCase(APITestCase): def test_http_headers(self): response = self.client.get(reverse('api-root'), **self.header) @@ -46,7 +48,7 @@ class AppTest(APITestCase): self.assertEqual(response.data['id'], self.user.pk) -class NetBoxPaginationTest(TestCase): +class NetBoxPaginationTestCase(TestCase): def setUp(self): self.paginator = NetBoxPagination() @@ -111,3 +113,44 @@ class NetBoxPaginationTest(TestCase): request = self._make_drf_request(query_params={'start': '1', 'ordering': 'created'}) with self.assertRaises(ValidationError): self.paginator.paginate_queryset(queryset, request) + + +class IntegerRangeSerializerTestCase(TestCase): + + def test_to_representation_emits_inclusive_bounds_for_non_canonical_range(self): + """A NumericRange with bounds='[]' must serialize to its inclusive (lower, upper) pair.""" + serializer = IntegerRangeSerializer() + self.assertEqual( + serializer.to_representation(NumericRange(100, 199, bounds='[]')), + (100, 199) + ) + self.assertEqual( + serializer.to_representation(NumericRange(100, 200, bounds='[)')), + (100, 199) + ) + + def test_to_internal_value_produces_canonical_half_open_range(self): + """An inclusive [lo, hi] pair is normalized to NumericRange(lo, hi+1, '[)').""" + serializer = IntegerRangeSerializer() + self.assertEqual( + serializer.to_internal_value([100, 199]), + NumericRange(100, 200, bounds='[)') + ) + + def test_to_internal_value_rejects_malformed_input(self): + """Input must be a two-element list or tuple of ints.""" + serializer = IntegerRangeSerializer() + with self.assertRaises(ValidationError): + serializer.to_internal_value('100-200') + with self.assertRaises(ValidationError): + serializer.to_internal_value([100]) + with self.assertRaises(ValidationError): + serializer.to_internal_value([100, 200, 300]) + + def test_to_internal_value_rejects_non_integer_bounds(self): + """Range boundaries must be integers, not strings or floats.""" + serializer = IntegerRangeSerializer() + with self.assertRaises(ValidationError): + serializer.to_internal_value(['100', '200']) + with self.assertRaises(ValidationError): + serializer.to_internal_value([100.5, 200.5]) diff --git a/netbox/netbox/tests/test_base_classes.py b/netbox/netbox/tests/test_base_classes.py index 5fad1ccd5..cc1ab1de5 100644 --- a/netbox/netbox/tests/test_base_classes.py +++ b/netbox/netbox/tests/test_base_classes.py @@ -52,6 +52,7 @@ from netbox.tables import ( OrganizationalModelTable, PrimaryModelTable, ) +from utilities.testing import TableTestCases class FormClassesTestCase(TestCase): @@ -222,6 +223,15 @@ class TableClassesTestCase(TestCase): model_name = model.__name__ return import_string(f'{app_label}.tables.{model_name}Table') + @staticmethod + def get_table_test_for_model(model): + """ + Import and return the table test class for a given model. + """ + app_label = model._meta.app_label + model_name = model.__name__ + return import_string(f'{app_label}.tests.test_tables.{model_name}TableTestCase') + @staticmethod def get_model_table_base_class(model): """ @@ -255,6 +265,42 @@ class TableClassesTestCase(TestCase): f"{table}.Meta does not inherit from {base_class}.Meta", ) + def test_model_table_test_classes(self): + """ + Check that each model-backed table has a standard table test case. + """ + for model in apps.get_models(): + if self.get_model_table_base_class(model) is None: + continue + + with self.subTest(model=model.__name__): + app_label = model._meta.app_label + model_name = model.__name__ + try: + table = self.get_table_for_model(model) + except ImportError: + self.fail( + f"No table class found for {model_name} " + f"(expected {app_label}.tables.{model_name}Table)" + ) + try: + table_test = self.get_table_test_for_model(model) + except ImportError: + self.fail( + f"No table test case found for {model_name} " + f"(expected {app_label}.tests.test_tables.{model_name}TableTestCase)" + ) + + self.assertTrue( + issubclass(table_test, TableTestCases.StandardTableTestCase), + f"{table_test} does not inherit from {TableTestCases.StandardTableTestCase}", + ) + self.assertIs( + table_test.table, + table, + f"{table_test}.table is not set to {table}", + ) + class SerializerClassesTestCase(TestCase): diff --git a/netbox/netbox/tests/test_config.py b/netbox/netbox/tests/test_config.py index ebdfdb166..9c0d721a3 100644 --- a/netbox/netbox/tests/test_config.py +++ b/netbox/netbox/tests/test_config.py @@ -1,31 +1,108 @@ -from django.conf import settings from django.core.cache import cache +from django.db import connection from django.test import TestCase, override_settings +from django.test.utils import CaptureQueriesContext from core.models import ConfigRevision from netbox.config import clear_config, get_config -# Prefix cache keys to avoid interfering with the local environment -CACHES = settings.CACHES -CACHES['default'].update({'KEY_PREFIX': 'TEST-'}) + +def _configrevision_query_count(queries): + """Count captured queries that touch the core_configrevision table.""" + return len([q for q in queries if 'core_configrevision' in q['sql']]) +# Use a per-process in-memory cache so the shared 'config'/'config_version' keys can't be +# contaminated by other tests running in parallel against the same Redis instance. +@override_settings(CACHES={ + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'netbox-config-tests', + }, +}) class ConfigTestCase(TestCase): - @override_settings(CACHES=CACHES) - def test_config_init_empty(self): + def setUp(self): + super().setUp() + # Register cleanup so it runs after each test, even on assertion failure. + # clear_config drops the thread-local Config; cache.clear wipes the cache + # entries that configrevision.activate writes. + self.addCleanup(cache.clear) + self.addCleanup(clear_config) + + # Defensive reset against pre-existing state from outside this class. + clear_config() cache.clear() + def test_config_init_empty(self): config = get_config() self.assertEqual(config.config, {}) self.assertEqual(config.version, None) + def test_empty_config_not_requeried(self): + # With no ConfigRevision present, the empty state should be cached after the first load so + # that subsequent requests are served from the cache rather than re-querying the database + # on every request (#22158). + with CaptureQueriesContext(connection) as ctx: + first = get_config() + self.assertGreaterEqual(_configrevision_query_count(ctx.captured_queries), 1) + self.assertEqual(first.config, {}) + self.assertEqual(first.version, None) + + # Simulate the request boundary, where the middleware drops the thread-local config. clear_config() - @override_settings(CACHES=CACHES) + with CaptureQueriesContext(connection) as ctx: + second = get_config() + self.assertEqual(_configrevision_query_count(ctx.captured_queries), 0) + self.assertEqual(second.config, {}) + self.assertEqual(second.version, None) + + def test_empty_then_create_first_revision(self): + # Prime the cache with the empty state. + get_config() + + # Creating the first ConfigRevision fires the post_save handler, which activates it and + # overwrites the cached empty state. + CONFIG_DATA = {'BANNER_TOP': 'A'} + configrevision = ConfigRevision.objects.create(data=CONFIG_DATA) + + clear_config() + config = get_config() + self.assertEqual(config.config, CONFIG_DATA) + self.assertEqual(config.version, configrevision.pk) + + def test_config_init_from_cache_null_data(self): + # ConfigRevision.data is nullable; a revision with data=None caches None. Config access + # must normalize that to an empty dict rather than crash on attribute lookup. + configrevision = ConfigRevision.objects.create(data=None) + configrevision.activate() + + clear_config() + config = get_config() + self.assertEqual(config.config, {}) + self.assertEqual(config.version, configrevision.pk) + # Attribute access must fall back to defaults without raising. + self.assertEqual(config.BANNER_TOP, '') + + def test_missing_version_key_requeries(self): + # If 'config_version' is missing (evicted/never written) while 'config' is cached, the + # cache must be treated as cold and re-populated from the database rather than leaving + # version=None when a ConfigRevision exists. + CONFIG_DATA = {'BANNER_TOP': 'A'} + configrevision = ConfigRevision.objects.create(data=CONFIG_DATA) + configrevision.activate() + + # Drop only the version key, leaving 'config' populated. + cache.delete('config_version') + clear_config() + + config = get_config() + self.assertEqual(config.config, CONFIG_DATA) + self.assertEqual(config.version, configrevision.pk) + def test_config_init_from_db(self): CONFIG_DATA = {'BANNER_TOP': 'A'} - cache.clear() # Create a config but don't load it into the cache configrevision = ConfigRevision.objects.create(data=CONFIG_DATA) @@ -34,12 +111,8 @@ class ConfigTestCase(TestCase): self.assertEqual(config.config, CONFIG_DATA) self.assertEqual(config.version, configrevision.pk) - clear_config() - - @override_settings(CACHES=CACHES) def test_config_init_from_cache(self): CONFIG_DATA = {'BANNER_TOP': 'B'} - cache.clear() # Create a config and load it into the cache configrevision = ConfigRevision.objects.create(data=CONFIG_DATA) @@ -49,12 +122,9 @@ class ConfigTestCase(TestCase): self.assertEqual(config.config, CONFIG_DATA) self.assertEqual(config.version, configrevision.pk) - clear_config() - - @override_settings(CACHES=CACHES, BANNER_TOP='Z') + @override_settings(BANNER_TOP='Z') def test_settings_override(self): CONFIG_DATA = {'BANNER_TOP': 'A'} - cache.clear() # Create a config and load it into the cache configrevision = ConfigRevision.objects.create(data=CONFIG_DATA) @@ -63,5 +133,3 @@ class ConfigTestCase(TestCase): config = get_config() self.assertEqual(config.BANNER_TOP, 'Z') self.assertEqual(config.version, configrevision.pk) - - clear_config() diff --git a/netbox/netbox/tests/test_forms.py b/netbox/netbox/tests/test_forms.py index f375d82ff..8a01c393a 100644 --- a/netbox/netbox/tests/test_forms.py +++ b/netbox/netbox/tests/test_forms.py @@ -5,7 +5,7 @@ from dcim.forms import InterfaceImportForm from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site -class NetBoxModelImportFormCleanTest(TestCase): +class NetBoxModelImportFormCleanTestCase(TestCase): """ Test the clean() method of NetBoxModelImportForm to ensure it properly converts empty strings to None for nullable fields during CSV import. diff --git a/netbox/netbox/tests/test_graphql.py b/netbox/netbox/tests/test_graphql.py index 3914a9493..2df3a7f98 100644 --- a/netbox/netbox/tests/test_graphql.py +++ b/netbox/netbox/tests/test_graphql.py @@ -1,11 +1,19 @@ import json +import strawberry +from django.contrib.contenttypes.models import ContentType from django.test import override_settings from django.urls import reverse from rest_framework import status +from strawberry.extensions import QueryDepthLimiter +from strawberry.schema.config import StrawberryConfig from dcim.choices import LocationStatusChoices from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Site, VirtualChassis +from extras.models import TableConfig +from netbox.graphql.scalars import BigInt, BigIntScalar +from netbox.graphql.schema import Query, get_schema_extensions +from utilities.tables import get_table_for_model from utilities.testing import APITestCase, TestCase, disable_warnings @@ -20,6 +28,45 @@ class GraphQLTestCase(TestCase): response = self.client.get(url) self.assertHttpStatus(response, 404) + def test_graphql_max_query_depth_disabled_by_default(self): + """ + QueryDepthLimiter should not be installed when GRAPHQL_MAX_QUERY_DEPTH is unset. + """ + self.assertFalse(any(isinstance(ext, QueryDepthLimiter) for ext in get_schema_extensions())) + + @override_settings(GRAPHQL_MAX_QUERY_DEPTH=0) + def test_graphql_max_query_depth_disabled_when_zero(self): + """ + QueryDepthLimiter should not be installed when GRAPHQL_MAX_QUERY_DEPTH is zero. + """ + self.assertFalse(any(isinstance(ext, QueryDepthLimiter) for ext in get_schema_extensions())) + + @override_settings(GRAPHQL_MAX_QUERY_DEPTH=-1) + def test_graphql_max_query_depth_disabled_when_negative(self): + """ + QueryDepthLimiter should not be installed when GRAPHQL_MAX_QUERY_DEPTH is negative. + """ + self.assertFalse(any(isinstance(ext, QueryDepthLimiter) for ext in get_schema_extensions())) + + @override_settings(GRAPHQL_MAX_QUERY_DEPTH=3) + def test_graphql_max_query_depth_enforced(self): + """ + Queries exceeding GRAPHQL_MAX_QUERY_DEPTH should be rejected. + """ + extensions = get_schema_extensions() + self.assertTrue(any(isinstance(ext, QueryDepthLimiter) for ext in extensions)) + + # Build a temporary schema with the configured extensions and execute a deep query + test_schema = strawberry.Schema( + query=Query, + config=StrawberryConfig(auto_camel_case=False, scalar_map={BigInt: BigIntScalar}), + extensions=extensions, + ) + deep_query = '{ site_list { tenant { group { parent { parent { parent { name } } } } } } }' + result = test_schema.execute_sync(deep_query) + self.assertIsNotNone(result.errors) + self.assertIn('exceeds maximum operation depth', str(result.errors[0])) + @override_settings(LOGIN_REQUIRED=True) def test_graphiql_interface(self): """ @@ -172,6 +219,26 @@ class GraphQLAPITestCase(APITestCase): self.assertNotIn('errors', data) self.assertEqual(len(data['data']['device_list']), 3) + def test_graphql_tableconfig_object_type_exposes_id(self): + """TableConfigType.object_type must expose ContentType fields (e.g. id).""" + self.add_permissions('extras.view_tableconfig') + url = reverse('graphql') + + site_ct = ContentType.objects.get_for_model(Site) + table_config = TableConfig.objects.create( + object_type=site_ct, + table=get_table_for_model(Site).__name__, + name='Test config', + columns=['name'], + ) + + query = '{ table_config(id: ' + str(table_config.pk) + ') { object_type { id } } }' + response = self.client.post(url, data={'query': query}, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + data = json.loads(response.content) + self.assertNotIn('errors', data) + self.assertEqual(int(data['data']['table_config']['object_type']['id']), site_ct.pk) + def test_offset_pagination(self): self.add_permissions('dcim.view_site') url = reverse('graphql') diff --git a/netbox/netbox/tests/test_import.py b/netbox/netbox/tests/test_import.py index 690a6dc14..2025172fd 100644 --- a/netbox/netbox/tests/test_import.py +++ b/netbox/netbox/tests/test_import.py @@ -1,5 +1,3 @@ -from django.test import override_settings - from core.models import ObjectType from dcim.models import * from extras.models import CustomField @@ -48,8 +46,8 @@ class CSVImportTestCase(ModelViewTestCase): self.assertHttpStatus(self.client.post(self._get_url('bulk_import'), data), 302) self.assertEqual(Region.objects.count(), 3) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_valid_tags(self): + self.add_permissions('dcim.view_region', 'extras.view_tag') csv_data = ( 'name,slug,tags', 'Region 1,region-1,"alpha,bravo"', @@ -91,8 +89,8 @@ class CSVImportTestCase(ModelViewTestCase): ) self.assertEqual(regions[3].tags.count(), 0) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_invalid_tags(self): + self.add_permissions('dcim.view_region', 'extras.view_tag') csv_data = ( 'name,slug,tags', 'Region 1,region-1,"Alpha,Bravo"', # Valid @@ -117,9 +115,8 @@ class CSVImportTestCase(ModelViewTestCase): self.assertHttpStatus(self.client.post(self._get_url('bulk_import'), data), 200) self.assertEqual(Region.objects.count(), 0) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_custom_field_defaults(self): - self.add_permissions('dcim.add_region') + self.add_permissions('dcim.view_region', 'dcim.add_region') csv_data = [ 'name,slug,description', 'Region 1,region-1,abc', diff --git a/netbox/netbox/tests/test_jobs.py b/netbox/netbox/tests/test_jobs.py index 5bb073951..3691592e2 100644 --- a/netbox/netbox/tests/test_jobs.py +++ b/netbox/netbox/tests/test_jobs.py @@ -1,13 +1,15 @@ +import uuid from datetime import timedelta +from unittest.mock import patch from django.test import TestCase from django.utils import timezone -from django_rq import get_queue from core.choices import JobStatusChoices from core.exceptions import JobFailed from core.models import DataSource, Job from utilities.testing import disable_warnings +from utilities.testing.mixins import RQQueueTestMixin from ..jobs import * from ..jobs import _INSTALL_ROOT @@ -24,14 +26,14 @@ class TestJobRunner(JobRunner): self.logger.error("Error message") -class JobRunnerTestCase(TestCase): - def tearDown(self): - super().tearDown() +@system_job(interval=60) +class TestSystemJobRunner(JobRunner): - # Clear all queues after running each test - get_queue('default').connection.flushall() - get_queue('high').connection.flushall() - get_queue('low').connection.flushall() + def run(self, *args, **kwargs): + pass + + +class BaseJobRunnerTestCase(RQQueueTestMixin, TestCase): @staticmethod def get_schedule_at(offset=1): @@ -39,9 +41,9 @@ class JobRunnerTestCase(TestCase): return timezone.now() + timedelta(weeks=offset) -class JobRunnerTest(JobRunnerTestCase): +class JobRunnerTestCase(BaseJobRunnerTestCase): """ - Test internal logic of `JobRunner`. + Test the internal logic of `JobRunner`. """ def test_name_default(self): @@ -92,7 +94,7 @@ class JobRunnerTest(JobRunnerTestCase): self.assertNotIn(_INSTALL_ROOT, tb_message) -class EnqueueTest(JobRunnerTestCase): +class EnqueueTestCase(BaseJobRunnerTestCase): """ Test enqueuing of `JobRunner`. """ @@ -168,11 +170,11 @@ class EnqueueTest(JobRunnerTestCase): self.assertEqual(TestJobRunner.get_jobs(instance).count(), 1) -class SystemJobTest(JobRunnerTestCase): +class SystemJobTestCase(BaseJobRunnerTestCase): """ Test that system jobs can be scheduled. - General functionality already tested by `JobRunnerTest` and `EnqueueTest`. + General functionality already tested by `JobRunnerTestCase` and `EnqueueTestCase`. """ def test_scheduling(self): @@ -193,3 +195,125 @@ class SystemJobTest(JobRunnerTestCase): self.assertEqual(job1, job2) self.assertEqual(TestJobRunner.get_jobs().count(), 1) + + def test_handle_skips_reschedule_when_successor_exists(self): + """ + When `handle()` finishes a periodic system job, it must not create a duplicate + scheduled job if a successor is already enqueued (issue #22232). This guards + against the race where a worker starts up between `job.terminate()` and the + finally block's reschedule, calling `enqueue_once()` which would create a parallel + job. + """ + interval = 60 + + # Simulate a successor that was already created by another worker. + successor = Job.objects.create( + name=TestSystemJobRunner.name, + status=JobStatusChoices.STATUS_SCHEDULED, + interval=interval, + scheduled=self.get_schedule_at(), + job_id=uuid.uuid4(), + ) + + # The just-finished job. `handle()` will run its finally block. + finished = Job.objects.create( + name=TestSystemJobRunner.name, + status=JobStatusChoices.STATUS_COMPLETED, + interval=interval, + started=timezone.now(), + completed=timezone.now(), + job_id=uuid.uuid4(), + ) + + TestSystemJobRunner.handle(finished) + + # Only the original successor should remain enqueued — no duplicate should have + # been created. + enqueued = Job.objects.filter( + name=TestSystemJobRunner.name, + status__in=JobStatusChoices.ENQUEUED_STATE_CHOICES, + interval=interval, + ) + self.assertEqual(enqueued.count(), 1) + self.assertEqual(enqueued.first().pk, successor.pk) + + def test_handle_reschedules_when_only_instance_bound_successor_exists(self): + """ + For a system (object-less) job, an instance-bound job of the same JobRunner class + must not be treated as a successor. The system job should still reschedule itself. + """ + interval = 60 + instance = DataSource.objects.create(name='test-ds', type='local') + + # An instance-bound enqueued job of the same class and interval — must NOT be + # treated as a successor of the object-less finished job. + Job.objects.create( + name=TestSystemJobRunner.name, + object=instance, + status=JobStatusChoices.STATUS_SCHEDULED, + interval=interval, + scheduled=self.get_schedule_at(), + job_id=uuid.uuid4(), + ) + + # Object-less finished system job. + finished = Job.objects.create( + name=TestSystemJobRunner.name, + status=JobStatusChoices.STATUS_COMPLETED, + interval=interval, + started=timezone.now(), + completed=timezone.now(), + job_id=uuid.uuid4(), + ) + + TestSystemJobRunner.handle(finished) + + # A new object-less successor should have been scheduled. + enqueued = Job.objects.filter( + name=TestSystemJobRunner.name, + object_id__isnull=True, + status__in=JobStatusChoices.ENQUEUED_STATE_CHOICES, + interval=interval, + ) + self.assertEqual(enqueued.count(), 1) + + def test_handle_reschedules_non_system_job_independently(self): + """ + Two recurring non-system jobs (e.g. scheduled scripts) for the same runner and + object with the same interval but distinct runtime kwargs must each reschedule + themselves; one must not be treated as the successor of the other and skipped. + """ + interval = 60 + instance = DataSource.objects.create(name='test-ds-script', type='local') + + # An unrelated recurring schedule for the same runner/object/interval. Stands in + # for a second scheduled-script entry with different `data`. + Job.objects.create( + name=TestJobRunner.name, + object=instance, + status=JobStatusChoices.STATUS_SCHEDULED, + interval=interval, + scheduled=self.get_schedule_at(), + job_id=uuid.uuid4(), + ) + + finished = Job.objects.create( + name=TestJobRunner.name, + object=instance, + status=JobStatusChoices.STATUS_COMPLETED, + interval=interval, + started=timezone.now(), + completed=timezone.now(), + job_id=uuid.uuid4(), + ) + + with patch.object(TestJobRunner, 'run'): + TestJobRunner.handle(finished) + + # Both the unrelated schedule and the finished job's successor should be enqueued. + enqueued = Job.objects.filter( + name=TestJobRunner.name, + status__in=JobStatusChoices.ENQUEUED_STATE_CHOICES, + interval=interval, + ) + self.assertEqual(enqueued.count(), 2) diff --git a/netbox/netbox/tests/test_migrations.py b/netbox/netbox/tests/test_migrations.py new file mode 100644 index 000000000..4d8c3b583 --- /dev/null +++ b/netbox/netbox/tests/test_migrations.py @@ -0,0 +1,89 @@ +import re +from collections import defaultdict + +from django.apps import apps +from django.conf import settings +from django.db.migrations.loader import MigrationLoader +from django.test import SimpleTestCase + +# Migrations which share a numeric prefix with a sibling migration but predate +# enforcement of this convention. New duplicates must not be added; these are +# grandfathered in to avoid rewriting historical migration history. +# +# Keyed by app label, valued by the set of duplicated numeric prefixes. +EXEMPTIONS = { + 'dcim': {'0203', '0226'}, +} + +MIGRATION_PREFIX_RE = re.compile(r'^(\d+)_') + + +def first_party_app_labels(): + """ + Return the labels of NetBox's own apps (those living within the project + source tree), excluding third-party and plugin apps. + """ + labels = [] + for app_config in apps.get_app_configs(): + if app_config.path and app_config.path.startswith(settings.BASE_DIR): + labels.append(app_config.label) + return labels + + +class DuplicateMigrationIndexTestCase(SimpleTestCase): + """ + Ensure that no two migrations within the same app share a numeric prefix + (e.g. ``0123_foo.py`` and ``0123_bar.py``). While technically permissible, + duplicate indexes invite confusion and have never been NetBox's (or + Django's) convention. See netbox-community/netbox#21945. + """ + + def find_duplicate_prefixes(self, app_labels): + """ + Map each given app label to the numeric prefixes shared by more than + one of its migrations: ``{app_label: {prefix: [migration_name, ...]}}``. + """ + per_app = {label: defaultdict(list) for label in app_labels} + loader = MigrationLoader(connection=None, ignore_no_migrations=True) + + for (migration_app, migration_name) in loader.disk_migrations: + if migration_app not in per_app: + continue + match = MIGRATION_PREFIX_RE.match(migration_name) + if match: + per_app[migration_app][match.group(1)].append(migration_name) + + return { + label: {prefix: sorted(names) for prefix, names in prefixes.items() if len(names) > 1} + for label, prefixes in per_app.items() + } + + def test_no_duplicate_migration_indexes(self): + errors = [] + app_labels = first_party_app_labels() + duplicates_by_app = self.find_duplicate_prefixes(app_labels) + + for app_label in app_labels: + duplicates = duplicates_by_app[app_label] + exempted = EXEMPTIONS.get(app_label, set()) + + # Flag any newly introduced duplicate prefixes. + for prefix, names in sorted(duplicates.items()): + if prefix in exempted: + continue + joined = ', '.join(f'{name}.py' for name in names) + errors.append(f' {app_label}: prefix {prefix} is shared by {joined}') + + # Flag stale exemptions whose duplicates have since been resolved, + # so the exemption list cannot rot. + for prefix in sorted(exempted - set(duplicates)): + errors.append( + f' {app_label}: stale exemption for prefix {prefix} (no longer duplicated); ' + f'remove it from EXEMPTIONS' + ) + + self.assertFalse( + errors, + 'Duplicate migration indexes detected. Renumber new migrations so each has a unique ' + 'numeric prefix within its app:\n' + '\n'.join(errors), + ) diff --git a/netbox/netbox/tests/test_model_test_coverage.py b/netbox/netbox/tests/test_model_test_coverage.py new file mode 100644 index 000000000..ee8241efc --- /dev/null +++ b/netbox/netbox/tests/test_model_test_coverage.py @@ -0,0 +1,300 @@ +import inspect +from importlib import import_module +from importlib.util import find_spec +from unittest.mock import MagicMock, patch + +from django.test import TestCase +from django.urls import NoReverseMatch + +from core.filtersets import ObjectTypeFilterSet +from core.models import ObjectType +from netbox.registry import registry +from utilities.testing import APITestCase, BaseFilterSetTests, ModelViewTestCase +from utilities.views import get_action_url + + +def intentional(reason): + return f'Intentional non-standard model coverage: {reason}' + + +EXEMPTIONS = { + 'api': { + 'core.objectchange': intentional( + 'Read-only audit log; changelog behavior is primarily tested through ' + 'parent objects rather than standalone CRUD-style model tests.' + ), + 'extras.script': intentional( + 'Database-backed script definitions expose bespoke run/admin behavior rather than standard CRUD semantics.' + ), + }, + 'views': { + 'core.objectchange': intentional( + 'Read-only audit log; changelog behavior is tested through parent object ' + 'views rather than a standalone ModelViewTestCase.' + ), + 'core.configrevision': intentional('Read-only configuration audit/history model, not a standard CRUD viewset.'), + 'extras.script': intentional( + 'Database-backed script definitions use bespoke script admin/run views ' + 'rather than the standard CRUD view pattern.' + ), + }, + 'filtersets': { + 'core.configrevision': intentional( + 'Read-only configuration audit/history model, not standard filterset test coverage.' + ), + 'extras.script': intentional( + 'Database-backed script definitions use bespoke behavior rather than standard model filtering workflows.' + ), + }, +} + + +def model_label(model): + return f'{model._meta.app_label}.{model._meta.model_name}' + + +def import_optional(module_name): + """ + Import an optional submodule if it exists. + + Returns None when the module (or its parent package) cannot be located. + Import errors raised from inside an existing module are propagated so + that real bugs do not get silently swallowed. + """ + try: + spec = find_spec(module_name) + except ModuleNotFoundError as err: + if module_name == err.name or module_name.startswith(f'{err.name}.'): + return None + raise + + if spec is None: + return None + + return import_module(module_name) + + +def has_tests(cls): + return any(name.startswith('test_') and callable(getattr(cls, name)) for name in dir(cls)) + + +def get_queryset_model(cls): + queryset = getattr(cls, 'queryset', None) + if queryset is not None: + return queryset.model + + filterset = getattr(cls, 'filterset', None) + if filterset is not None: + return filterset._meta.model + + return None + + +class ModelTestCoverageTestCase(TestCase): + def get_public_models(self): + models = [] + + for object_type in ObjectType.objects.public().order_by('app_label', 'model'): + if object_type.is_plugin_model: + continue + + model = object_type.model_class() + if model is not None: + models.append(model) + + return models + + def get_app_labels(self, models): + return sorted({model._meta.app_label for model in models}) + + def collect_covered_models(self, app_labels, module_suffix, base_class, get_model): + covered = {} + + for app_label in app_labels: + module = import_optional(f'{app_label}.tests.{module_suffix}') + if module is None: + continue + + for _, cls in inspect.getmembers(module, inspect.isclass): + if cls.__module__ != module.__name__: + continue + if not issubclass(cls, base_class): + continue + if not has_tests(cls): + continue + + model = get_model(cls) + if model is not None: + covered.setdefault(model, []).append(f'{cls.__module__}.{cls.__qualname__}') + + return covered + + def has_action_url(self, model, action='list', rest_api=False): + try: + get_action_url(model, action=action, rest_api=rest_api) + except NoReverseMatch: + return False + + return True + + def format_exemptions(self, category, labels): + return '\n'.join(f' - {label}: {EXEMPTIONS[category][label]}' for label in labels) + + def assert_coverage(self, category, expected, covered): + expected_labels = {model_label(model) for model in expected} + covered_labels = {model_label(model) for model in covered} + exempted_labels = set(EXEMPTIONS[category]) + + stale_exemptions = sorted(exempted_labels - expected_labels) + self.assertFalse( + stale_exemptions, + f'Remove stale {category} coverage exemptions; these models are ' + f'no longer expected in this category:\n' + self.format_exemptions(category, stale_exemptions), + ) + + covered_exemptions = sorted(exempted_labels & covered_labels) + self.assertFalse( + covered_exemptions, + f'Remove stale {category} coverage exemptions; these models now ' + f'have standard test coverage:\n' + self.format_exemptions(category, covered_exemptions), + ) + + missing = sorted(expected_labels - covered_labels - exempted_labels) + self.assertFalse( + missing, + f'Missing {category} test coverage for:\n' + '\n'.join(f' - {label}' for label in missing), + ) + + def test_api_test_cases_exist_for_api_models(self): + models = self.get_public_models() + app_labels = self.get_app_labels(models) + + expected = {model for model in models if self.has_action_url(model, action='list', rest_api=True)} + + covered = self.collect_covered_models( + app_labels, + 'test_api', + APITestCase, + lambda cls: getattr(cls, 'model', None), + ) + + self.assert_coverage('api', expected, covered) + + def test_view_test_cases_exist_for_ui_models(self): + models = self.get_public_models() + app_labels = self.get_app_labels(models) + + expected = {model for model in models if self.has_action_url(model, action='list')} + + covered = self.collect_covered_models( + app_labels, + 'test_views', + ModelViewTestCase, + lambda cls: getattr(cls, 'model', None), + ) + + self.assert_coverage('views', expected, covered) + + def test_filterset_test_cases_exist_for_registered_filtersets(self): + models = self.get_public_models() + app_labels = self.get_app_labels(models) + + # Defensive import: app filtersets are normally imported during app/test + # setup, but loading them here keeps the test self-contained. + for app_label in app_labels: + import_optional(f'{app_label}.filtersets') + + expected = {model for model in models if model_label(model) in registry['filtersets']} + + covered = self.collect_covered_models( + app_labels, + 'test_filtersets', + BaseFilterSetTests, + get_queryset_model, + ) + + self.assert_coverage('filtersets', expected, covered) + + +class HelperFunctionTestCase(TestCase): + """ + Exercise the helper functions directly so their defensive branches are + covered even when the main meta-test doesn't trigger them organically. + """ + + def test_import_optional_returns_module_when_present(self): + module = import_optional('netbox.tests.test_model_test_coverage') + self.assertIsNotNone(module) + self.assertEqual(module.__name__, 'netbox.tests.test_model_test_coverage') + + def test_import_optional_returns_none_for_missing_submodule(self): + # Parent package exists, leaf does not -> find_spec returns None. + self.assertIsNone(import_optional('netbox.tests.does_not_exist')) + + def test_import_optional_returns_none_for_missing_parent(self): + # Parent package itself does not exist -> ModuleNotFoundError swallowed. + self.assertIsNone(import_optional('nonexistent_pkg.tests.test_api')) + + def test_import_optional_propagates_unrelated_find_spec_error(self): + # find_spec raising ModuleNotFoundError for a different module than the + # one requested means a parent package itself failed to import; that + # error must propagate so real bugs aren't silently swallowed. + with patch( + 'netbox.tests.test_model_test_coverage.find_spec', + side_effect=ModuleNotFoundError( + "No module named 'unrelated_dependency'", + name='unrelated_dependency', + ), + ): + with self.assertRaises(ModuleNotFoundError): + import_optional('netbox.tests.test_model_test_coverage') + + def test_import_optional_propagates_import_module_errors(self): + # If find_spec succeeds but import_module raises (e.g. the module + # exists on disk but a dependency inside it fails to import), the + # error must propagate rather than being treated as "module missing." + with ( + patch( + 'netbox.tests.test_model_test_coverage.find_spec', + return_value=MagicMock(), + ), + patch( + 'netbox.tests.test_model_test_coverage.import_module', + side_effect=ModuleNotFoundError( + "No module named 'unrelated_dependency'", + name='unrelated_dependency', + ), + ), + ): + with self.assertRaises(ModuleNotFoundError): + import_optional('netbox.tests.test_model_test_coverage') + + def test_has_tests_detects_test_methods(self): + class WithTest: + def test_thing(self): + pass + + class WithoutTest: + def helper(self): + pass + + self.assertTrue(has_tests(WithTest)) + self.assertFalse(has_tests(WithoutTest)) + + def test_get_queryset_model_prefers_queryset(self): + class Sample: + queryset = ObjectType.objects.all() + + self.assertEqual(get_queryset_model(Sample), ObjectType) + + def test_get_queryset_model_falls_back_to_filterset(self): + class Sample: + filterset = ObjectTypeFilterSet + + self.assertEqual(get_queryset_model(Sample), ObjectType) + + def test_get_queryset_model_returns_none_when_unset(self): + class Sample: + pass + + self.assertIsNone(get_queryset_model(Sample)) diff --git a/netbox/netbox/tests/test_models.py b/netbox/netbox/tests/test_models.py index 3da4144c2..0351aea82 100644 --- a/netbox/netbox/tests/test_models.py +++ b/netbox/netbox/tests/test_models.py @@ -7,7 +7,7 @@ from core.models import ObjectChange from netbox.tests.dummy_plugin.models import DummyNetBoxModel -class ModelTest(TestCase): +class ModelTestCase(TestCase): def test_get_absolute_url(self): m = ObjectChange() diff --git a/netbox/netbox/tests/test_object_actions.py b/netbox/netbox/tests/test_object_actions.py index fc195d4e5..e99a83c6c 100644 --- a/netbox/netbox/tests/test_object_actions.py +++ b/netbox/netbox/tests/test_object_actions.py @@ -7,7 +7,7 @@ from dcim.models import Device, DeviceType, Manufacturer from netbox.object_actions import AddObject, BulkEdit, BulkImport -class ObjectActionTest(TestCase): +class ObjectActionTestCase(TestCase): def test_get_url_core_model(self): """Test URL generation for core NetBox models""" diff --git a/netbox/netbox/tests/test_plugins.py b/netbox/netbox/tests/test_plugins.py index c0eecd09d..1292d5ad1 100644 --- a/netbox/netbox/tests/test_plugins.py +++ b/netbox/netbox/tests/test_plugins.py @@ -7,6 +7,7 @@ from django.urls import reverse from core.choices import JobIntervalChoices from core.models import ObjectType +from extras.dashboard.widgets import DashboardWidget from netbox.graphql.schema import Query from netbox.plugins.navigation import PluginMenu, PluginMenuButton, PluginMenuItem from netbox.plugins.utils import get_plugin_config @@ -18,7 +19,7 @@ from netbox.tests.dummy_plugin.webhook_callbacks import set_context @skipIf('netbox.tests.dummy_plugin' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS") -class PluginTest(TestCase): +class PluginTestCase(TestCase): def test_config(self): @@ -103,6 +104,15 @@ class PluginTest(TestCase): self.assertIn(GlobalContent, registry['plugins']['template_extensions'][None]) self.assertIn(SiteContent, registry['plugins']['template_extensions']['dcim.site']) + def test_dashboard_widget(self): + """ + Check that plugin dashboard widgets are registered. + """ + self.assertIn('netbox.DummyDashboardWidget', registry['widgets']) + + widget_class = registry['widgets']['netbox.DummyDashboardWidget'] + self.assertTrue(issubclass(widget_class, DashboardWidget)) + def test_registered_columns(self): """ Check that a plugin can register a custom column on a core model table. @@ -229,7 +239,7 @@ class PluginTest(TestCase): self.assertIn(set_context, registry['webhook_callbacks']) -class PluginNavigationTest(TestCase): +class PluginNavigationTestCase(TestCase): def test_plugin_menu_item_independent_permissions(self): item1 = PluginMenuItem(link='test1', link_text='Test 1') diff --git a/netbox/netbox/tests/test_registry.py b/netbox/netbox/tests/test_registry.py index e834c4356..bb2ed1713 100644 --- a/netbox/netbox/tests/test_registry.py +++ b/netbox/netbox/tests/test_registry.py @@ -3,7 +3,7 @@ from django.test import TestCase from netbox.registry import Registry -class RegistryTest(TestCase): +class RegistryTestCase(TestCase): def test_set_store(self): reg = Registry({ diff --git a/netbox/netbox/tests/test_tables.py b/netbox/netbox/tests/test_tables.py index fcab9fd79..aa6fd5ad2 100644 --- a/netbox/netbox/tests/test_tables.py +++ b/netbox/netbox/tests/test_tables.py @@ -8,7 +8,7 @@ from netbox.tables import NetBoxTable, columns from utilities.testing import create_tags, create_test_device, create_test_user -class BaseTableTest(TestCase): +class BaseTableTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -99,7 +99,7 @@ class TagColumnTable(NetBoxTable): default_columns = fields -class TagColumnTest(TestCase): +class TagColumnTestCase(TestCase): @classmethod def setUpTestData(cls): diff --git a/netbox/netbox/tests/test_ui.py b/netbox/netbox/tests/test_ui.py index 31c38bf81..55cbb3842 100644 --- a/netbox/netbox/tests/test_ui.py +++ b/netbox/netbox/tests/test_ui.py @@ -1,6 +1,8 @@ +from decimal import Decimal from types import SimpleNamespace -from django.test import RequestFactory, TestCase +from django.template import Context, Template +from django.test import RequestFactory, SimpleTestCase, TestCase from circuits.choices import CircuitStatusChoices, VirtualCircuitTerminationRoleChoices from circuits.models import ( @@ -15,6 +17,7 @@ from dcim.choices import InterfaceTypeChoices from dcim.models import Interface, Site from netbox.ui import attrs from netbox.ui.panels import ObjectsTablePanel +from netbox.ui.utils import build_coords_url from users.models import ObjectPermission, User from utilities.testing import create_test_device from vpn.choices import ( @@ -29,7 +32,7 @@ from vpn.choices import ( from vpn.models import IKEPolicy, IKEProposal, IPSecPolicy, IPSecProfile -class ChoiceAttrTest(TestCase): +class ChoiceAttrTestCase(TestCase): """ Test class for validating the behavior of ChoiceAttr attribute accessor. @@ -112,7 +115,7 @@ class ChoiceAttrTest(TestCase): ) -class RelatedObjectListAttrTest(TestCase): +class RelatedObjectListAttrTestCase(TestCase): """ Test suite for RelatedObjectListAttr functionality. @@ -222,7 +225,7 @@ class RelatedObjectListAttrTest(TestCase): self.assertIn('…', rendered) -class TextAttrTest(TestCase): +class TextAttrTestCase(TestCase): def test_get_value_with_format_string(self): attr = attrs.TextAttr('asn', format_string='AS{}') @@ -247,7 +250,7 @@ class TextAttrTest(TestCase): self.assertTrue(context['copy_button']) -class NumericAttrTest(TestCase): +class NumericAttrTestCase(TestCase): def test_get_context_with_unit_accessor(self): attr = attrs.NumericAttr('speed', unit_accessor='speed_unit') @@ -268,7 +271,7 @@ class NumericAttrTest(TestCase): self.assertTrue(context['copy_button']) -class BooleanAttrTest(TestCase): +class BooleanAttrTestCase(TestCase): def test_false_value_shown_by_default(self): attr = attrs.BooleanAttr('enabled') @@ -286,7 +289,7 @@ class BooleanAttrTest(TestCase): self.assertIs(attr.get_value(obj), True) -class ImageAttrTest(TestCase): +class ImageAttrTestCase(TestCase): def test_invalid_decoding_raises_value_error(self): with self.assertRaises(ValueError): @@ -314,7 +317,7 @@ class ImageAttrTest(TestCase): self.assertFalse(context['load_lazy']) -class RelatedObjectAttrTest(TestCase): +class RelatedObjectAttrTestCase(TestCase): def test_get_context_with_grouped_by(self): region = SimpleNamespace(name='Region 1') @@ -339,7 +342,24 @@ class RelatedObjectAttrTest(TestCase): self.assertTrue(context['linkify']) -class GenericForeignKeyAttrTest(TestCase): +class GenericForeignKeyAttrTestCase(TestCase): + + class TreeNode: + def __init__(self, name, ancestors=()): + self.name = name + self.ancestors = list(ancestors) + self.include_self = None + self._meta = SimpleNamespace(verbose_name='location') + + def __str__(self): + return self.name + + def get_ancestors(self, include_self=False): + self.include_self = include_self + + if include_self: + return [*self.ancestors, self] + return self.ancestors def test_get_context_content_type(self): value = SimpleNamespace(_meta=SimpleNamespace(verbose_name='provider')) @@ -355,8 +375,57 @@ class GenericForeignKeyAttrTest(TestCase): context = attr.get_context(obj, 'assigned_object', value, {}) self.assertTrue(context['linkify']) + def test_get_context_nested_disabled(self): + root = self.TreeNode('Root') + child = self.TreeNode('Child', ancestors=[root]) -class GPSCoordinatesAttrTest(TestCase): + obj = SimpleNamespace() + attr = attrs.GenericForeignKeyAttr('assigned_object') + context = attr.get_context(obj, 'assigned_object', child, {}) + + self.assertIsNone(context['nodes']) + + def test_get_context_nested_non_hierarchical_object(self): + value = SimpleNamespace(_meta=SimpleNamespace(verbose_name='site')) + obj = SimpleNamespace() + attr = attrs.GenericForeignKeyAttr('assigned_object', nested=True) + context = attr.get_context(obj, 'assigned_object', value, {}) + + self.assertIsNone(context['nodes']) + + def test_get_context_nested_hierarchical_object(self): + root = self.TreeNode('Root') + parent = self.TreeNode('Parent', ancestors=[root]) + child = self.TreeNode('Child', ancestors=[root, parent]) + + obj = SimpleNamespace() + attr = attrs.GenericForeignKeyAttr('assigned_object', nested=True) + context = attr.get_context(obj, 'assigned_object', child, {}) + + self.assertEqual(context['nodes'], [root, parent, child]) + self.assertTrue(child.include_self) + + def test_get_context_nested_max_depth(self): + root = self.TreeNode('Root') + parent = self.TreeNode('Parent', ancestors=[root]) + child = self.TreeNode('Child', ancestors=[root, parent]) + + obj = SimpleNamespace() + attr = attrs.GenericForeignKeyAttr('assigned_object', nested=True, max_depth=2) + context = attr.get_context(obj, 'assigned_object', child, {}) + + self.assertEqual(context['nodes'], [parent, child]) + + def test_get_context_nested_null_value(self): + obj = SimpleNamespace() + attr = attrs.GenericForeignKeyAttr('assigned_object', nested=True) + context = attr.get_context(obj, 'assigned_object', None, {}) + + self.assertIsNone(context['content_type']) + self.assertIsNone(context['nodes']) + + +class GPSCoordinatesAttrTestCase(TestCase): def test_missing_latitude_returns_placeholder(self): attr = attrs.GPSCoordinatesAttr() @@ -373,8 +442,80 @@ class GPSCoordinatesAttrTest(TestCase): obj = SimpleNamespace(latitude=None, longitude=None) self.assertEqual(attr.render(obj, {'name': 'coordinates'}), attr.placeholder) + def test_build_coords_url_legacy_prefix(self): + url = build_coords_url('https://maps.google.com/?q=', 48.858, 2.294) + self.assertEqual(url, 'https://maps.google.com/?q=48.858,2.294') -class DateTimeAttrTest(TestCase): + def test_build_coords_url_lat_lon_placeholders(self): + url = build_coords_url( + 'https://www.openstreetmap.org/?mlat={lat}&mlon={lon}#map=16/{lat}/{lon}', + 48.858, + 2.294, + ) + self.assertEqual(url, 'https://www.openstreetmap.org/?mlat=48.858&mlon=2.294#map=16/48.858/2.294') + + def test_build_coords_url_lat_placeholder_only(self): + url = build_coords_url('https://example.com/?lat={lat}', 48.858, 2.294) + self.assertEqual(url, 'https://example.com/?lat=48.858') + + def test_build_coords_url_lon_placeholder_only(self): + url = build_coords_url('https://example.com/?lon={lon}', 48.858, 2.294) + self.assertEqual(url, 'https://example.com/?lon=2.294') + + def test_build_coords_url_unknown_placeholder_falls_back_to_legacy(self): + # URL with only an unknown placeholder (no {lat}/{lon}) → legacy append + url = build_coords_url('https://example.com/?q={unknown}', 48.858, 2.294) + self.assertEqual(url, 'https://example.com/?q={unknown}48.858,2.294') + + def test_build_coords_url_known_and_unknown_placeholder(self): + # {lat} is substituted; unknown key is left as a literal placeholder + url = build_coords_url( + 'https://example.com/?lat={lat}&layer={layer}', 48.858, 2.294 + ) + self.assertEqual(url, 'https://example.com/?lat=48.858&layer={layer}') + + def test_build_coords_url_decimal_values_no_locale_separator(self): + # Decimal field values must format with '.' as the decimal separator regardless of locale; + # a locale-style comma separator would produce e.g. '48,858258' and break the URL + url = build_coords_url( + 'https://maps.google.com/?q=', + Decimal('48.858258'), + Decimal('2.294498'), + ) + self.assertEqual(url, 'https://maps.google.com/?q=48.858258,2.294498') + + def test_build_coords_url_decimal_with_placeholders_no_locale_separator(self): + url = build_coords_url( + 'https://www.openstreetmap.org/?mlat={lat}&mlon={lon}', + Decimal('48.858258'), + Decimal('2.294498'), + ) + self.assertEqual(url, 'https://www.openstreetmap.org/?mlat=48.858258&mlon=2.294498') + + +class AddressAttrTestCase(TestCase): + + def test_plain_prefix_map_url_is_passed_through(self): + attr = attrs.AddressAttr('address', map_url='https://maps.google.com/?q=') + obj = SimpleNamespace(address='1 Main St') + context = attr.get_context(obj, 'address', '1 Main St', {}) + self.assertEqual(context['map_url'], 'https://maps.google.com/?q=') + + def test_gps_format_map_url_is_suppressed_for_addresses(self): + # A GPS-format URL cannot render address links; map_url should be None + attr = attrs.AddressAttr('address', map_url='https://maps.example.com/?mlat={lat}&mlon={lon}') + obj = SimpleNamespace(address='1 Main St') + context = attr.get_context(obj, 'address', '1 Main St', {}) + self.assertIsNone(context['map_url']) + + def test_no_map_url(self): + attr = attrs.AddressAttr('address', map_url=False) + obj = SimpleNamespace(address='1 Main St') + context = attr.get_context(obj, 'address', '1 Main St', {}) + self.assertIsNone(context['map_url']) + + +class DateTimeAttrTestCase(TestCase): def test_default_spec(self): attr = attrs.DateTimeAttr('created') @@ -395,7 +536,255 @@ class DateTimeAttrTest(TestCase): self.assertEqual(context['spec'], 'minutes') -class ObjectsTablePanelTest(TestCase): +class WeightAttrTestCase(SimpleTestCase): + + def _ctx(self, system=''): + return {'name': 'weight', 'preferences': {'ui.measurement_system': system}} + + def _obj(self, weight, unit, abs_g, display=None): + display_fn = (lambda: display) if display else (lambda: unit) + return SimpleNamespace( + weight=weight, + weight_unit=unit, + _abs_weight=abs_g, + get_weight_unit_display=display_fn, + ) + + def test_none_returns_placeholder(self): + attr = attrs.WeightAttr('weight') + obj = SimpleNamespace(weight=None) + self.assertEqual(attr.render(obj, self._ctx()), attr.placeholder) + + def test_inherit_shows_stored_value(self): + attr = attrs.WeightAttr('weight') + obj = self._obj(5, 'kg', 5000, 'Kilograms') + result = attr.render(obj, self._ctx(system='')) + self.assertIn('5', result) + self.assertIn('kg', result) + + def test_metric_converts_lbs_to_kg(self): + # 10 lb = 4535.92 g → 4535.92 / 1000 = 4.54 kg + attr = attrs.WeightAttr('weight') + obj = self._obj(10, 'lb', 4535.92, 'Pounds') + result = attr.render(obj, self._ctx(system='metric')) + self.assertIn('4.54', result) + self.assertIn('kg', result) + + def test_metric_no_conversion_for_metric_unit(self): + attr = attrs.WeightAttr('weight') + obj = self._obj(5, 'kg', 5000, 'Kilograms') + result = attr.render(obj, self._ctx(system='metric')) + self.assertIn('5', result) + self.assertIn('kg', result) + + def test_imperial_converts_kg_to_lbs(self): + # 1 kg = 1000 g → 1000 / 453.592 = 2.2 lbs + attr = attrs.WeightAttr('weight') + obj = self._obj(1, 'kg', 1000, 'Kilograms') + result = attr.render(obj, self._ctx(system='imperial')) + self.assertIn('2.2', result) + self.assertIn('lbs', result) + + def test_imperial_converts_kg_to_singular_lb(self): + # 453.592 g = exactly 1.0 lb → singular 'lb' + attr = attrs.WeightAttr('weight') + obj = self._obj(1, 'kg', 453.592, 'Kilograms') + result = attr.render(obj, self._ctx(system='imperial')) + self.assertIn('1.0', result) + self.assertIn('lb', result) + self.assertNotIn('lbs', result) + + def test_imperial_no_conversion_for_imperial_unit(self): + attr = attrs.WeightAttr('weight') + obj = self._obj(10, 'lb', 4535.92, 'Pounds') + result = attr.render(obj, self._ctx(system='imperial')) + self.assertIn('10', result) + self.assertIn('lbs', result) + + def test_metric_no_conversion_when_abs_weight_is_none(self): + # abs_weight=None → falsy → falls through to stored value + attr = attrs.WeightAttr('weight') + obj = SimpleNamespace(weight=10, weight_unit='lb', _abs_weight=None) + result = attr.render(obj, self._ctx(system='metric')) + self.assertIn('10', result) + self.assertIn('lbs', result) + + +class DistanceAttrTestCase(SimpleTestCase): + + def _ctx(self, system=''): + return {'name': 'distance', 'preferences': {'ui.measurement_system': system}} + + def _obj(self, distance, unit, abs_m, display=None): + display_fn = (lambda: display) if display else (lambda: unit) + return SimpleNamespace( + distance=distance, + distance_unit=unit, + _abs_distance=abs_m, + get_distance_unit_display=display_fn, + ) + + def test_none_returns_placeholder(self): + attr = attrs.DistanceAttr('distance') + obj = SimpleNamespace(distance=None) + self.assertEqual(attr.render(obj, self._ctx()), attr.placeholder) + + def test_inherit_shows_stored_value(self): + attr = attrs.DistanceAttr('distance') + obj = self._obj(10, 'km', 10000, 'Kilometers') + result = attr.render(obj, self._ctx(system='')) + self.assertIn('10', result) + self.assertIn('km', result) + + def test_metric_converts_ft_to_m_under_threshold(self): + # 500 ft = 152.4 m (< 1000) → display in m + attr = attrs.DistanceAttr('distance') + obj = self._obj(500, 'ft', 152.4, 'Feet') + result = attr.render(obj, self._ctx(system='metric')) + self.assertIn('152.4', result) + self.assertNotIn('km', result) + self.assertIn('m', result) + + def test_metric_converts_mi_to_km_over_threshold(self): + # 5 mi = 8046.72 m (>= 1000) → 8046.72 / 1000 = 8.05 km + attr = attrs.DistanceAttr('distance') + obj = self._obj(5, 'mi', 8046.72, 'Miles') + result = attr.render(obj, self._ctx(system='metric')) + self.assertIn('8.05', result) + self.assertIn('km', result) + + def test_imperial_converts_m_to_ft_under_threshold(self): + # 500 m (< 1609.344) → 500 / 0.3048 = 1640.42 ft + attr = attrs.DistanceAttr('distance') + obj = self._obj(500, 'm', 500, 'Meters') + result = attr.render(obj, self._ctx(system='imperial')) + self.assertIn('1640.42', result) + self.assertIn('ft', result) + + def test_imperial_converts_km_to_mi_over_threshold(self): + # 10 km = 10000 m (>= 1609.344) → 10000 / 1609.344 = 6.21 mi + attr = attrs.DistanceAttr('distance') + obj = self._obj(10, 'km', 10000, 'Kilometers') + result = attr.render(obj, self._ctx(system='imperial')) + self.assertIn('6.21', result) + self.assertIn('mi', result) + + def test_metric_no_conversion_for_metric_unit(self): + attr = attrs.DistanceAttr('distance') + obj = self._obj(10, 'km', 10000, 'Kilometers') + result = attr.render(obj, self._ctx(system='metric')) + self.assertIn('10', result) + self.assertIn('km', result) + + def test_imperial_no_conversion_for_imperial_unit(self): + attr = attrs.DistanceAttr('distance') + obj = self._obj(10, 'mi', 16093.44, 'Miles') + result = attr.render(obj, self._ctx(system='imperial')) + self.assertIn('10', result) + self.assertIn('mi', result) + + def test_metric_no_conversion_when_abs_distance_is_none(self): + # abs_distance=None → falls through to stored value + attr = attrs.DistanceAttr('distance') + obj = SimpleNamespace(distance=10, distance_unit='ft', _abs_distance=None) + result = attr.render(obj, self._ctx(system='metric')) + self.assertIn('10', result) + self.assertIn('ft', result) + + +class DisplayWeightTagTestCase(SimpleTestCase): + TEMPLATE = Template('{% load helpers %}{% display_weight weight weight_unit abs_weight %}') + + def _render(self, weight, weight_unit, abs_weight, system=''): + ctx = Context({ + 'preferences': {'ui.measurement_system': system}, + 'weight': weight, + 'weight_unit': weight_unit, + 'abs_weight': abs_weight, + }) + return self.TEMPLATE.render(ctx).strip() + + def test_none_weight_returns_empty(self): + self.assertEqual(self._render(None, 'kg', None), '') + + def test_zero_weight_is_not_suppressed(self): + self.assertEqual(self._render(0, 'kg', 0), '0 kg') + + def test_inherit_stores_kg(self): + self.assertEqual(self._render(5, 'kg', 5000), '5 kg') + + def test_inherit_stores_lb_plural(self): + self.assertEqual(self._render(10, 'lb', 4535.92), '10 lbs') + + def test_inherit_stores_lb_singular(self): + self.assertEqual(self._render(1, 'lb', 453.592), '1 lb') + + def test_metric_converts_lb_to_kg(self): + # 10 lb = 4535.92 g → round(4535.92/1000, 2) = 4.54 kg + result = self._render(10, 'lb', 4535.92, system='metric') + self.assertEqual(result, '4.54 kg') + + def test_imperial_converts_kg_to_lbs(self): + # 1 kg = 1000 g → round(1000/453.592, 2) = 2.2 lbs + result = self._render(1, 'kg', 1000, system='imperial') + self.assertEqual(result, '2.2 lbs') + + def test_imperial_converts_kg_to_singular_lb(self): + # 453.592 g = 1.0 lb → singular + result = self._render(1, 'kg', 453.592, system='imperial') + self.assertEqual(result, '1 lb') + + def test_metric_no_conversion_for_metric_unit(self): + result = self._render(5, 'kg', 5000, system='metric') + self.assertEqual(result, '5 kg') + + def test_imperial_no_conversion_for_imperial_unit(self): + result = self._render(10, 'lb', 4535.92, system='imperial') + self.assertEqual(result, '10 lbs') + + +class DisplayDistanceTagTestCase(SimpleTestCase): + TEMPLATE = Template('{% load helpers %}{% display_distance distance distance_unit abs_distance %}') + + def _render(self, distance, distance_unit, abs_distance, system=''): + ctx = Context({ + 'preferences': {'ui.measurement_system': system}, + 'distance': distance, + 'distance_unit': distance_unit, + 'abs_distance': abs_distance, + }) + return self.TEMPLATE.render(ctx).strip() + + def test_none_distance_returns_empty(self): + self.assertEqual(self._render(None, 'km', None), '') + + def test_inherit_stores_km(self): + self.assertEqual(self._render(10, 'km', 10000), '10 km') + + def test_metric_converts_ft_to_m_under_threshold(self): + # 500 ft = 152.4 m (< 1000) + self.assertEqual(self._render(500, 'ft', 152.4, system='metric'), '152.4 m') + + def test_metric_converts_mi_to_km_over_threshold(self): + # 5 mi = 8046.72 m (>= 1000) → 8.05 km + self.assertEqual(self._render(5, 'mi', 8046.72, system='metric'), '8.05 km') + + def test_imperial_converts_m_to_ft_under_threshold(self): + # 500 m (< 1609.344) → 500/0.3048 = 1640.42 ft + self.assertEqual(self._render(500, 'm', 500, system='imperial'), '1640.42 ft') + + def test_imperial_converts_km_to_mi_over_threshold(self): + # 10 km = 10000 m (>= 1609.344) → 6.21 mi + self.assertEqual(self._render(10, 'km', 10000, system='imperial'), '6.21 mi') + + def test_metric_no_conversion_for_metric_unit(self): + self.assertEqual(self._render(10, 'km', 10000, system='metric'), '10 km') + + def test_imperial_no_conversion_for_imperial_unit(self): + self.assertEqual(self._render(10, 'mi', 16093.44, system='imperial'), '10 mi') + + +class ObjectsTablePanelTestCase(TestCase): """ Verify that ObjectsTablePanel.should_render() hides the panel when the requesting user lacks view permission for the panel's model. diff --git a/netbox/netbox/tests/test_views.py b/netbox/netbox/tests/test_views.py index 06d83fef9..ee9599dc2 100644 --- a/netbox/netbox/tests/test_views.py +++ b/netbox/netbox/tests/test_views.py @@ -62,8 +62,8 @@ class SearchViewTestCase(TestCase): self.assertNotIn('Site Echo', content) self.assertNotIn('Site Foxtrot', content) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_search_no_results(self): + self.add_permissions('dcim.view_site') url = reverse('search') params = { 'q': 'xxxxxxxxx', # Matches nothing diff --git a/netbox/netbox/ui/attrs.py b/netbox/netbox/ui/attrs.py index d5f066ddd..8734fb4a1 100644 --- a/netbox/netbox/ui/attrs.py +++ b/netbox/netbox/ui/attrs.py @@ -3,6 +3,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from netbox.config import get_config +from netbox.ui.utils import build_coords_url, is_coordinate_map_url from utilities.data import resolve_attr_path __all__ = ( @@ -11,6 +12,7 @@ __all__ = ( 'ChoiceAttr', 'ColorAttr', 'DateTimeAttr', + 'DistanceAttr', 'GPSCoordinatesAttr', 'GenericForeignKeyAttr', 'ImageAttr', @@ -23,6 +25,7 @@ __all__ = ( 'TextAttr', 'TimezoneAttr', 'UtilizationAttr', + 'WeightAttr', ) PLACEHOLDER_HTML = '' @@ -274,7 +277,7 @@ class RelatedObjectAttr(ObjectAttribute): Parameters: linkify (bool): If True, the rendered value will be hyperlinked to the related object's detail view grouped_by (str): A second-order object to annotate alongside the related object; for example, an attribute - representing the dcim.Site model might specify grouped_by="region" + representing the dcim.Site model might specify grouped_by="region" colored (bool): If True, render the object as a colored badge when it exposes a `color` attribute """ template_name = 'ui/attrs/object.html' @@ -412,19 +415,48 @@ class GenericForeignKeyAttr(ObjectAttribute): Parameters: linkify (bool): If True, the rendered value will be hyperlinked - to the related object's detail view + to the related object's detail view. + nested (bool): If True and the related object exposes a callable + `get_ancestors(include_self=True)`, render the object together + with its ancestors as a breadcrumb, similar to `NestedObjectAttr`. + Non-hierarchical objects continue to render normally. + max_depth (int): Maximum number of ancestors to display when + `nested` is enabled. Ignored otherwise. """ template_name = 'ui/attrs/generic_object.html' - def __init__(self, *args, linkify=None, **kwargs): + def __init__(self, *args, linkify=None, nested=False, max_depth=None, **kwargs): super().__init__(*args, **kwargs) self.linkify = linkify + self.nested = nested + self.max_depth = max_depth + + def _get_nodes(self, value): + """ + Retrieves a list of nodes representing the hierarchical path to a given value. + """ + if value is None: + return None + + get_ancestors = getattr(value, 'get_ancestors', None) + if not callable(get_ancestors): + return None + + nodes = list(get_ancestors(include_self=True)) + + if self.max_depth is not None: + nodes = nodes[-self.max_depth:] + + return nodes def get_context(self, obj, attr, value, context): content_type = value._meta.verbose_name if value is not None else None + nodes = self._get_nodes(value) if (self.nested and value is not None) else None + return { 'content_type': content_type, 'linkify': self.linkify, + 'nodes': nodes, } @@ -434,7 +466,7 @@ class AddressAttr(MapURLMixin, ObjectAttribute): Parameters: map_url (bool/str): The URL to use when rendering the address. If True, the address will render as a - hyperlink using settings.MAPS_URL. + hyperlink using settings.MAPS_URL. """ template_name = 'ui/attrs/address.html' @@ -443,8 +475,12 @@ class AddressAttr(MapURLMixin, ObjectAttribute): self._map_url = map_url def get_context(self, obj, attr, value, context): + map_url = self.map_url + # A coordinate-format MAPS_URL (containing {lat}/{lon}) cannot be used for address rendering + if map_url and is_coordinate_map_url(map_url): + map_url = None return { - 'map_url': self.map_url, + 'map_url': map_url, } @@ -471,11 +507,14 @@ class GPSCoordinatesAttr(MapURLMixin, ObjectAttribute): longitude = resolve_attr_path(obj, self.longitude_attr) if latitude is None or longitude is None: return self.placeholder + map_url = self.map_url + if map_url: + map_url = build_coords_url(map_url, latitude, longitude) return render_to_string(self.template_name, { 'name': context['name'], 'latitude': latitude, 'longitude': longitude, - 'map_url': self.map_url, + 'map_url': map_url, }) @@ -532,3 +571,110 @@ class UtilizationAttr(ObjectAttribute): Renders the value of an attribute as a utilization graph. """ template_name = 'ui/attrs/utilization.html' + + +IMPERIAL_WEIGHT = {'lb', 'oz'} +METRIC_WEIGHT = {'kg', 'g'} +IMPERIAL_DISTANCE = {'mi', 'ft'} +METRIC_DISTANCE = {'km', 'm'} + + +def compute_weight_display(weight, weight_unit, abs_weight, system): + """ + Return (display_value, display_unit) for a weight, respecting the user's measurement system. + abs_weight is in grams (from WeightMixin._abs_weight). + oz and g pass through unchanged since there is no cross-system equivalent. + """ + if system == 'metric' and weight_unit in IMPERIAL_WEIGHT and abs_weight is not None: + return round(abs_weight / 1000, 2), 'kg' + if system == 'imperial' and weight_unit in METRIC_WEIGHT and abs_weight is not None: + lbs = round(abs_weight / 453.592, 2) + return lbs, 'lb' if lbs == 1 else 'lbs' + if weight_unit == 'lb': + return weight, 'lb' if weight == 1 else 'lbs' + return weight, weight_unit + + +def compute_distance_display(distance, distance_unit, abs_distance, system): + """ + Return (display_value, display_unit) for a distance, respecting the user's measurement system. + abs_distance is in metres (from DistanceMixin._abs_distance). + Distances < 1 km are shown in metres; < 1 mi are shown in feet. + """ + if system == 'metric' and distance_unit in IMPERIAL_DISTANCE and abs_distance is not None: + abs_m = float(abs_distance) + if abs_m >= 1000: + return round(abs_m / 1000, 2), 'km' + return round(abs_m, 2), 'm' + if system == 'imperial' and distance_unit in METRIC_DISTANCE and abs_distance is not None: + abs_m = float(abs_distance) + if abs_m >= 1609.344: + return round(abs_m / 1609.344, 2), 'mi' + return round(abs_m / 0.3048, 2), 'ft' + return distance, distance_unit + + +class WeightAttr(ObjectAttribute): + """ + A weight attribute that converts to the user's preferred measurement system. + + Parameters: + unit_attr (str): Name of the field holding the weight unit (default: 'weight_unit') + abs_attr (str): The internal _abs_weight field name on WeightMixin (stored in grams). + Accessed via Python — not subject to Django's template underscore restriction. + """ + template_name = 'ui/attrs/numeric.html' + + def __init__(self, *args, unit_attr='weight_unit', abs_attr='_abs_weight', **kwargs): + super().__init__(*args, **kwargs) + self.unit_attr = unit_attr + self.abs_attr = abs_attr + + def render(self, obj, context): + weight = resolve_attr_path(obj, self.accessor) + if weight is None: + return self.placeholder + + system = (context.get('preferences') or {}).get('ui.measurement_system') or '' + unit = resolve_attr_path(obj, self.unit_attr) + abs_weight = resolve_attr_path(obj, self.abs_attr) + display_value, display_unit = compute_weight_display(weight, unit, abs_weight, system) + + return render_to_string(self.template_name, { + 'name': context['name'], + 'value': display_value, + 'unit': display_unit, + }) + + +class DistanceAttr(ObjectAttribute): + """ + A distance attribute that converts to the user's preferred measurement system. + + Parameters: + unit_attr (str): Name of the field holding the distance unit (default: 'distance_unit') + abs_attr (str): The internal _abs_distance field name on DistanceMixin (stored in metres). + Accessed via Python — not subject to Django's template underscore restriction. + """ + template_name = 'ui/attrs/numeric.html' + + def __init__(self, *args, unit_attr='distance_unit', abs_attr='_abs_distance', **kwargs): + super().__init__(*args, **kwargs) + self.unit_attr = unit_attr + self.abs_attr = abs_attr + + def render(self, obj, context): + distance = resolve_attr_path(obj, self.accessor) + if distance is None: + return self.placeholder + + system = (context.get('preferences') or {}).get('ui.measurement_system') or '' + unit = resolve_attr_path(obj, self.unit_attr) + abs_distance = resolve_attr_path(obj, self.abs_attr) + display_value, display_unit = compute_distance_display(distance, unit, abs_distance, system) + + return render_to_string(self.template_name, { + 'name': context['name'], + 'value': display_value, + 'unit': display_unit, + }) diff --git a/netbox/netbox/ui/panels.py b/netbox/netbox/ui/panels.py index dadcd285a..06738a2b6 100644 --- a/netbox/netbox/ui/panels.py +++ b/netbox/netbox/ui/panels.py @@ -205,7 +205,11 @@ class ObjectAttributesPanel(ObjectPanel, metaclass=ObjectAttributesPanelMeta): 'attrs': [ { 'label': attr.label or self._name_to_label(name), - 'value': attr.render(ctx['object'], {'name': name, 'perms': ctx['perms']}), + 'value': attr.render(ctx['object'], { + 'name': name, + 'perms': ctx['perms'], + 'preferences': context.get('preferences', {}), + }), } for name, attr in self._attrs.items() if name in attr_names ], } diff --git a/netbox/netbox/ui/utils.py b/netbox/netbox/ui/utils.py new file mode 100644 index 000000000..ade132865 --- /dev/null +++ b/netbox/netbox/ui/utils.py @@ -0,0 +1,23 @@ +__all__ = ( + 'build_coords_url', + 'is_coordinate_map_url', +) + + +def is_coordinate_map_url(url): + """Return True if the URL contains GPS coordinate placeholders ({lat} or {lon}).""" + return '{lat}' in url or '{lon}' in url + + +def build_coords_url(map_url, latitude, longitude): + """ + Build a GPS map URL from a base URL and coordinate values. + + If the URL contains {lat} or {lon} placeholders they are substituted directly; + otherwise the coordinates are appended as a comma-separated pair. + """ + lat_str = str(latitude) + lon_str = str(longitude) + if is_coordinate_map_url(map_url): + return map_url.replace('{lat}', lat_str).replace('{lon}', lon_str) + return f'{map_url}{lat_str},{lon_str}' diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 8f0a98b50..bdd7528d0 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -2,6 +2,7 @@ import logging import re from collections import Counter from copy import deepcopy +from types import SimpleNamespace from django.conf import settings from django.contrib import messages @@ -22,6 +23,7 @@ from core.models import ObjectType from core.signals import clear_events from extras.choices import CustomFieldUIEditableChoices from extras.models import CustomField, ExportTemplate +from netbox.forms.bulk_rename import NetBoxModelBulkRenameForm from netbox.models.features import ChangeLoggingMixin from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename from utilities.error_handlers import handle_protectederror @@ -184,7 +186,11 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): # Render an ExportTemplate if request.GET['export']: - template = get_object_or_404(ExportTemplate, object_types=object_type, name=request.GET['export']) + template = get_object_or_404( + ExportTemplate.objects.restrict(request.user, 'view'), + object_types=object_type, + name=request.GET['export'], + ) return self.export_template(template, request) # Check for YAML export support on the model @@ -870,9 +876,14 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView): An extendable view for renaming objects in bulk. Attributes: - field_name: The name of the object attribute for which the value is being updated (defaults to "name") + field_name: The name of the object attribute to rename (defaults to "name"). Used when + rename_fields is not set; kept for backward compatibility with plugins. + rename_fields: Tuple of field names that can be selected for renaming. When two or more + fields are listed, the form renders a checkbox per field so the user can apply the + find/replace pattern to any combination of them simultaneously. """ field_name = 'name' + rename_fields = () template_name = 'generic/bulk_rename.html' # Match BulkEditView/BulkDeleteView behavior: allow passing a FilterSet # so "Select all N matching query" can expand across the full queryset. @@ -881,19 +892,30 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Create a new Form class from BulkRenameForm - class _Form(BulkRenameForm): - pk = ModelMultipleChoiceField( - queryset=self.queryset, - widget=MultipleHiddenInput() - ) + # Use the changelog-aware form for models that support change logging + base_form = ( + NetBoxModelBulkRenameForm + if issubclass(self.queryset.model, ChangeLoggingMixin) + else BulkRenameForm + ) - self.form = _Form + self.form = type('_Form', (base_form,), { + 'pk': ModelMultipleChoiceField( + queryset=self.queryset, + widget=MultipleHiddenInput(), + ), + }) def get_required_permission(self): return get_permission_for_model(self.queryset.model, 'change') - def _rename_objects(self, form, selected_objects): + def _rename_objects(self, form, selected_objects, field_names=None): + if field_names is None: + field_names = [self.field_name] + + find = form.cleaned_data['find'] + replace = form.cleaned_data['replace'] + use_regex = form.cleaned_data['use_regex'] renamed_pks = [] for obj in selected_objects: @@ -901,22 +923,30 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView): if hasattr(obj, 'snapshot'): obj.snapshot() - find = form.cleaned_data['find'] - replace = form.cleaned_data['replace'] - if form.cleaned_data['use_regex']: - try: - obj.new_name = re.sub(find, replace, getattr(obj, self.field_name, '') or '') - # Catch regex group reference errors - except re.error: - obj.new_name = getattr(obj, self.field_name) - else: - obj.new_name = (getattr(obj, self.field_name, '') or '').replace(find, replace) + new_values = {} + for field in field_names: + current = getattr(obj, field, '') or '' + if use_regex: + try: + new_values[field] = re.sub(find, replace, current) + # Catch regex group reference errors + except re.error: + new_values[field] = current + else: + new_values[field] = current.replace(find, replace) + + obj.new_names = SimpleNamespace(**new_values) + obj.has_changes = any( + new_values[f] != (getattr(obj, f, '') or '') for f in field_names + ) renamed_pks.append(obj.pk) return renamed_pks def post(self, request): logger = logging.getLogger('netbox.views.BulkRenameView') + # Default field list: either all rename_fields or the single legacy field_name + field_names = list(self.rename_fields) if self.rename_fields else [self.field_name] # If we are editing *all* objects in the queryset, replace the PK list with all matched objects. if request.POST.get('_all') and self.filterset is not None: @@ -930,49 +960,63 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView): form = self.form(request.POST, initial={'pk': pk_list}) if form.is_valid(): - try: - with transaction.atomic(using=router.db_for_write(self.queryset.model)): - renamed_pks = self._rename_objects(form, selected_objects) + submitted = [ + f for f in request.POST.getlist('field_names') + if self.rename_fields and f in self.rename_fields + ] + if self.rename_fields and not submitted: + form.add_error(None, _("Select at least one field to rename.")) + elif submitted: + field_names = submitted + if not form.errors: + try: + with transaction.atomic(using=router.db_for_write(self.queryset.model)): + renamed_pks = self._rename_objects(form, selected_objects, field_names) - if '_apply' in request.POST: - # For MPTT models, delay tree updates until all saves are complete - if issubclass(self.queryset.model, MPTTModel): - with self.queryset.model.objects.delay_mptt_updates(): + if '_apply' in request.POST: + # For MPTT models, delay tree updates until all saves are complete + if issubclass(self.queryset.model, MPTTModel): + with self.queryset.model.objects.delay_mptt_updates(): + for obj in selected_objects: + for field in field_names: + setattr(obj, field, getattr(obj.new_names, field)) + obj._changelog_message = form.cleaned_data.get('changelog_message', '') + obj.save() + else: for obj in selected_objects: - setattr(obj, self.field_name, obj.new_name) + for field in field_names: + setattr(obj, field, getattr(obj.new_names, field)) + obj._changelog_message = form.cleaned_data.get('changelog_message', '') obj.save() - else: - for obj in selected_objects: - setattr(obj, self.field_name, obj.new_name) - obj.save() - # Enforce constrained permissions - if self.queryset.filter(pk__in=renamed_pks).count() != len(selected_objects): - raise PermissionsViolation + # Enforce constrained permissions + if self.queryset.filter(pk__in=renamed_pks).count() != len(selected_objects): + raise PermissionsViolation - messages.success( - request, - _("Renamed {count} {object_type}").format( - count=len(selected_objects), - object_type=self.queryset.model._meta.verbose_name_plural + messages.success( + request, + _("Renamed {count} {object_type}").format( + count=len(selected_objects), + object_type=self.queryset.model._meta.verbose_name_plural + ) ) - ) - return redirect(self.get_return_url(request)) + return redirect(self.get_return_url(request)) - except IntegrityError as e: - messages.error(self.request, ", ".join(e.args)) - clear_events.send(sender=self) + except IntegrityError as e: + messages.error(self.request, ", ".join(e.args)) + clear_events.send(sender=self) - except (AbortRequest, PermissionsViolation) as e: - logger.debug(e.message) - form.add_error(None, e.message) - clear_events.send(sender=self) + except (AbortRequest, PermissionsViolation) as e: + logger.debug(e.message) + form.add_error(None, e.message) + clear_events.send(sender=self) else: form = self.form(initial={'pk': pk_list}) return render(request, self.template_name, { - 'field_name': self.field_name, + 'rename_fields': self.rename_fields, + 'selected_field_names': field_names, 'form': form, 'obj_type_plural': self.queryset.model._meta.verbose_name_plural, 'selected_objects': selected_objects, diff --git a/netbox/project-static/dist/graphiql/js.cookie.min.js b/netbox/project-static/dist/graphiql/js.cookie.min.js index 962d48d0e..a5d37e23e 100644 --- a/netbox/project-static/dist/graphiql/js.cookie.min.js +++ b/netbox/project-static/dist/graphiql/js.cookie.min.js @@ -1,2 +1,2 @@ -/*! js-cookie v3.0.5 | MIT */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,(function(){"use strict";function e(e){for(var t=1;tcode{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:.5rem;padding-bottom:.5rem;color:var(--tblr-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}thead,tbody,tfoot,tr,td,th{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}input,button,select,optgroup,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}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button:not(:disabled),[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]: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-fields-wrapper,::-webkit-datetime-edit-text,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button{cursor:pointer;filter:grayscale(1)}::-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,.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:"\2014\a0"}.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-xxl,.container-xl,.container-lg,.container-md,.container-sm{--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-sm,.container{max-width:540px}}@media(min-width:768px){.container-md,.container-sm,.container{max-width:720px}}@media(min-width:992px){.container-lg,.container-md,.container-sm,.container{max-width:960px}}@media(min-width:1200px){.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1140px}}@media(min-width:1400px){.container-xxl,.container-xl,.container-lg,.container-md,.container-sm,.container{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: .25rem}.g-1,.gy-1{--tblr-gutter-y: .25rem}.g-2,.gx-2{--tblr-gutter-x: .5rem}.g-2,.gy-2{--tblr-gutter-y: .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: .25rem}.g-sm-1,.gy-sm-1{--tblr-gutter-y: .25rem}.g-sm-2,.gx-sm-2{--tblr-gutter-x: .5rem}.g-sm-2,.gy-sm-2{--tblr-gutter-y: .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: .25rem}.g-md-1,.gy-md-1{--tblr-gutter-y: .25rem}.g-md-2,.gx-md-2{--tblr-gutter-x: .5rem}.g-md-2,.gy-md-2{--tblr-gutter-y: .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: .25rem}.g-lg-1,.gy-lg-1{--tblr-gutter-y: .25rem}.g-lg-2,.gx-lg-2{--tblr-gutter-x: .5rem}.g-lg-2,.gy-lg-2{--tblr-gutter-y: .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: .25rem}.g-xl-1,.gy-xl-1{--tblr-gutter-y: .25rem}.g-xl-2,.gx-xl-2{--tblr-gutter-x: .5rem}.g-xl-2,.gy-xl-2{--tblr-gutter-y: .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: .25rem}.g-xxl-1,.gy-xxl-1{--tblr-gutter-y: .25rem}.g-xxl-2,.gx-xxl-2{--tblr-gutter-x: .5rem}.g-xxl-2,.gy-xxl-2{--tblr-gutter-y: .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}}.table,.markdown>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), .075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--tblr-table-border-color)}.table>:not(caption)>*>*,.markdown>table>:not(caption)>*>*{padding:.5rem;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)))}.table>tbody,.markdown>table>tbody{vertical-align:inherit}.table>thead,.markdown>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}.table-sm>:not(caption)>*>*,.markdown>table>:not(caption)>*>*{padding:.25rem}.table-bordered>:not(caption)>*,.markdown>table>:not(caption)>*{border-width:var(--tblr-border-width) 0}.table-bordered>:not(caption)>*>*,.markdown>table>: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(204, 230.6, 229);--tblr-table-border-color: rgb(169.4, 192.68, 194.2);--tblr-table-striped-bg: rgb(195.35, 221.12, 220.3);--tblr-table-striped-color: #1f2937;--tblr-table-active-bg: rgb(186.7, 211.64, 211.6);--tblr-table-active-color: #1f2937;--tblr-table-hover-bg: rgb(191.025, 216.38, 215.95);--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);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:#80c2be;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::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-sm,.form-control-plaintext.form-control-lg{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);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:#80c2be;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;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);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:#80c2be;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}.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%28127.5, 194, 190%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;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;appearance:none;background-color:var(--tblr-primary);border:2px var(--tblr-border-style) #ffffff;border-radius:1rem;box-shadow:0 .1rem .25rem #0000001a;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{transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b3dad8}.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;appearance:none;background-color:var(--tblr-primary);border:2px var(--tblr-border-style) #ffffff;border-radius:1rem;box-shadow:0 .1rem .25rem #0000001a;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{transition:none}}.form-range::-moz-range-thumb:active{background-color:#b3dad8}.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;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}.form-floating>.form-control::placeholder,.form-floating>.form-control-plaintext::placeholder{color:transparent}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown),.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill,.form-floating>.form-control-plaintext:-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:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-control-plaintext~label,.form-floating>.form-select~label{transform:scale(.85) translateY(-.5rem) translate(.15rem)}.form-floating>.form-control:-webkit-autofill~label{transform:scale(.85) translateY(-.5rem) translate(.15rem)}.form-floating>textarea:focus~label:after,.form-floating>textarea:not(:placeholder-shown)~label:after{position:absolute;inset:1rem .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>:disabled~label,.form-floating>.form-control: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-select,.input-group>.form-floating{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus,.input-group>.form-floating:focus-within{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>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text,.input-group-lg>.btn{padding:.6875rem 1.5rem;font-size:1rem;border-radius:var(--tblr-border-radius-lg)}.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text,.input-group-sm>.btn{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)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),.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{border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),.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{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)}.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip,.is-valid~.valid-feedback,.is-valid~.valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-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}.was-validated .form-control:valid:focus,.form-control.is-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}.was-validated .form-select:valid,.form-select.is-valid{border-color:var(--tblr-form-valid-border-color)}.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"],.form-select.is-valid:not([multiple]):not([size]),.form-select.is-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}.was-validated .form-select:valid:focus,.form-select.is-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 .form-control-color:valid,.form-control-color.is-valid{width:5.375rem}.was-validated .form-check-input:valid,.form-check-input.is-valid{border-color:var(--tblr-form-valid-border-color)}.was-validated .form-check-input:valid:checked,.form-check-input.is-valid:checked{background-color:var(--tblr-form-valid-color)}.was-validated .form-check-input:valid:focus,.form-check-input.is-valid:focus{box-shadow:0 0 0 .25rem rgba(var(--tblr-success-rgb),.25)}.was-validated .form-check-input:valid~.form-check-label,.form-check-input.is-valid~.form-check-label{color:var(--tblr-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):valid,.input-group>.form-control:not(:focus).is-valid,.was-validated .input-group>.form-select:not(:focus):valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.input-group>.form-floating:not(:focus-within).is-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)}.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip,.is-invalid~.invalid-feedback,select.tomselected.is-invalid+div.ts-wrapper~.invalid-feedback,.is-invalid~.invalid-tooltip,select.tomselected.is-invalid+div.ts-wrapper~.invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid,select.tomselected.is-invalid+div.form-control.ts-wrapper{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}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus,select.tomselected.is-invalid+div.form-control.ts-wrapper: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}.was-validated .form-select:invalid,.form-select.is-invalid,select.tomselected.is-invalid+div.form-select.ts-wrapper{border-color:var(--tblr-form-invalid-border-color)}.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"],.form-select.is-invalid:not([multiple]):not([size]),select.tomselected.is-invalid+div.form-select.ts-wrapper:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],select.tomselected.is-invalid+div.form-select.ts-wrapper: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}.was-validated .form-select:invalid:focus,.form-select.is-invalid:focus,select.tomselected.is-invalid+div.form-select.ts-wrapper: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 .form-control-color:invalid,.form-control-color.is-invalid,select.tomselected.is-invalid+div.form-control-color.ts-wrapper{width:5.375rem}.was-validated .form-check-input:invalid,.form-check-input.is-invalid,select.tomselected.is-invalid+div.form-check-input.ts-wrapper{border-color:var(--tblr-form-invalid-border-color)}.was-validated .form-check-input:invalid:checked,.form-check-input.is-invalid:checked,select.tomselected.is-invalid+div.form-check-input.ts-wrapper:checked{background-color:var(--tblr-form-invalid-color)}.was-validated .form-check-input:invalid:focus,.form-check-input.is-invalid:focus,select.tomselected.is-invalid+div.form-check-input.ts-wrapper:focus{box-shadow:0 0 0 .25rem rgba(var(--tblr-danger-rgb),.25)}.was-validated .form-check-input:invalid~.form-check-label,.form-check-input.is-invalid~.form-check-label,select.tomselected.is-invalid+div.form-check-input.ts-wrapper~.form-check-label{color:var(--tblr-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):invalid,.input-group>.form-control:not(:focus).is-invalid,.input-group>select.tomselected.is-invalid+div.form-control.ts-wrapper:not(:focus),.was-validated .input-group>.form-select:not(:focus):invalid,.input-group>.form-select:not(:focus).is-invalid,.input-group>select.tomselected.is-invalid+div.form-select.ts-wrapper:not(:focus),.was-validated .input-group>.form-floating:not(:focus-within):invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>select.tomselected.is-invalid+div.form-floating.ts-wrapper:not(:focus-within){z-index:4}.btn{--tblr-btn-padding-x: .5rem;--tblr-btn-padding-y: .25rem;--tblr-btn-font-family: var(--tblr-body-font-family);--tblr-btn-font-size: .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: .4;--tblr-btn-focus-box-shadow: 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;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,:not(.btn-check)+.btn:active,.btn:first-child:active,.btn.active,.btn.show{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,:not(.btn-check)+.btn:active:focus-visible,.btn:first-child:active:focus-visible,.btn.active:focus-visible,.btn.show: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: 37, 151, 144;text-decoration:none}.btn-link:hover,.btn-link:focus-visible{text-decoration:underline}.btn-link:focus-visible{color:var(--tblr-btn-color)}.btn-link:hover{color:var(--tblr-btn-hover-color)}.btn-lg,.btn-group-lg>.btn{--tblr-btn-padding-y: .6875rem;--tblr-btn-padding-x: 1.5rem;--tblr-btn-font-size: 1rem;--tblr-btn-border-radius: var(--tblr-border-radius-lg)}.btn-sm,.btn-group-sm>.btn{--tblr-btn-padding-y: .3125rem;--tblr-btn-padding-x: .5rem;--tblr-btn-font-size: .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}}.dropup,.dropend,.dropdown,.dropstart,.dropup-center,.dropdown-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: .25rem;--tblr-dropdown-spacer: 1px;--tblr-dropdown-font-size: .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), .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: .75rem;--tblr-dropdown-item-padding-y: .5rem;--tblr-dropdown-header-color: #4b5563;--tblr-dropdown-header-padding-x: .75rem;--tblr-dropdown-header-padding-y: .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:hover,.dropdown-item:focus{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, .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>.btn,.btn-group-vertical>.btn{position:relative;flex:1 1 auto}.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{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>:not(.btn-check:first-child)+.btn,.btn-group>.btn-group:not(:first-child){margin-left:calc(-1 * var(--tblr-border-width))}.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn,.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.dropdown-toggle-split:after,.dropup .dropdown-toggle-split:after,.dropend .dropdown-toggle-split:after{margin-left:0}.dropstart .dropdown-toggle-split:before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:1.125rem;padding-left:1.125rem}.btn-group.show .dropdown-toggle{box-shadow:inset 0 3px 5px #00000020}.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:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:calc(-1 * var(--tblr-border-width))}.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:nth-child(n+3),.btn-group-vertical>:not(.btn-check)+.btn,.btn-group-vertical>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--tblr-nav-link-padding-x: .75rem;--tblr-nav-link-padding-y: .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:none;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}.nav-link:hover,.nav-link:focus{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:hover,.nav-tabs .nav-link:focus{isolation:isolate;border-color:var(--tblr-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{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: rgba(var(--tblr-secondary-rgb), .15)}.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: .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:hover,.nav-underline .nav-link:focus{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-link,.nav-fill .nav-item{flex:1 1 auto;text-align:center}.nav-justified>.nav-link,.nav-justified .nav-item{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: .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: .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: .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), .15);--tblr-navbar-toggler-border-radius: var(--tblr-border-radius);--tblr-navbar-toggler-focus-width: 0;--tblr-navbar-toggler-transition: box-shadow .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-sm,.navbar>.container-md,.navbar>.container-lg,.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:hover,.navbar-brand:focus{color:var(--tblr-navbar-brand-hover-color);text-decoration:none}.navbar-nav{--tblr-nav-link-padding-x: 0;--tblr-nav-link-padding-y: .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:hover,.navbar-text a:focus{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, .7);--tblr-navbar-hover-color: rgba(255, 255, 255, .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, .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;inset:0;padding:var(--tblr-card-img-overlay-padding);border-radius:var(--tblr-card-inner-border-radius)}.card-img,.card-img-top,.card-img-bottom{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-img-top,.card-group>.card:not(:last-child)>.card-header{border-top-right-radius:0}.card-group>.card:not(:last-child)>.card-img-bottom,.card-group>.card:not(:last-child)>.card-footer{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-img-top,.card-group>.card:not(:first-child)>.card-header{border-top-left-radius:0}.card-group>.card:not(:first-child)>.card-img-bottom,.card-group>.card:not(:first-child)>.card-footer{border-bottom-left-radius:0}}.pagination{--tblr-pagination-padding-x: .25rem;--tblr-pagination-padding-y: calc(.25rem + 1px) ;--tblr-pagination-font-size: .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 .25rem rgba(var(--tblr-primary-rgb), .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)}.page-link.active,.active>.page-link{z-index:3;color:var(--tblr-pagination-active-color);background-color:var(--tblr-pagination-active-bg);border-color:var(--tblr-pagination-active-border-color)}.page-link.disabled,.disabled>.page-link{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:-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: .75rem;--tblr-pagination-font-size: 1.09375rem;--tblr-pagination-border-radius: var(--tblr-border-radius-lg)}.pagination-sm{--tblr-pagination-padding-x: .5rem;--tblr-pagination-padding-y: .25rem;--tblr-pagination-font-size: .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: .5rem;--tblr-progress-font-size: .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 .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), .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):hover,.list-group-item-action:not(.active):focus{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: .75rem;--tblr-toast-padding-y: .5rem;--tblr-toast-spacing: calc(var(--tblr-page-padding) * 2);--tblr-toast-max-width: 350px;--tblr-toast-font-size: .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), .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: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: .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: .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:translateY(-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: .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-header,.modal-fullscreen .modal-footer{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-header,.modal-fullscreen-sm-down .modal-footer{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-header,.modal-fullscreen-md-down .modal-footer{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-header,.modal-fullscreen-lg-down .modal-footer{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-header,.modal-fullscreen-xl-down .modal-footer{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-header,.modal-fullscreen-xxl-down .modal-footer{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: .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: .9;--tblr-tooltip-arrow-width: .8rem;--tblr-tooltip-arrow-height: .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-top .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow{bottom:calc(-1 * var(--tblr-tooltip-arrow-height))}.bs-tooltip-top .tooltip-arrow:before,.bs-tooltip-auto[data-popper-placement^=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-end .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow{left:calc(-1 * var(--tblr-tooltip-arrow-height));width:var(--tblr-tooltip-arrow-height);height:var(--tblr-tooltip-arrow-width)}.bs-tooltip-end .tooltip-arrow:before,.bs-tooltip-auto[data-popper-placement^=right] .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-bottom .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow{top:calc(-1 * var(--tblr-tooltip-arrow-height))}.bs-tooltip-bottom .tooltip-arrow:before,.bs-tooltip-auto[data-popper-placement^=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-start .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow{right:calc(-1 * var(--tblr-tooltip-arrow-height));width:var(--tblr-tooltip-arrow-height);height:var(--tblr-tooltip-arrow-width)}.bs-tooltip-start .tooltip-arrow:before,.bs-tooltip-auto[data-popper-placement^=left] .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: .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: .5rem;--tblr-popover-header-font-size: .875rem;--tblr-popover-header-color: inherit;--tblr-popover-header-bg: transparent;--tblr-popover-body-padding-x: .5rem;--tblr-popover-body-padding-y: .5rem;--tblr-popover-body-color: inherit;--tblr-popover-arrow-width: 1rem;--tblr-popover-arrow-height: .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:before,.popover .popover-arrow:after{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-top>.popover-arrow,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow{bottom:calc(-1 * (var(--tblr-popover-arrow-height)) - var(--tblr-popover-border-width))}.bs-popover-top>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:before,.bs-popover-top>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:after{border-width:var(--tblr-popover-arrow-height) calc(var(--tblr-popover-arrow-width) * .5) 0}.bs-popover-top>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:before{bottom:0;border-top-color:var(--tblr-popover-arrow-border)}.bs-popover-top>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:after{bottom:var(--tblr-popover-border-width);border-top-color:var(--tblr-popover-bg)}.bs-popover-end>.popover-arrow,.bs-popover-auto[data-popper-placement^=right]>.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-end>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:before,.bs-popover-end>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:after{border-width:calc(var(--tblr-popover-arrow-width) * .5) var(--tblr-popover-arrow-height) calc(var(--tblr-popover-arrow-width) * .5) 0}.bs-popover-end>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:before{left:0;border-right-color:var(--tblr-popover-arrow-border)}.bs-popover-end>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:after{left:var(--tblr-popover-border-width);border-right-color:var(--tblr-popover-bg)}.bs-popover-bottom>.popover-arrow,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow{top:calc(-1 * (var(--tblr-popover-arrow-height)) - var(--tblr-popover-border-width))}.bs-popover-bottom>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:before,.bs-popover-bottom>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:after{border-width:0 calc(var(--tblr-popover-arrow-width) * .5) var(--tblr-popover-arrow-height)}.bs-popover-bottom>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:before{top:0;border-bottom-color:var(--tblr-popover-arrow-border)}.bs-popover-bottom>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:after{top:var(--tblr-popover-border-width);border-bottom-color:var(--tblr-popover-bg)}.bs-popover-bottom .popover-header:before,.bs-popover-auto[data-popper-placement^=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-start>.popover-arrow,.bs-popover-auto[data-popper-placement^=left]>.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-start>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:before,.bs-popover-start>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:after{border-width:calc(var(--tblr-popover-arrow-width) * .5) 0 calc(var(--tblr-popover-arrow-width) * .5) var(--tblr-popover-arrow-height)}.bs-popover-start>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:before{right:0;border-left-color:var(--tblr-popover-arrow-border)}.bs-popover-start>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=left]>.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.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-start),.active.carousel-item-end{transform:translate(100%)}.carousel-item-prev:not(.carousel-item-end),.active.carousel-item-start{transform:translate(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end{z-index:1;opacity:1}.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{z-index:0;opacity:0;transition:opacity 0s .6s}@media(prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{transition:none}}.carousel-control-prev,.carousel-control-next{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:none;filter:var(--tblr-carousel-control-icon-filter);border:0;opacity:.5;transition:opacity .15s ease}@media(prefers-reduced-motion:reduce){.carousel-control-prev,.carousel-control-next{transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-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-grow,.spinner-border{display:inline-block;flex-shrink:0;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: -.125em;--tblr-spinner-border-width: 2px;--tblr-spinner-animation-speed: .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: -.125em;--tblr-spinner-animation-speed: .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-xxl,.offcanvas-xl,.offcanvas-lg,.offcanvas-md,.offcanvas-sm{--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 .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:translate(-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:translate(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.showing,.offcanvas-sm.show:not(.hiding){transform:none}.offcanvas-sm.showing,.offcanvas-sm.hiding,.offcanvas-sm.show{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:translate(-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:translate(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.showing,.offcanvas-md.show:not(.hiding){transform:none}.offcanvas-md.showing,.offcanvas-md.hiding,.offcanvas-md.show{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:translate(-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:translate(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.showing,.offcanvas-lg.show:not(.hiding){transform:none}.offcanvas-lg.showing,.offcanvas-lg.hiding,.offcanvas-lg.show{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:translate(-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:translate(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.showing,.offcanvas-xl.show:not(.hiding){transform:none}.offcanvas-xl.showing,.offcanvas-xl.hiding,.offcanvas-xl.show{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:translate(-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:translate(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.showing,.offcanvas-xxl.show:not(.hiding){transform:none}.offcanvas-xxl.showing,.offcanvas-xxl.hiding,.offcanvas-xxl.show{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:translate(-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:translate(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.showing,.offcanvas.show:not(.hiding){transform:none}.offcanvas.showing,.offcanvas.hiding,.offcanvas.show{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{mask-image:linear-gradient(130deg,#000 55%,#000000e6,#000 95%);mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{to{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{object-fit:contain!important}.object-fit-cover{object-fit:cover!important}.object-fit-fill{object-fit:fill!important}.object-fit-scale{object-fit:scale-down!important}.object-fit-none{object-fit:none!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:translate(-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: .1}.border-opacity-25{--tblr-border-opacity: .25}.border-opacity-50{--tblr-border-opacity: .5}.border-opacity-75{--tblr-border-opacity: .75}.border-opacity-100{--tblr-border-opacity: 1}.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}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!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}.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{column-gap:0!important}.column-gap-1{column-gap:.25rem!important}.column-gap-2{column-gap:.5rem!important}.column-gap-3{column-gap:1rem!important}.column-gap-4{column-gap:1.5rem!important}.column-gap-5{column-gap:2rem!important}.column-gap-6{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,.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:#00000080!important}.text-white-50{--tblr-text-opacity: 1;color:#ffffff80!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: .25}.text-opacity-50{--tblr-text-opacity: .5}.text-opacity-75{--tblr-text-opacity: .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,.link-opacity-10-hover:hover{--tblr-link-opacity: .1}.link-opacity-25,.link-opacity-25-hover:hover{--tblr-link-opacity: .25}.link-opacity-50,.link-opacity-50-hover:hover{--tblr-link-opacity: .5}.link-opacity-75,.link-opacity-75-hover:hover{--tblr-link-opacity: .75}.link-opacity-100,.link-opacity-100-hover:hover{--tblr-link-opacity: 1}.link-offset-1,.link-offset-1-hover:hover{text-underline-offset:.125em!important}.link-offset-2,.link-offset-2-hover:hover{text-underline-offset:.25em!important}.link-offset-3,.link-offset-3-hover:hover{text-underline-offset:.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,.link-underline-opacity-0-hover:hover{--tblr-link-underline-opacity: 0}.link-underline-opacity-10,.link-underline-opacity-10-hover:hover{--tblr-link-underline-opacity: .1}.link-underline-opacity-25,.link-underline-opacity-25-hover:hover{--tblr-link-underline-opacity: .25}.link-underline-opacity-50,.link-underline-opacity-50-hover:hover{--tblr-link-underline-opacity: .5}.link-underline-opacity-75,.link-underline-opacity-75-hover:hover{--tblr-link-underline-opacity: .75}.link-underline-opacity-100,.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: .1}.bg-opacity-25{--tblr-bg-opacity: .25}.bg-opacity-50{--tblr-bg-opacity: .5}.bg-opacity-75{--tblr-bg-opacity: .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{user-select:all!important}.user-select-auto{user-select:auto!important}.user-select-none{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{object-fit:contain!important}.object-cover{object-fit:cover!important}.object-fill{object-fit:fill!important}.object-scale-down{object-fit:scale-down!important}.object-none{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{columns:2!important}.columns-3{columns:3!important}.columns-4{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{object-fit:contain!important}.object-fit-sm-cover{object-fit:cover!important}.object-fit-sm-fill{object-fit:fill!important}.object-fit-sm-scale{object-fit:scale-down!important}.object-fit-sm-none{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{column-gap:0!important}.column-gap-sm-1{column-gap:.25rem!important}.column-gap-sm-2{column-gap:.5rem!important}.column-gap-sm-3{column-gap:1rem!important}.column-gap-sm-4{column-gap:1.5rem!important}.column-gap-sm-5{column-gap:2rem!important}.column-gap-sm-6{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{columns:2!important}.columns-sm-3{columns:3!important}.columns-sm-4{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{object-fit:contain!important}.object-fit-md-cover{object-fit:cover!important}.object-fit-md-fill{object-fit:fill!important}.object-fit-md-scale{object-fit:scale-down!important}.object-fit-md-none{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{column-gap:0!important}.column-gap-md-1{column-gap:.25rem!important}.column-gap-md-2{column-gap:.5rem!important}.column-gap-md-3{column-gap:1rem!important}.column-gap-md-4{column-gap:1.5rem!important}.column-gap-md-5{column-gap:2rem!important}.column-gap-md-6{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{columns:2!important}.columns-md-3{columns:3!important}.columns-md-4{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{object-fit:contain!important}.object-fit-lg-cover{object-fit:cover!important}.object-fit-lg-fill{object-fit:fill!important}.object-fit-lg-scale{object-fit:scale-down!important}.object-fit-lg-none{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{column-gap:0!important}.column-gap-lg-1{column-gap:.25rem!important}.column-gap-lg-2{column-gap:.5rem!important}.column-gap-lg-3{column-gap:1rem!important}.column-gap-lg-4{column-gap:1.5rem!important}.column-gap-lg-5{column-gap:2rem!important}.column-gap-lg-6{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{columns:2!important}.columns-lg-3{columns:3!important}.columns-lg-4{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{object-fit:contain!important}.object-fit-xl-cover{object-fit:cover!important}.object-fit-xl-fill{object-fit:fill!important}.object-fit-xl-scale{object-fit:scale-down!important}.object-fit-xl-none{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{column-gap:0!important}.column-gap-xl-1{column-gap:.25rem!important}.column-gap-xl-2{column-gap:.5rem!important}.column-gap-xl-3{column-gap:1rem!important}.column-gap-xl-4{column-gap:1.5rem!important}.column-gap-xl-5{column-gap:2rem!important}.column-gap-xl-6{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{columns:2!important}.columns-xl-3{columns:3!important}.columns-xl-4{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{object-fit:contain!important}.object-fit-xxl-cover{object-fit:cover!important}.object-fit-xxl-fill{object-fit:fill!important}.object-fit-xxl-scale{object-fit:scale-down!important}.object-fit-xxl-none{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{column-gap:0!important}.column-gap-xxl-1{column-gap:.25rem!important}.column-gap-xxl-2{column-gap:.5rem!important}.column-gap-xxl-3{column-gap:1rem!important}.column-gap-xxl-4{column-gap:1.5rem!important}.column-gap-xxl-5{column-gap:2rem!important}.column-gap-xxl-6{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{columns:2!important}.columns-xxl-3{columns:3!important}.columns-xxl-4{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}}:root,:host{--tblr-font-monospace: Roboto Mono;--tblr-font-sans-serif: Inter, system-ui, 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-brand: #00857D;--tblr-primary: #00857D;--tblr-primary-rgb: 0, 133, 125;--tblr-primary-fg: var(--tblr-light);--tblr-primary-darken: rgb(0, 119.7, 112.5);--tblr-primary-darken: color-mix(in oklab, var(--tblr-primary), transparent 20%);--tblr-primary-lt: rgb(229.5, 242.8, 242);--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, 243, 242;--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: .25rem;--tblr-spacer-2: .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: .875rem;--tblr-font-size-h5: .75rem;--tblr-font-size-h6: .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), .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), .06);--tblr-shadow-card: 0 0 4px rgba(var(--tblr-body-color-rgb), .04);--tblr-shadow-card-hover: rgba(var(--tblr-body-color-rgb), .16) 0 2px 16px 0;--tblr-shadow-dropdown: 0 16px 24px 2px rgba(0, 0, 0, .07), 0 6px 30px 5px rgba(0, 0, 0, .06), 0 8px 10px -5px rgba(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))}:root,:host{font-size:16px;height:100%}@media(min-width:992px){:root,:host{margin-left:calc(100vw - 100%);margin-right:0}}:root,:host,[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, .1);--tblr-border-dark-color: #9ca3af;--tblr-border-dark-color-translucent: rgba(4, 32, 69, .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), .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, .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){:root,:host,[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:scaleZ(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:scaleZ(1)}}@keyframes rotate-360{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes blink{0%{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:transparent}}*{scrollbar-color:color-mix(in srgb,var(--tblr-scrollbar-color, var(--tblr-body-color)) 20%,transparent) transparent}*::-webkit-scrollbar{width:1rem;height:1rem;transition:background .3s}@media(prefers-reduced-motion:reduce){*::-webkit-scrollbar{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:transparent}*: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:transparent}.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: #00857D;--tblr-navbar-active-bg: rgba(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:transparent;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-right,.navbar-expand-sm.navbar-vertical.navbar-end{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:18rem}.navbar-expand-sm.navbar-vertical.navbar-right~.navbar,.navbar-expand-sm.navbar-vertical.navbar-right~.page-wrapper,.navbar-expand-sm.navbar-vertical.navbar-end~.navbar,.navbar-expand-sm.navbar-vertical.navbar-end~.page-wrapper{margin-left:0;margin-right:18rem}}@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:transparent;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-right,.navbar-expand-md.navbar-vertical.navbar-end{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:18rem}.navbar-expand-md.navbar-vertical.navbar-right~.navbar,.navbar-expand-md.navbar-vertical.navbar-right~.page-wrapper,.navbar-expand-md.navbar-vertical.navbar-end~.navbar,.navbar-expand-md.navbar-vertical.navbar-end~.page-wrapper{margin-left:0;margin-right:18rem}}@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:transparent;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-right,.navbar-expand-lg.navbar-vertical.navbar-end{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:18rem}.navbar-expand-lg.navbar-vertical.navbar-right~.navbar,.navbar-expand-lg.navbar-vertical.navbar-right~.page-wrapper,.navbar-expand-lg.navbar-vertical.navbar-end~.navbar,.navbar-expand-lg.navbar-vertical.navbar-end~.page-wrapper{margin-left:0;margin-right:18rem}}@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:transparent;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-right,.navbar-expand-xl.navbar-vertical.navbar-end{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:18rem}.navbar-expand-xl.navbar-vertical.navbar-right~.navbar,.navbar-expand-xl.navbar-vertical.navbar-right~.page-wrapper,.navbar-expand-xl.navbar-vertical.navbar-end~.navbar,.navbar-expand-xl.navbar-vertical.navbar-end~.page-wrapper{margin-left:0;margin-right:18rem}}@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:transparent;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-right,.navbar-expand-xxl.navbar-vertical.navbar-end{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:18rem}.navbar-expand-xxl.navbar-vertical.navbar-right~.navbar,.navbar-expand-xxl.navbar-vertical.navbar-right~.page-wrapper,.navbar-expand-xxl.navbar-vertical.navbar-end~.navbar,.navbar-expand-xxl.navbar-vertical.navbar-end~.page-wrapper{margin-left:0;margin-right:18rem}}.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:transparent;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-right,.navbar-expand.navbar-vertical.navbar-end{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:18rem}.navbar-expand.navbar-vertical.navbar-right~.navbar,.navbar-expand.navbar-vertical.navbar-right~.page-wrapper,.navbar-expand.navbar-vertical.navbar-end~.navbar,.navbar-expand.navbar-vertical.navbar-end~.page-wrapper{margin-left:0;margin-right:18rem}.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}@media(prefers-reduced-motion:reduce){.navbar-toggler-icon{transition:none}}.navbar-toggler-icon{position:relative}.navbar-toggler-icon:before,.navbar-toggler-icon:after{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:before,.navbar-toggler-icon:after{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:transparent!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:18rem;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-right,.navbar-vertical.navbar-expand-sm.navbar-end{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:18rem}.navbar-vertical.navbar-expand-sm~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand-sm.navbar-right~.page,.navbar-vertical.navbar-expand-sm.navbar-end~.page{padding-left:0;padding-right:18rem}.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:transparent;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:18rem;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-right,.navbar-vertical.navbar-expand-md.navbar-end{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:18rem}.navbar-vertical.navbar-expand-md~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand-md.navbar-right~.page,.navbar-vertical.navbar-expand-md.navbar-end~.page{padding-left:0;padding-right:18rem}.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:transparent;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:18rem;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-right,.navbar-vertical.navbar-expand-lg.navbar-end{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:18rem}.navbar-vertical.navbar-expand-lg~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand-lg.navbar-right~.page,.navbar-vertical.navbar-expand-lg.navbar-end~.page{padding-left:0;padding-right:18rem}.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:transparent;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:18rem;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-right,.navbar-vertical.navbar-expand-xl.navbar-end{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:18rem}.navbar-vertical.navbar-expand-xl~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand-xl.navbar-right~.page,.navbar-vertical.navbar-expand-xl.navbar-end~.page{padding-left:0;padding-right:18rem}.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:transparent;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:18rem;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-right,.navbar-vertical.navbar-expand-xxl.navbar-end{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:18rem}.navbar-vertical.navbar-expand-xxl~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand-xxl.navbar-right~.page,.navbar-vertical.navbar-expand-xxl.navbar-end~.page{padding-left:0;padding-right:18rem}.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:transparent;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:18rem;position:fixed;top:0;left:0;bottom:0;z-index:1030;align-items:flex-start;transition:transform .3s}@media(prefers-reduced-motion:reduce){.navbar-vertical.navbar-expand{transition:none}}.navbar-vertical.navbar-expand{overflow-y:scroll;padding:0}.navbar-vertical.navbar-expand.navbar-right,.navbar-vertical.navbar-expand.navbar-end{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:18rem}.navbar-vertical.navbar-expand~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand.navbar-right~.page,.navbar-vertical.navbar-expand.navbar-end~.page{padding-left:0;padding-right:18rem}.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:transparent;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;inset:0;background-image:linear-gradient(180deg,#0000,#0009)}.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));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:none;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: .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: .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,.alert-important .alert-icon{color:inherit}.alert-minor{background:transparent;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: .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;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: .25rem;--tblr-avatar-font-size: .5rem;--tblr-avatar-icon-size: .5rem;--tblr-avatar-brand-size: .5rem}.avatar-xxs .badge:empty{width:.25rem;height:.25rem}.avatar-xs{--tblr-avatar-size: 1.25rem;--tblr-avatar-status-size: .375rem;--tblr-avatar-font-size: .625rem;--tblr-avatar-icon-size: .75rem;--tblr-avatar-brand-size: .75rem}.avatar-xs .badge:empty{width:.375rem;height:.375rem}.avatar-sm{--tblr-avatar-size: 2rem;--tblr-avatar-status-size: .5rem;--tblr-avatar-font-size: .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: .75rem;--tblr-avatar-font-size: .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: .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: .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: .5em;--tblr-badge-padding-y: .25em;--tblr-badge-font-size: .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;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:empty,.badge-dot{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: .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: .71428571em;--tblr-badge-icon-size: 1em;--tblr-badge-padding-y: 2px;--tblr-badge-padding-x: .25rem}.badge-lg{--tblr-badge-font-size: 1em;--tblr-badge-icon-size: 1em;--tblr-badge-padding-y: .25rem;--tblr-badge-padding-x: .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: .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:transparent}.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: "\b7"}.breadcrumb-arrows{--tblr-breadcrumb-divider: "\203a"}.breadcrumb-bullets{--tblr-breadcrumb-divider: "\2022"}.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), .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-right,.btn .icon-end{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:#009f95;background-color:transparent;border-color:transparent;box-shadow:none}.btn-link .icon{color:inherit}.btn-link:hover{color:#006a64;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, .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,.btn-grey,.btn-gray{--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,.btn-outline.btn-grey,.btn-outline.btn-gray{--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,.btn-ghost.btn-grey,.btn-ghost.btn-gray{--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, .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, .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, .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, .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, .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,.btn-white{--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,.btn-outline.btn-white{--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,.btn-ghost.btn-white{--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, .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,.btn-black{--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,.btn-outline.btn-black{--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,.btn-ghost.btn-black{--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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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-sm,.btn-group-sm>.btn{--tblr-btn-line-height: 1.3333333333;--tblr-btn-icon-size: 1rem}.btn-lg,.btn-group-lg>.btn{--tblr-btn-line-height: 1.5rem;--tblr-btn-icon-size: 1.5rem}.btn-xl,.btn-group-xl>.btn{--tblr-btn-line-height: 2;--tblr-btn-icon-size: 2rem;--tblr-btn-padding-y: .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-icon,.btn-action{padding-left:0;padding-right:0}.btn-icon .icon,.btn-action .icon{margin:calc(-1 * var(--tblr-btn-padding-x))}.btn-list{--tblr-list-gap: .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:transparent;box-shadow:none}.btn-action:after{content:none}.btn-action:focus{outline:none;box-shadow:none}.btn-action:hover,.btn-action.show{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:hover .icon,.btn-animate-icon:focus-visible .icon{transform:translate(4px)}.btn-animate-icon.btn-animate-icon-rotate:hover .icon,.btn-animate-icon.btn-animate-icon-rotate:focus-visible .icon{transform:rotate(90deg)}.btn-animate-icon.btn-animate-icon-move-start:hover .icon,.btn-animate-icon.btn-animate-icon-move-start:focus-visible .icon{transform:translate(-4px)}.btn-animate-icon.btn-animate-icon-pulse:hover .icon,.btn-animate-icon.btn-animate-icon-pulse:focus-visible .icon{transform:none;animation:pulse .9s}.btn-animate-icon.btn-animate-icon-shake:hover .icon,.btn-animate-icon.btn-animate-icon-shake:focus-visible .icon{transform:none;animation:shake .9s}.btn-animate-icon.btn-animate-icon-tada:hover .icon,.btn-animate-icon.btn-animate-icon-tada:focus-visible .icon{transform:none;animation:tada .9s}.btn-group,.btn-group-vertical{box-shadow:var(--tblr-shadow-input)}.btn-group>.btn-check:checked+.btn,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:5}.btn-group>.btn-check:focus+.btn,.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus{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.prev-month,.calendar-date.next-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-start .date-item,.calendar-range.range-end .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;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;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,#1f2937e6,#1f293700)}.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}.card .card{box-shadow:none}.card-borderless,.card-borderless .card-header,.card-borderless .card-footer{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,#0000,#0009)}.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}@media(prefers-reduced-motion:reduce){.card-btn{transition:none}}.card-btn{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)}.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;inset:0;content:"";background:#1f29377a}.card-cover:first-child,.card-cover:first-child:before{border-radius:6px 6px 0 0}.card-cover-blurred:before{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:transparent}.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-right,.card-rotate-end{transform:rotate(1.5deg)}.card-link{color:inherit}.card-link:hover{color:inherit;text-decoration:none;box-shadow:0 1px 6px #00000014}.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:transparent;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 thead tr:first-child,.card-table tbody tr:first-child,.card-table tfoot tr:first-child,.card-table thead tr:first-child td,.card-table thead tr:first-child th,.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{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,.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: .4;--tblr-btn-close-hover-opacity: .75;--tblr-btn-close-focus-shadow: 0 0 0 .25rem rgba(var(--tblr-primary-rgb), .25);--tblr-btn-close-focus-opacity: 1;--tblr-btn-close-disabled-opacity: .25;--tblr-btn-close-size: 1em;width:var(--tblr-btn-close-size);height:var(--tblr-btn-close-size);padding:.25em;color:var(--tblr-btn-close-color);mask:var(--tblr-btn-close-bg) no-repeat center/calc(var(--tblr-btn-close-size) * .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;user-select:none;opacity:var(--tblr-btn-close-disabled-opacity)}.dropdown-menu{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 .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(0px,9px,9px,0px)}.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-title,.empty-subtitle{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}.input-group+.form-hint,.form-control+.form-hint,.form-select+.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);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:#0000001a;color:#fff;border-color:transparent}.form-control-dark:focus{background-color:#0000001a;box-shadow:none;border-color:#ffffff3d}.form-control-dark::placeholder{color:#fff9}.form-control-rounded{border-radius:10rem}.form-control-flush{padding:0;background:none!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;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 .form-control,.input-group .btn{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:#80c2be!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 #0000000d}.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;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:"";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:hover .form-imagecheck-image,.form-imagecheck-input:focus~.form-imagecheck-figure .form-imagecheck-image,.form-imagecheck-input:checked~.form-imagecheck-figure .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:hover .form-imagecheck-caption,.form-imagecheck-input:focus~.form-imagecheck-figure .form-imagecheck-caption,.form-imagecheck-input:checked~.form-imagecheck-figure .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;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{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:0rem;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,.form-check-single .form-check-input{margin:0}.form-switch .form-check-input{height:1.25rem;margin-top:0rem}.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-select.is-invalid-lite,.form-control.is-invalid-lite,.form-select.is-valid-lite,.form-control.is-valid-lite{border-color:var(--tblr-border-color)!important}.legend{--tblr-legend-size: .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:#00857d;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:hover .list-group-item-actions,.list-group-hoverable .list-group-item-actions.show{opacity:1}.list-group-transparent{--tblr-list-group-border-radius: 0;margin:0 -1.25rem}.list-group-transparent .list-group-item{background:none;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:" \b7 ";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:translate(-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;transition:background .3s}@media(prefers-reduced-motion:reduce){.modal-body::-webkit-scrollbar{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:transparent}.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:transparent}.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:transparent;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{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-link.active,.nav-vertical .nav-item.show .nav-link{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-link.active,.nav-bordered .nav-item.show .nav-link{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}@media(prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link{align-items:center}.nav-link:hover,.nav-link:focus{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;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-prev,.page-item.page-next{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:#00857d}.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%}to,60%{right:-90%;left:100%}}.progress{position:relative;width:100%;line-height:.5rem;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;inset:0;z-index:0;height:100%;background:transparent;pointer-events:none}.progressbg-value{font-weight:var(--tblr-font-weight-medium);margin-left:auto;padding-left:2rem}.ribbon{--tblr-ribbon-margin: .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>.h1,.markdown>h2,.markdown>.h2,.markdown>h3,.markdown>.h3,.markdown>h4,.markdown>.h4,.markdown>h5,.markdown>.h5,.markdown>h6,.markdown>.h6{font-weight:var(--tblr-font-weight-bold)}.markdown>h2,.markdown>.h2,.markdown>h3,.markdown>.h3,.markdown>h4,.markdown>.h4,.markdown>h5,.markdown>.h5,.markdown>h6,.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 #0000000a}.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:transparent;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:#0000000a;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:translate(-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)}.steps-vertical .step-item:not(:last-child):after{position:absolute;content:"";transform:translate(-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)}60%{transform:scale(1.25)}}@keyframes status-pulsate-secondary{10%{transform:scale(1)}30%{transform:scale(3)}80%{transform:scale(3)}to{transform:scale(1)}}@keyframes status-pulsate-tertiary{25%{transform:scale(1)}80%{transform:scale(3);opacity:0}to{transform:scale(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:transparent!important;color:var(--tblr-body-color)!important}.status-primary{--tblr-status-color: #00857D;--tblr-status-color-rgb: 0, 133, 125}.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: .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:nth-child(1){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:nth-child(1){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:transparent;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:none}.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-up,.switch-icon-slide-left,.switch-icon-slide-start,.switch-icon-slide-right,.switch-icon-slide-end,.switch-icon-slide-down{overflow:hidden}.switch-icon-slide-up .switch-icon-a,.switch-icon-slide-up .switch-icon-b,.switch-icon-slide-left .switch-icon-a,.switch-icon-slide-left .switch-icon-b,.switch-icon-slide-start .switch-icon-a,.switch-icon-slide-start .switch-icon-b,.switch-icon-slide-right .switch-icon-a,.switch-icon-slide-right .switch-icon-b,.switch-icon-slide-end .switch-icon-a,.switch-icon-slide-end .switch-icon-b,.switch-icon-slide-down .switch-icon-a,.switch-icon-slide-down .switch-icon-b{transition:opacity .3s,transform .3s}@media(prefers-reduced-motion:reduce){.switch-icon-slide-up .switch-icon-a,.switch-icon-slide-up .switch-icon-b,.switch-icon-slide-left .switch-icon-a,.switch-icon-slide-left .switch-icon-b,.switch-icon-slide-start .switch-icon-a,.switch-icon-slide-start .switch-icon-b,.switch-icon-slide-right .switch-icon-a,.switch-icon-slide-right .switch-icon-b,.switch-icon-slide-end .switch-icon-a,.switch-icon-slide-end .switch-icon-b,.switch-icon-slide-down .switch-icon-a,.switch-icon-slide-down .switch-icon-b{transition:none}}.switch-icon-slide-up .switch-icon-a,.switch-icon-slide-left .switch-icon-a,.switch-icon-slide-start .switch-icon-a,.switch-icon-slide-right .switch-icon-a,.switch-icon-slide-end .switch-icon-a,.switch-icon-slide-down .switch-icon-a{transform:translateY(0)}.switch-icon-slide-up .switch-icon-b,.switch-icon-slide-left .switch-icon-b,.switch-icon-slide-start .switch-icon-b,.switch-icon-slide-right .switch-icon-b,.switch-icon-slide-end .switch-icon-b,.switch-icon-slide-down .switch-icon-b{transform:translateY(100%)}.switch-icon-slide-up.active .switch-icon-a,.switch-icon-slide-left.active .switch-icon-a,.switch-icon-slide-start.active .switch-icon-a,.switch-icon-slide-right.active .switch-icon-a,.switch-icon-slide-end.active .switch-icon-a,.switch-icon-slide-down.active .switch-icon-a{transform:translateY(-100%)}.switch-icon-slide-up.active .switch-icon-b,.switch-icon-slide-left.active .switch-icon-b,.switch-icon-slide-start.active .switch-icon-b,.switch-icon-slide-right.active .switch-icon-b,.switch-icon-slide-end.active .switch-icon-b,.switch-icon-slide-down.active .switch-icon-b{transform:translateY(0)}.switch-icon-slide-left .switch-icon-a,.switch-icon-slide-start .switch-icon-a{transform:translate(0)}.switch-icon-slide-left .switch-icon-b,.switch-icon-slide-start .switch-icon-b{transform:translate(100%)}.switch-icon-slide-left.active .switch-icon-a,.switch-icon-slide-start.active .switch-icon-a{transform:translate(-100%)}.switch-icon-slide-left.active .switch-icon-b,.switch-icon-slide-start.active .switch-icon-b,.switch-icon-slide-right .switch-icon-a,.switch-icon-slide-end .switch-icon-a{transform:translate(0)}.switch-icon-slide-right .switch-icon-b,.switch-icon-slide-end .switch-icon-b{transform:translate(-100%)}.switch-icon-slide-right.active .switch-icon-a,.switch-icon-slide-end.active .switch-icon-a{transform:translate(100%)}.switch-icon-slide-right.active .switch-icon-b,.switch-icon-slide-end.active .switch-icon-b{transform:translate(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)}.table thead th,.markdown>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{.table thead th,.markdown>table thead th{background:transparent}}.table-responsive .table,.table-responsive .markdown>table{margin-bottom:0}.table-responsive+.card-footer{border-top:0}.table-transparent thead th{background:transparent}.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:.5rem!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:.5rem!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:.5rem!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:.5rem!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:.5rem!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:.5rem!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}@media(prefers-reduced-motion:reduce){.table-sort{transition:none}}.table-sort{margin:-.5rem;padding:.5rem}.table-sort:hover,.table-sort.asc,.table-sort.desc{color:var(--tblr-body-color)}.table-sort:after{content:"";display:inline-flex;width:1rem;height:1rem;vertical-align:bottom;mask-image:url("data:image/svg+xml,");background:currentColor;margin-left:.25rem}.table-sort.asc:after{mask-image:url("data:image/svg+xml,")}.table-sort.desc:after{mask-image:url("data:image/svg+xml,")}.table-borderless thead th{background:transparent}.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: .625rem;--tblr-badge-padding-x: .25rem;--tblr-badge-padding-y: .125rem;margin-right:-.25rem}.tag-avatar,.tag-flag,.tag-payment,.tag-icon,.tag-check{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: .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:#1f29370a 0 2px 4px}.toast .toast-header{user-select:none}.toast button[data-bs-dismiss=toast]{outline:none}.toast-primary{--tblr-toast-color: #00857D}.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: .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-right:before,.hr-text.hr-text-end:before{content:""}.hr-text.hr-text-right:after,.hr-text.hr-text-end:after{content:none}.hr-text.hr-text-right>*:first-child,.hr-text.hr-text-end>*: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{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,.field-group h2 a,h4 a,h5 a,h6 a,.h1 a,.h2 a,.h3 a,.h4 a,.h5 a,.h6 a,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,.field-group h2,.field-group .h2,.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,.strong,b{font-weight:var(--tblr-font-weight-bold)}blockquote{padding: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:"\2014 "}ul,ol{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;transition:background .3s}@media(prefers-reduced-motion:reduce){pre::-webkit-scrollbar{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:transparent}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:transparent}pre code{background:transparent;padding:0}code{background:var(--tblr-code-bg);padding:2px 4px;border-radius:var(--tblr-border-radius)}abbr{text-decoration:underline dotted;cursor:help;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}::selection,.text-selected{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-avatar,.mention-app,.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:none;border-radius:0}.mention-count{color:var(--tblr-secondary);margin-left:.6666666667em}.text-incorrect{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);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 .field-group h2,.field-group .steps h2,.steps .field-group .h2,.field-group .steps .h2,.steps .h3{counter-increment:step}.steps h3:not(:first-child),.steps .field-group h2:not(:first-child),.field-group .steps h2:not(:first-child),.steps .field-group .h2:not(:first-child),.field-group .steps .h2:not(:first-child),.steps .h3:not(:first-child){margin-top:2.5rem!important}.steps h3:before,.steps .field-group h2:before,.field-group .steps h2:before,.steps .field-group .h2:before,.field-group .steps .h2: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;inset: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}.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;inset: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:not(caption),.visually-hidden-focusable:not(:focus):not(:focus-within):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:#f9fafb3d}.bg-dark-overlay{color:#fff;background-color:#1f29373d}.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:hover,.link-primary:focus{color:RGBA(0,106,100,var(--tblr-link-opacity, 1))!important;text-decoration-color:RGBA(0,106,100,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,.text-bg-gray{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:hover,.link-secondary:focus{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:hover,.link-success:focus{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:hover,.link-info:focus{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:hover,.link-warning:focus{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:hover,.link-danger:focus{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,.text-bg-white{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:hover,.link-light:focus{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,.text-bg-black{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:hover,.link-dark:focus{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:hover,.link-muted:focus{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:hover,.link-blue:focus{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:hover,.link-azure:focus{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:hover,.link-indigo:focus{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:hover,.link-purple:focus{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:hover,.link-pink:focus{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:hover,.link-red:focus{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:hover,.link-orange:focus{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:hover,.link-yellow:focus{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:hover,.link-lime:focus{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:hover,.link-green:focus{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:hover,.link-teal:focus{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:hover,.link-cyan:focus{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:hover,.link-white:focus{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:hover,.scrollable.hover:focus,.scrollable.hover:active{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:0!important}.opacity-5{opacity:.05!important}.opacity-10{opacity:.1!important}.opacity-15{opacity:.15!important}.opacity-20{opacity:.2!important}.opacity-25{opacity:.25!important}.opacity-30{opacity:.3!important}.opacity-35{opacity:calc(35 / 100)!important}.opacity-40{opacity:.4!important}.opacity-45{opacity:.45!important}.opacity-50{opacity:.5!important}.opacity-55{opacity:.55!important}.opacity-60{opacity:.6!important}.opacity-65{opacity:.65!important}.opacity-70{opacity:calc(70 / 100)!important}.opacity-75{opacity:.75!important}.opacity-80{opacity:.8!important}.opacity-85{opacity:.85!important}.opacity-90{opacity:.9!important}.opacity-95{opacity:calc(95 / 100)!important}.opacity-100{opacity:1!important}.hover-shadow-sm:hover{box-shadow:0 .125rem .25rem #00000013!important}.hover-shadow:hover{box-shadow:rgba(var(--tblr-body-color-rgb),.04) 0 2px 4px!important}.hover-shadow-lg:hover{box-shadow:0 1rem 3rem #0000002d!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-rotate-start,.hover-rotate-end,.hover-scale,.hover-elevate-down,.hover-elevate-up{transition:transform .3s ease}.hover-rotate-start:hover,.hover-rotate-end:hover,.hover-scale:hover,.hover-elevate-down:hover,.hover-elevate-up: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)}.ts-control{border:1px solid var(--tblr-border-color);padding:.5625rem 1rem;width:100%;overflow:hidden;position:relative;z-index:1;box-sizing:border-box;box-shadow:none;border-radius:var(--tblr-border-radius);display:flex;flex-wrap:wrap}.ts-wrapper.multi.has-items .ts-control{padding:calc(.5625rem - 1px) 1rem calc(.5625rem - 4px)}.full .ts-control{background-color:var(--tblr-bg-forms)}.disabled .ts-control,.disabled .ts-control *{cursor:default!important}.focus .ts-control{box-shadow:none}.ts-control>*{vertical-align:baseline;display:inline-block}.ts-wrapper.multi .ts-control>div{cursor:pointer;margin:0 3px 3px 0;padding:1px 5px;background:#efefef;color:#1f2937;border:0px solid #e5e7eb;overflow:auto}.ts-wrapper.multi .ts-control>div.active{background:#00857d;color:#fff;border:0px solid rgba(0,0,0,0)}.ts-wrapper.multi.disabled .ts-control>div,.ts-wrapper.multi.disabled .ts-control>div.active{color:#787878;background:#fff;border:0px solid white}.ts-control>input{flex:1 1 auto;min-width:7rem;display:inline-block!important;padding:0!important;min-height:0!important;max-height:none!important;max-width:100%!important;margin:0!important;text-indent:0!important;border:0 none!important;background:none!important;line-height:inherit!important;user-select:auto!important;box-shadow:none!important}.ts-control>input::-ms-clear{display:none}.ts-control>input:focus{outline:none!important}.has-items .ts-control>input{margin:0 4px!important}.ts-control.rtl{text-align:right}.ts-control.rtl.single .ts-control:after{left:calc(1rem + 5px);right:auto}.ts-control.rtl .ts-control>input{margin:0 4px 0 -2px!important}.disabled .ts-control{opacity:1;background-color:var(--tblr-bg-surface-secondary)}.input-hidden .ts-control>input{opacity:0;position:absolute;left:-10000px}.ts-dropdown{position:absolute;top:100%;left:0;width:100%;z-index:10;border:1px solid #d0d0d0;background:var(--tblr-bg-surface);margin:.25rem 0 0;border-top:0 none;box-sizing:border-box;box-shadow:0 1px 3px #0000001a;border-radius:0 0 var(--tblr-border-radius) var(--tblr-border-radius)}.ts-dropdown [data-selectable]{cursor:pointer;overflow:hidden}.ts-dropdown [data-selectable] .highlight{background:#ffed2866;border-radius:1px}.ts-dropdown .option,.ts-dropdown .optgroup-header,.ts-dropdown .no-results,.ts-dropdown .create{padding:3px 1rem}.ts-dropdown .option,.ts-dropdown [data-disabled],.ts-dropdown [data-disabled] [data-selectable].option{cursor:inherit;opacity:.5}.ts-dropdown [data-selectable].option{opacity:1;cursor:pointer}.ts-dropdown .optgroup:first-child .optgroup-header{border-top:0 none}.ts-dropdown .optgroup-header{color:#4b5563;background:var(--tblr-bg-surface);cursor:default}.ts-dropdown .active{background-color:rgba(var(--tblr-secondary-rgb),.08);color:inherit}.ts-dropdown .active.create{color:inherit}.ts-dropdown .create{color:#1f293780}.ts-dropdown .spinner{display:inline-block;width:30px;height:30px;margin:3px 1rem}.ts-dropdown .spinner:after{content:" ";display:block;width:24px;height:24px;margin:3px;border-radius:50%;border:5px solid #d0d0d0;border-color:#d0d0d0 transparent #d0d0d0 transparent;animation:lds-dual-ring 1.2s linear infinite}@keyframes lds-dual-ring{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.ts-dropdown-content{overflow:hidden auto;max-height:200px;scroll-behavior:smooth}.ts-wrapper.plugin-drag_drop .ts-dragging{color:transparent!important}.ts-wrapper.plugin-drag_drop .ts-dragging>*{visibility:hidden!important}.plugin-checkbox_options:not(.rtl) .option input{margin-right:.5rem}.plugin-checkbox_options.rtl .option input{margin-left:.5rem}.plugin-clear_button{--ts-pr-clear-button: 1em}.plugin-clear_button .clear-button{opacity:0;position:absolute;top:50%;transform:translateY(-50%);right:calc(1rem - 5px);margin-right:0!important;background:transparent!important;transition:opacity .5s;cursor:pointer}.plugin-clear_button.form-select .clear-button,.plugin-clear_button.single .clear-button{right:max(var(--ts-pr-caret),1rem)}.plugin-clear_button.focus.has-items .clear-button,.plugin-clear_button:not(.disabled):hover.has-items .clear-button{opacity:1}.ts-wrapper .dropdown-header{position:relative;padding:6px 1rem;border-bottom:1px solid #d0d0d0;background:color-mix(var(--tblr-bg-surface),#d0d0d0,85%);border-radius:var(--tblr-border-radius) var(--tblr-border-radius) 0 0}.ts-wrapper .dropdown-header-close{position:absolute;right:1rem;top:50%;color:#1f2937;opacity:.4;margin-top:-12px;line-height:20px;font-size:20px!important}.ts-wrapper .dropdown-header-close:hover{color:#000}.plugin-dropdown_input.focus.dropdown-active .ts-control{box-shadow:none;border:1px solid var(--tblr-border-color);box-shadow:var(--tblr-shadow-input)}.plugin-dropdown_input .dropdown-input{border:1px solid #d0d0d0;border-width:0 0 1px;display:block;padding:.5625rem 1rem;box-shadow:none;width:100%;background:transparent}.plugin-dropdown_input.focus .ts-dropdown .dropdown-input{border-color:#80c2be;outline:0;box-shadow:var(--tblr-shadow-input),0 0 0 .25rem rgba(var(--tblr-primary-rgb),.25)}.plugin-dropdown_input .items-placeholder{border:0 none!important;box-shadow:none!important;width:100%}.plugin-dropdown_input.has-items .items-placeholder,.plugin-dropdown_input.dropdown-active .items-placeholder{display:none!important}.ts-wrapper.plugin-input_autogrow.has-items .ts-control>input{min-width:0}.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control>input{flex:none;min-width:4px}.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control>input::placeholder{color:transparent}.ts-dropdown.plugin-optgroup_columns .ts-dropdown-content{display:flex}.ts-dropdown.plugin-optgroup_columns .optgroup{border-right:1px solid #f2f2f2;border-top:0 none;flex-grow:1;flex-basis:0;min-width:0}.ts-dropdown.plugin-optgroup_columns .optgroup:last-child{border-right:0 none}.ts-dropdown.plugin-optgroup_columns .optgroup:before{display:none}.ts-dropdown.plugin-optgroup_columns .optgroup-header{border-top:0 none}.ts-wrapper.plugin-remove_button .item{display:inline-flex;align-items:center}.ts-wrapper.plugin-remove_button .item .remove{color:inherit;text-decoration:none;vertical-align:middle;display:inline-block;padding:0 5px;border-radius:0 2px 2px 0;box-sizing:border-box}.ts-wrapper.plugin-remove_button .item .remove:hover{background:#0000000d}.ts-wrapper.plugin-remove_button.disabled .item .remove:hover{background:none}.ts-wrapper.plugin-remove_button .remove-single{position:absolute;right:0;top:0;font-size:23px}.ts-wrapper.plugin-remove_button:not(.rtl) .item{padding-right:0!important}.ts-wrapper.plugin-remove_button:not(.rtl) .item .remove{border-left:1px solid #e5e7eb;margin-left:5px}.ts-wrapper.plugin-remove_button:not(.rtl) .item.active .remove{border-left-color:#0000}.ts-wrapper.plugin-remove_button:not(.rtl).disabled .item .remove{border-left-color:#fff}.ts-wrapper.plugin-remove_button.rtl .item{padding-left:0!important}.ts-wrapper.plugin-remove_button.rtl .item .remove{border-right:1px solid #e5e7eb;margin-right:5px}.ts-wrapper.plugin-remove_button.rtl .item.active .remove{border-right-color:#0000}.ts-wrapper.plugin-remove_button.rtl.disabled .item .remove{border-right-color:#fff}:root{--ts-pr-clear-button: 0px;--ts-pr-caret: 0px;--ts-pr-min: .75rem}.ts-wrapper.single .ts-control,.ts-wrapper.single .ts-control input{cursor:pointer}.ts-control:not(.rtl){padding-right:max(var(--ts-pr-min),var(--ts-pr-clear-button) + var(--ts-pr-caret))!important}.ts-control.rtl{padding-left:max(var(--ts-pr-min),var(--ts-pr-clear-button) + var(--ts-pr-caret))!important}.ts-wrapper{position:relative}.ts-dropdown,.ts-control,.ts-control input{color:#1f2937;font-family:inherit;font-size:inherit;line-height:1.25rem}.ts-control,.ts-wrapper.single.input-active .ts-control{background:var(--tblr-bg-forms);cursor:text}.ts-hidden-accessible{border:0!important;clip:rect(0 0 0 0)!important;clip-path:inset(50%)!important;overflow:hidden!important;padding:0!important;position:absolute!important;width:1px!important;white-space:nowrap!important}.ts-dropdown,.ts-dropdown.form-control,.ts-dropdown.form-select{height:auto;padding:0;z-index:1000;background:var(--tblr-bg-surface);border:1px solid var(--tblr-border-color-translucent);border-radius:6px;box-shadow:0 6px 12px #0000002d}.ts-dropdown .optgroup-header{font-size:.765625rem;line-height:1.4285714286}.ts-dropdown .optgroup:first-child:before{display:none}.ts-dropdown .optgroup:before{content:" ";display:block;height:0;margin:var(--tblr-spacer-2) 0;overflow:hidden;border-top:1px solid var(--tblr-border-color-translucent);margin-left:-1rem;margin-right:-1rem}.ts-dropdown .create{padding-left:1rem}.ts-dropdown-content{padding:5px 0}.ts-control{box-shadow:var(--tblr-shadow-input);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion:reduce){.ts-control{transition:none}}.ts-control{display:flex;align-items:center}.focus .ts-control{border-color:#80c2be;outline:0;box-shadow:var(--tblr-shadow-input),0 0 0 .25rem rgba(var(--tblr-primary-rgb),.25)}.ts-control .item{display:flex;align-items:center}.ts-control input::placeholder{color:var(--bs-secondary-color, #a7aeb8);opacity:1}.ts-wrapper.is-invalid,select.tomselected.is-invalid+div.ts-wrapper,.was-validated .invalid,.was-validated :invalid+.ts-wrapper{border-color:var(--tblr-form-invalid-color)}.ts-wrapper.is-invalid:not(.single),select.tomselected.is-invalid+div.ts-wrapper:not(.single),.was-validated .invalid:not(.single),.was-validated :invalid+.ts-wrapper:not(.single){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-position:right 1.53125rem center;background-size:1.8125rem 1.8125rem;background-repeat:no-repeat}.ts-wrapper.is-invalid.single,select.tomselected.is-invalid+div.ts-wrapper.single,.was-validated .invalid.single,.was-validated :invalid+.ts-wrapper.single{background-image: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"),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-position:right 1rem center,center right 3rem;background-size:16px 12px,1.8125rem 1.8125rem;background-repeat:no-repeat}.ts-wrapper.is-invalid.focus .ts-control,select.tomselected.is-invalid+div.ts-wrapper.focus .ts-control,.was-validated .invalid.focus .ts-control,.was-validated :invalid+.ts-wrapper.focus .ts-control{border-color:var(--tblr-form-invalid-color);box-shadow:0 0 0 .25rem rgba(var(--tblr-form-invalid-color),.25)}.ts-wrapper.is-valid,.was-validated .valid,.was-validated :valid+.ts-wrapper{border-color:var(--tblr-form-valid-color)}.ts-wrapper.is-valid:not(.single),.was-validated .valid:not(.single),.was-validated :valid+.ts-wrapper:not(.single){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-position:right 1.53125rem center;background-size:1.8125rem 1.8125rem;background-repeat:no-repeat}.ts-wrapper.is-valid.single,.was-validated .valid.single,.was-validated :valid+.ts-wrapper.single{background-image: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"),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-position:right 1rem center,center right 3rem;background-size:16px 12px,1.8125rem 1.8125rem;background-repeat:no-repeat}.ts-wrapper.is-valid.focus .ts-control,.was-validated .valid.focus .ts-control,.was-validated :valid+.ts-wrapper.focus .ts-control{border-color:var(--tblr-form-valid-color);box-shadow:0 0 0 .25rem rgba(var(--tblr-form-valid-color),.25)}.ts-wrapper{min-height:calc(1.25rem + 1.125rem + calc(var(--tblr-border-width) * 2));display:flex}.input-group-sm>.ts-wrapper,.ts-wrapper.form-select-sm,.ts-wrapper.form-control-sm{min-height:calc(1.25rem + .625rem + calc(var(--tblr-border-width) * 2))}.input-group-sm>.ts-wrapper .ts-control,.ts-wrapper.form-select-sm .ts-control,.ts-wrapper.form-control-sm .ts-control{border-radius:var(--tblr-border-radius-sm);font-size:.75rem}.input-group-sm>.ts-wrapper.has-items .ts-control,.ts-wrapper.form-select-sm.has-items .ts-control,.ts-wrapper.form-control-sm.has-items .ts-control{font-size:.75rem}.input-group-sm>.ts-wrapper.multi.has-items .ts-control,.ts-wrapper.form-select-sm.multi.has-items .ts-control,.ts-wrapper.form-control-sm.multi.has-items .ts-control{padding-top:calc((calc(1.25rem + .625rem + calc(var(--tblr-border-width) * 2)) - 1.25rem * .75rem - calc((var(--tblr-border-width) + 1px) * 2)) / 2)!important}.ts-wrapper.multi.has-items .ts-control{padding-left:calc(1rem - 5px);--ts-pr-min: calc(1rem - 5px) }.ts-wrapper.multi .ts-control>div{border-radius:calc(var(--tblr-border-radius) - 1px)}.input-group-lg>.ts-wrapper,.ts-wrapper.form-control-lg,.ts-wrapper.form-select-lg{min-height:calc(1.25rem + 1.375rem + calc(var(--tblr-border-width) * 2))}.input-group-lg>.ts-wrapper .ts-control,.ts-wrapper.form-control-lg .ts-control,.ts-wrapper.form-select-lg .ts-control{border-radius:var(--tblr-border-radius-lg);font-size:1rem}.ts-wrapper:not(.form-control,.form-select){padding:0;border:none;height:auto;box-shadow:none;background:none}.ts-wrapper:not(.form-control,.form-select).single .ts-control{background-image: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");background-repeat:no-repeat;background-position:right 1rem center;background-size:16px 12px}.ts-wrapper.form-select,.ts-wrapper.single{--ts-pr-caret: 3rem}.ts-wrapper.form-control,.ts-wrapper.form-select{padding:0!important;height:auto;box-shadow:none;display:flex}.ts-wrapper.form-control .ts-control,.ts-wrapper.form-control.single.input-active .ts-control,.ts-wrapper.form-select .ts-control,.ts-wrapper.form-select.single.input-active .ts-control{border:none!important}.ts-wrapper.form-control:not(.disabled) .ts-control,.ts-wrapper.form-control:not(.disabled).single.input-active .ts-control,.ts-wrapper.form-select:not(.disabled) .ts-control,.ts-wrapper.form-select:not(.disabled).single.input-active .ts-control{background:transparent!important}.input-group>.ts-wrapper{flex-grow:1;width:1%}.input-group>.ts-wrapper:not(:nth-child(2))>.ts-control{border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.ts-wrapper:not(:last-child)>.ts-control{border-top-right-radius:0;border-bottom-right-radius:0}.form-select .ts-dropdown,.form-select .ts-control,.form-select .ts-control input{color:var(--tblr-body-color)}:root{--ts-pr-clear-button: 0rem;--ts-pr-caret: 0rem}.ts-input{color:inherit}.focus .ts-control{border-radius:var(--tblr-border-radius)}.ts-control{color:inherit}.ts-control .dropdown-menu{width:100%;height:auto}.ts-wrapper .form-control,.ts-wrapper .form-select,.ts-wrapper.form-control,.ts-wrapper.form-select{box-shadow:var(--tblr-shadow-input)}.ts-wrapper.is-invalid .ts-control,select.tomselected.is-invalid+div.ts-wrapper .ts-control,.ts-wrapper.is-valid .ts-control{--ts-pr-clear-button: 1.5rem}.ts-dropdown{background:var(--tblr-bg-surface);color:var(--tblr-body-color);box-shadow:var(--tblr-shadow-dropdown);z-index:1000}.ts-dropdown .option{padding:.5rem .75rem}.ts-control,.ts-control input{color:var(--tblr-body-color)}.ts-control input::placeholder{color:var(--tblr-tertiary)}.ts-wrapper.multi .ts-control>div,.ts-wrapper.multi.disabled .ts-control>div{background:var(--tblr-bg-surface-secondary);border:1px solid var(--tblr-border-color);color:var(--tblr-body-color)}.ts-wrapper.disabled .ts-control{opacity:1}.ts-wrapper.disabled .ts-control>div.item{color:var(--tblr-gray-500)}html{scroll-behavior:auto!important}mark,.mark{padding-left:0;padding-right:0}.table-responsive .dropdown,.table-responsive .btn-group,.table-responsive .btn-group-vertical{position:static}.progress{min-width:80px}hr.dropdown-divider,.dropdown-divider.hr{margin-bottom:.25rem;margin-top:.25rem}.dropdown-item{font-weight:400}*{font-feature-settings:"liga" 0;font-variant-ligatures:none}@media(min-width:992px){:root,:host{margin-left:0;scrollbar-gutter:stable}}pre{background-color:transparent;color:inherit}.alert{background:var(--tblr-bg-surface)}.badge{user-select:text}.btn{display:inline-block}.btn:focus{border:1px solid var(--tblr-primary-fg);outline:2px solid var(--tblr-primary)!important}.btn-sm,.btn-group-sm>.btn{border-radius:6px}.dropdown-item{display:inline-block}.footer .text-primary{color:#001423!important}.nav-tabs .nav-link{display:inline-block}.page,.page-tabs .nav-tabs .nav-link.active{background-color:var(--tblr-bg-surface-tertiary)!important}.navbar{--tblr-navbar-active-bg: rgba(0, 0, 0, .06)}[data-bs-theme=dark],body[data-bs-theme=dark] [data-bs-theme=light]{--tblr-alert-color: darken(var(--tblr-warning),10%);--tblr-link-color: #00F2D4;--tblr-link-color-rgb: 0,242,212;--tblr-link-hover-color-rgb: 0,242,212;--tblr-secondary: #9ca3af;--tblr-primary: #00F2D4;--tblr-primary-fg: #001423;--tblr-primary-rgb: 0,242,212;--tblr-btn-active-color: #001423}html[data-bs-theme=dark] ::selection,body[data-bs-theme=dark] html[data-bs-theme=light] ::selection{background-color:rgba(var(--tblr-primary-rgb),.48)}html[data-bs-theme=dark] .btn-primary,body[data-bs-theme=dark] html[data-bs-theme=light] .btn-primary,html[data-bs-theme=dark] .bg-primary .card-title,body[data-bs-theme=dark] html[data-bs-theme=light] .bg-primary .card-title,html[data-bs-theme=dark] .bg-primary a,body[data-bs-theme=dark] html[data-bs-theme=light] .bg-primary a,html[data-bs-theme=dark] .bg-primary i,body[data-bs-theme=dark] html[data-bs-theme=light] .bg-primary i,html[data-bs-theme=dark] .text-bg-primary,body[data-bs-theme=dark] html[data-bs-theme=light] .text-bg-primary{color:#001423!important}html[data-bs-theme=dark] .card,body[data-bs-theme=dark] html[data-bs-theme=light] .card{background:#001423!important}html[data-bs-theme=dark],body[data-bs-theme=dark] html[data-bs-theme=light],html[data-bs-theme=dark] .navbar,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar,html[data-bs-theme=dark] .page-header,body[data-bs-theme=dark] html[data-bs-theme=light] .page-header{background-color:#001423}html[data-bs-theme=dark] .page,body[data-bs-theme=dark] html[data-bs-theme=light] .page,html[data-bs-theme=dark] .page-tabs .nav-tabs .nav-link.active,body[data-bs-theme=dark] html[data-bs-theme=light] .page-tabs .nav-tabs .nav-link.active{background-color:#081b2a!important}html[data-bs-theme=dark] .page-link.active,body[data-bs-theme=dark] html[data-bs-theme=light] .page-link.active,html[data-bs-theme=dark] .active>.page-link,body[data-bs-theme=dark] html[data-bs-theme=light] .active>.page-link{color:#001423}html[data-bs-theme=dark] .text-bg-primary,body[data-bs-theme=dark] html[data-bs-theme=light] .text-bg-primary{color:#001423!important}html[data-bs-theme=dark] .text-muted,body[data-bs-theme=dark] html[data-bs-theme=light] .text-muted{color:var(--tblr-secondary-color)!important}html[data-bs-theme=dark] .text-secondary,body[data-bs-theme=dark] html[data-bs-theme=light] .text-secondary{color:#9ca3af!important}html[data-bs-theme=dark] .footer .text-primary,body[data-bs-theme=dark] html[data-bs-theme=light] .footer .text-primary{color:#fff!important}html[data-bs-theme=dark] .toast,body[data-bs-theme=dark] html[data-bs-theme=light] .toast{color:var(--tblr-body-color)}html[data-bs-theme=dark] .table-primary,body[data-bs-theme=dark] html[data-bs-theme=light] .table-primary{--tblr-table-bg: rgba(var(--tblr-secondary-rgb), .48);--tblr-table-hover-bg: inherit;--tblr-table-hover-color: inherit}pre code{padding:unset}.dropdown-toggle:after{font-family:Material Design Icons;content:"\f0140";padding-right:9px;border-bottom:none;border-left:none;transform:none;vertical-align:.05em;height:auto}.accordion-button-toggle{align-items:center;justify-content:center;flex:0 0 auto}.accordion-button-toggle>i{display:block;line-height:1}:root:not(.dummy)[data-bs-theme=light] .hide-theme-light,:root:not(.dummy)[data-bs-theme=dark] .hide-theme-dark,body[data-bs-theme=dark] [data-bs-theme=light]:root:not(.dummy) .hide-theme-dark{display:none!important}:root:not(.dummy)[data-bs-theme=dark] .hide-theme-light,body[data-bs-theme=dark] [data-bs-theme=light]:root:not(.dummy) .hide-theme-light,:root:not(.dummy)[data-bs-theme=light] .hide-theme-dark{display:inline-flex!important}.ts-wrapper.multi .ts-control{padding:7px 7px 3px}.ts-wrapper.multi .ts-control div{margin:0 4px 4px 0}.badge a{color:inherit;text-decoration:none}.page-body .card{margin-bottom:1rem}.page-body .card .card-header,.page-body .card .card-body,.page-body .card .card-footer{padding:.75rem}.page-body .card .card-header{background:var(--tblr-bg-surface-tertiary)}.page-body .card h2.card-header,.page-body .card .card-header.h2{font-size:var(--tblr-font-size-h5);line-height:var(--tblr-line-height-h5);margin-bottom:0}.page-body .card .list-group-item{padding:.5rem .75rem}.page-body .card .table,.page-body .card .markdown>table{margin-bottom:0}form.object-edit{margin:auto;max-width:800px}.col-form-label.required{font-weight:700}.col-form-label.required:after{position:absolute;display:inline-block;margin-left:0;font-family:Material Design Icons;font-size:8px;content:"\f06c4"}.has-errors input,.has-errors select,.has-errors textarea{border:1px solid #d63939}select[multiple] optgroup{top:0;background-color:var(--bs-body-bg);font-style:normal;font-weight:700}select[multiple] option{padding-left:.5rem}.modifier-select{min-width:10rem;max-width:15rem;width:auto;white-space:nowrap}.page{background-color:var(--tblr-bg-surface-secondary)}.page-header{background-color:var(--tblr-bg-surface);min-height:0}.navbar-vertical.navbar-expand-lg .navbar-collapse .nav-link-icon,.navbar-vertical.navbar-expand-lg .navbar-collapse .nav-link-title{color:#001423}.navbar-vertical.navbar-expand-lg .navbar-collapse .text-secondary{color:#00857d!important}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item a{color:#001423}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item .btn-group{visibility:hidden}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item:hover,.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item.active{background-color:var(--tblr-navbar-active-bg)}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item:hover a,.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item.active a{text-decoration:none}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item:hover .btn-group,.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item.active .btn-group{visibility:visible}.navbar-vertical.navbar-expand-lg .navbar-nav{z-index:1}@media(max-width:991.98px){.navbar-vertical.navbar-expand-lg .navbar-brand{padding:.2rem 0}}.navbar-vertical.navbar-expand-lg .navbar-brand a:hover{text-decoration:none}.navbar-vertical.navbar-expand-lg img.motif{bottom:0;display:none;left:0;mask-image:linear-gradient(180deg,#0000,#0000004d);opacity:.5;position:fixed;user-drag:none;user-select:none;-moz-user-select:none;-webkit-user-drag:none;-webkit-user-select:none;-ms-user-select:none;width:18rem}@media(min-width:992px){.navbar-vertical.navbar-expand-lg img.motif{display:block}}html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg{background:linear-gradient(180deg,#00857d00,#00857d1a),#fff}html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg{background:linear-gradient(180deg,#00f2d400,#00f2d41a),#001423}html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg .nav-item.dropdown.active:after,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg .nav-item.dropdown.active:after{border-color:#00f2d4!important}html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg .nav-link-title,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg .nav-link-title,html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg .nav-link-icon,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg .nav-link-icon,html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg .dropdown-item a,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg .dropdown-item a{color:#fff!important}html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg .dropdown-item.active,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg .dropdown-item.active,html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg .dropdown-item:hover,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg .dropdown-item:hover{background-color:#ffffff0f!important}html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg .text-secondary,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg .text-secondary{color:#00f2d4!important}html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg img.motif,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg img.motif{opacity:.25}.progress{height:20px}.progress .progress-label{display:flex;flex-direction:column;justify-content:center;padding-left:.25rem}.table thead th,.markdown>table thead th{font-size:.625rem}table.object-list tbody>tr:last-child>td{border-bottom-width:0}table.object-list th.asc>a:after{content:"\f0140";font-family:Material Design Icons}table.object-list th.desc>a:after{content:"\f0143";font-family:Material Design Icons}table.attr-table th{font-weight:400;width:min-content}table.attr-table th,table.attr-table td{border-bottom-style:dashed}table.attr-table tr:last-child{border-bottom-style:hidden}table.attr-table td{overflow-wrap:anywhere}td pre{margin-bottom:0}table th.orderable a{color:var(--tblr-body-color)}html[data-bs-theme=dark] .table thead th,body[data-bs-theme=dark] html[data-bs-theme=light] .table thead th,body[data-bs-theme=dark] html[data-bs-theme=light] .markdown>table thead th,html[data-bs-theme=dark] .markdown>table thead th{background:#001423!important}.page-tabs{border-bottom:1px solid var(--tblr-border-color-translucent)}.page-tabs .nav-tabs{position:relative;border:none}.page-tabs .nav-tabs .nav-link.active,.page-tabs .nav-tabs .nav-link:active,.page-tabs .nav-tabs .nav-link:hover{border-color:var(--tblr-border-color-translucent);border-bottom-color:transparent}.page-tabs .nav-tabs .nav-link.active{color:inherit;background:var(--tblr-bg-surface-secondary);border-bottom-color:transparent}pre.change-data{border-radius:0;padding:0;margin-inline:-.75rem}pre.change-data>span{display:block;padding-inline:.5rem;max-width:100%;min-width:fit-content;border-left:.25rem solid transparent}pre.change-data>span.added{background-color:var(--tblr-green-200);border-left-color:var(--tblr-green-darken)}pre.change-data>span.removed{background-color:var(--tblr-red-200);border-left-color:var(--tblr-red-darken)}pre.change-diff{border:var(--tblr-border-width) solid transparent}pre.change-diff.change-added{background-color:var(--tblr-green-lt);border-color:var(--tblr-green)}pre.change-diff.change-removed{background-color:var(--tblr-red-lt);border-color:var(--tblr-red)}pre.block{padding:1rem;border:var(--tblr-border-width) solid #e5e7eb;border-radius:6px}.grid-stack .card-header.bg-default{background:var(--tblr-bg-surface-secondary)!important}.grid-stack .card-header a{color:inherit!important}tr[data-cable-status=connected]{background-color:#2fb34426}tr[data-cable-status=planned]{background-color:#066fd126}tr[data-cable-status=decommissioning]{background-color:#f59f0026}tr[data-mark-connected=true]{background-color:#2fb34426}tr[data-virtual=true]{background-color:#00857d26}tr[data-enabled=disabled]{background-color:#9ca3af26}tr[data-cable-status=connected] button.mark-installed{display:none}tr:not([data-cable-status=connected]) button.mark-planned{display:none}.rendered-markdown table{width:100%}.rendered-markdown table th{border-bottom:2px solid #dddddd;padding:8px}.rendered-markdown table td{border-top:1px solid #dddddd;padding:8px}.rendered-markdown table th[align=left]{text-align:left}.rendered-markdown table th[align=center]{text-align:center}.rendered-markdown table th[align=right]{text-align:right}.rendered-markdown p:last-child{margin-bottom:0}td>.rendered-markdown{max-height:200px;overflow-y:scroll}.markdown-widget .preview{border:1px solid #e5e7eb;border-radius:6px;min-height:200px}span.color-label{display:inline-block;width:5rem;height:1rem;padding:.25em .5em;border:1px solid #303030;border-radius:6px}.record-depth{display:inline;user-select:none;opacity:33%}.record-depth span:only-of-type,.record-depth span:last-of-type{margin-right:.25rem}.hide-last-child :last-child{visibility:hidden;opacity:0}.netbox-edition{letter-spacing:.15rem}.logo{height:80px}.sso-icon{height:24px}img.plugin-icon{max-width:1.4285em;height:auto}.thumbnail{max-width:200px}.thumbnail img{border:1px solid #606060}.image-preview-popover{--bs-popover-max-width: clamp(240px, 25vw, 640px)}.image-preview-popover .popover-header{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.image-preview-popover .popover-body{display:flex;justify-content:center;align-items:center}.image-preview-popover img{display:block;max-width:100%;max-height:clamp(160px,33vh,640px);height:auto}html[data-bs-theme=dark] img.plugin-icon,body[data-bs-theme=dark] html[data-bs-theme=light] img.plugin-icon{filter:grayscale(100%) invert(100%) brightness(80%)}.sticky-actions{z-index:4}.sticky-actions[data-sticky-when=always],.sticky-actions.is-sticky-active{position:sticky;bottom:.25rem}.sticky-actions[data-sticky-position=left],.sticky-actions[data-sticky-position=right]{display:flex;align-items:center;gap:.5rem;padding:.5rem;max-width:100%;width:fit-content}.sticky-actions[data-sticky-position=right]{margin-left:auto}.sticky-actions[data-sticky-position=full]{max-width:100%}.sticky-actions-footer{max-width:100%;margin-top:1.5rem;padding:1rem;padding-bottom:calc(1rem + env(safe-area-inset-bottom,0));background:var(--tblr-bg-surface-secondary);border-top:1px solid var(--tblr-border-color)}.sticky-actions-footer[data-sticky-when=always],.sticky-actions-footer.is-sticky-active{bottom:0}.sticky-actions-footer>.btn-list{justify-content:flex-end;margin-bottom:0}.object-edit--with-sticky-actions{padding-bottom:calc(1.5rem + env(safe-area-inset-bottom,0))}[data-sticky-when=selection] .bulk-action-buttons .btn:disabled,[data-sticky-when=selection] .bulk-action-buttons .btn.disabled{border-color:transparent;box-shadow:none}[data-sticky-when=selection] .bulk-action-buttons .btn.disabled{pointer-events:none}.btn-float-group-right{position:sticky;bottom:10px;z-index:4;margin-left:auto;width:fit-content}.btn-float-group-left{position:sticky;bottom:10px;z-index:4;width:fit-content}tr[data-read=True] td{background-color:var(--tblr-bg-surface-secondary);color:#6b7280}.rack-loading-container{min-height:200px;margin-left:30px} +@charset "UTF-8";:root,[data-bs-theme=light]{--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: #00857D;--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: 0, 133, 125;--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(0, 53.2, 50);--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(204, 230.6, 229);--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(153, 206.2, 203);--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", system-ui, sans-serif;--tblr-font-monospace: "Roboto Mono";--tblr-gradient: linear-gradient(180deg, rgba(255, 255, 255, .15), rgba(255, 255, 255, 0));--tblr-body-font-family: var(--tblr-font-sans-serif);--tblr-body-font-size: .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, .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, .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: #00857D;--tblr-link-color-rgb: 0, 133, 125;--tblr-link-decoration: none;--tblr-link-hover-color: rgb(0, 106.4, 100);--tblr-link-hover-color-rgb: 0, 106, 100;--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, .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), .04) 0 2px 4px 0;--tblr-box-shadow-sm: 0 .125rem .25rem rgba(0, 0, 0, .075);--tblr-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, .175);--tblr-box-shadow-inset: 0 0 transparent;--tblr-focus-ring-width: .25rem;--tblr-focus-ring-opacity: .25;--tblr-focus-ring-color: rgba(var(--tblr-primary-rgb), .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, .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, .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(102, 181.8, 177);--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(0, 26.6, 25);--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(0, 79.8, 75);--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(102, 181.8, 177);--tblr-link-hover-color: rgb(132.6, 196.44, 192.6);--tblr-link-color-rgb: 102, 182, 177;--tblr-link-hover-color-rgb: 133, 196, 193;--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, .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)}*,*:before,*:after{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:rgba(0,0,0,0)}hr,.hr{margin:2rem 0;color:inherit;border:0;border-top:var(--tblr-border-width) solid;opacity:.16}h6,.h6,h5,.h5,h4,.h4,h3,.field-group h2,.field-group .h2,.h3,h2,.h2,h1,.h1{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,.field-group h2,.field-group .h2,.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]{text-decoration:underline dotted;cursor:help;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{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}pre,code,kbd,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:.5rem;padding-bottom:.5rem;color:var(--tblr-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}thead,tbody,tfoot,tr,td,th{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}input,button,select,optgroup,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}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button:not(:disabled),[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]: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-fields-wrapper,::-webkit-datetime-edit-text,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button{cursor:pointer;filter:grayscale(1)}::-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,.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:"\2014\a0"}.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-xxl,.container-xl,.container-lg,.container-md,.container-sm{--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-sm,.container{max-width:540px}}@media(min-width:768px){.container-md,.container-sm,.container{max-width:720px}}@media(min-width:992px){.container-lg,.container-md,.container-sm,.container{max-width:960px}}@media(min-width:1200px){.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1140px}}@media(min-width:1400px){.container-xxl,.container-xl,.container-lg,.container-md,.container-sm,.container{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: .25rem}.g-1,.gy-1{--tblr-gutter-y: .25rem}.g-2,.gx-2{--tblr-gutter-x: .5rem}.g-2,.gy-2{--tblr-gutter-y: .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: .25rem}.g-sm-1,.gy-sm-1{--tblr-gutter-y: .25rem}.g-sm-2,.gx-sm-2{--tblr-gutter-x: .5rem}.g-sm-2,.gy-sm-2{--tblr-gutter-y: .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: .25rem}.g-md-1,.gy-md-1{--tblr-gutter-y: .25rem}.g-md-2,.gx-md-2{--tblr-gutter-x: .5rem}.g-md-2,.gy-md-2{--tblr-gutter-y: .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: .25rem}.g-lg-1,.gy-lg-1{--tblr-gutter-y: .25rem}.g-lg-2,.gx-lg-2{--tblr-gutter-x: .5rem}.g-lg-2,.gy-lg-2{--tblr-gutter-y: .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: .25rem}.g-xl-1,.gy-xl-1{--tblr-gutter-y: .25rem}.g-xl-2,.gx-xl-2{--tblr-gutter-x: .5rem}.g-xl-2,.gy-xl-2{--tblr-gutter-y: .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: .25rem}.g-xxl-1,.gy-xxl-1{--tblr-gutter-y: .25rem}.g-xxl-2,.gx-xxl-2{--tblr-gutter-x: .5rem}.g-xxl-2,.gy-xxl-2{--tblr-gutter-y: .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}}.table,.markdown>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), .075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--tblr-table-border-color)}.table>:not(caption)>*>*,.markdown>table>:not(caption)>*>*{padding:.5rem;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)))}.table>tbody,.markdown>table>tbody{vertical-align:inherit}.table>thead,.markdown>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}.table-sm>:not(caption)>*>*,.markdown>table>:not(caption)>*>*{padding:.25rem}.table-bordered>:not(caption)>*,.markdown>table>:not(caption)>*{border-width:var(--tblr-border-width) 0}.table-bordered>:not(caption)>*>*,.markdown>table>: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(204, 230.6, 229);--tblr-table-border-color: rgb(169.4, 192.68, 194.2);--tblr-table-striped-bg: rgb(195.35, 221.12, 220.3);--tblr-table-striped-color: #1f2937;--tblr-table-active-bg: rgb(186.7, 211.64, 211.6);--tblr-table-active-color: #1f2937;--tblr-table-hover-bg: rgb(191.025, 216.38, 215.95);--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);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:#80c2be;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::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-sm,.form-control-plaintext.form-control-lg{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);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:#80c2be;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;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);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:#80c2be;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}.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%28127.5, 194, 190%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;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;appearance:none;background-color:var(--tblr-primary);border:2px var(--tblr-border-style) #ffffff;border-radius:1rem;box-shadow:0 .1rem .25rem #0000001a;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{transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b3dad8}.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;appearance:none;background-color:var(--tblr-primary);border:2px var(--tblr-border-style) #ffffff;border-radius:1rem;box-shadow:0 .1rem .25rem #0000001a;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{transition:none}}.form-range::-moz-range-thumb:active{background-color:#b3dad8}.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;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}.form-floating>.form-control::placeholder,.form-floating>.form-control-plaintext::placeholder{color:transparent}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown),.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill,.form-floating>.form-control-plaintext:-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:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-control-plaintext~label,.form-floating>.form-select~label{transform:scale(.85) translateY(-.5rem) translate(.15rem)}.form-floating>.form-control:-webkit-autofill~label{transform:scale(.85) translateY(-.5rem) translate(.15rem)}.form-floating>textarea:focus~label:after,.form-floating>textarea:not(:placeholder-shown)~label:after{position:absolute;inset:1rem .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>:disabled~label,.form-floating>.form-control: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-select,.input-group>.form-floating{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus,.input-group>.form-floating:focus-within{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>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text,.input-group-lg>.btn{padding:.6875rem 1.5rem;font-size:1rem;border-radius:var(--tblr-border-radius-lg)}.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text,.input-group-sm>.btn{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)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),.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{border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),.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{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)}.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip,.is-valid~.valid-feedback,.is-valid~.valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-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}.was-validated .form-control:valid:focus,.form-control.is-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}.was-validated .form-select:valid,.form-select.is-valid{border-color:var(--tblr-form-valid-border-color)}.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"],.form-select.is-valid:not([multiple]):not([size]),.form-select.is-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}.was-validated .form-select:valid:focus,.form-select.is-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 .form-control-color:valid,.form-control-color.is-valid{width:5.375rem}.was-validated .form-check-input:valid,.form-check-input.is-valid{border-color:var(--tblr-form-valid-border-color)}.was-validated .form-check-input:valid:checked,.form-check-input.is-valid:checked{background-color:var(--tblr-form-valid-color)}.was-validated .form-check-input:valid:focus,.form-check-input.is-valid:focus{box-shadow:0 0 0 .25rem rgba(var(--tblr-success-rgb),.25)}.was-validated .form-check-input:valid~.form-check-label,.form-check-input.is-valid~.form-check-label{color:var(--tblr-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):valid,.input-group>.form-control:not(:focus).is-valid,.was-validated .input-group>.form-select:not(:focus):valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.input-group>.form-floating:not(:focus-within).is-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)}.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip,.is-invalid~.invalid-feedback,select.tomselected.is-invalid+div.ts-wrapper~.invalid-feedback,.is-invalid~.invalid-tooltip,select.tomselected.is-invalid+div.ts-wrapper~.invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid,select.tomselected.is-invalid+div.form-control.ts-wrapper{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}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus,select.tomselected.is-invalid+div.form-control.ts-wrapper: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}.was-validated .form-select:invalid,.form-select.is-invalid,select.tomselected.is-invalid+div.form-select.ts-wrapper{border-color:var(--tblr-form-invalid-border-color)}.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"],.form-select.is-invalid:not([multiple]):not([size]),select.tomselected.is-invalid+div.form-select.ts-wrapper:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],select.tomselected.is-invalid+div.form-select.ts-wrapper: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}.was-validated .form-select:invalid:focus,.form-select.is-invalid:focus,select.tomselected.is-invalid+div.form-select.ts-wrapper: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 .form-control-color:invalid,.form-control-color.is-invalid,select.tomselected.is-invalid+div.form-control-color.ts-wrapper{width:5.375rem}.was-validated .form-check-input:invalid,.form-check-input.is-invalid,select.tomselected.is-invalid+div.form-check-input.ts-wrapper{border-color:var(--tblr-form-invalid-border-color)}.was-validated .form-check-input:invalid:checked,.form-check-input.is-invalid:checked,select.tomselected.is-invalid+div.form-check-input.ts-wrapper:checked{background-color:var(--tblr-form-invalid-color)}.was-validated .form-check-input:invalid:focus,.form-check-input.is-invalid:focus,select.tomselected.is-invalid+div.form-check-input.ts-wrapper:focus{box-shadow:0 0 0 .25rem rgba(var(--tblr-danger-rgb),.25)}.was-validated .form-check-input:invalid~.form-check-label,.form-check-input.is-invalid~.form-check-label,select.tomselected.is-invalid+div.form-check-input.ts-wrapper~.form-check-label{color:var(--tblr-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):invalid,.input-group>.form-control:not(:focus).is-invalid,.input-group>select.tomselected.is-invalid+div.form-control.ts-wrapper:not(:focus),.was-validated .input-group>.form-select:not(:focus):invalid,.input-group>.form-select:not(:focus).is-invalid,.input-group>select.tomselected.is-invalid+div.form-select.ts-wrapper:not(:focus),.was-validated .input-group>.form-floating:not(:focus-within):invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>select.tomselected.is-invalid+div.form-floating.ts-wrapper:not(:focus-within){z-index:4}.btn{--tblr-btn-padding-x: .5rem;--tblr-btn-padding-y: .25rem;--tblr-btn-font-family: var(--tblr-body-font-family);--tblr-btn-font-size: .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: .4;--tblr-btn-focus-box-shadow: 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;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,:not(.btn-check)+.btn:active,.btn:first-child:active,.btn.active,.btn.show{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,:not(.btn-check)+.btn:active:focus-visible,.btn:first-child:active:focus-visible,.btn.active:focus-visible,.btn.show: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: 37, 151, 144;text-decoration:none}.btn-link:hover,.btn-link:focus-visible{text-decoration:underline}.btn-link:focus-visible{color:var(--tblr-btn-color)}.btn-link:hover{color:var(--tblr-btn-hover-color)}.btn-lg,.btn-group-lg>.btn{--tblr-btn-padding-y: .6875rem;--tblr-btn-padding-x: 1.5rem;--tblr-btn-font-size: 1rem;--tblr-btn-border-radius: var(--tblr-border-radius-lg)}.btn-sm,.btn-group-sm>.btn{--tblr-btn-padding-y: .3125rem;--tblr-btn-padding-x: .5rem;--tblr-btn-font-size: .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}}.dropup,.dropend,.dropdown,.dropstart,.dropup-center,.dropdown-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: .25rem;--tblr-dropdown-spacer: 1px;--tblr-dropdown-font-size: .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), .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: .75rem;--tblr-dropdown-item-padding-y: .5rem;--tblr-dropdown-header-color: #4b5563;--tblr-dropdown-header-padding-x: .75rem;--tblr-dropdown-header-padding-y: .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:hover,.dropdown-item:focus{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, .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>.btn,.btn-group-vertical>.btn{position:relative;flex:1 1 auto}.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{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>:not(.btn-check:first-child)+.btn,.btn-group>.btn-group:not(:first-child){margin-left:calc(-1 * var(--tblr-border-width))}.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn,.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.dropdown-toggle-split:after,.dropup .dropdown-toggle-split:after,.dropend .dropdown-toggle-split:after{margin-left:0}.dropstart .dropdown-toggle-split:before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:1.125rem;padding-left:1.125rem}.btn-group.show .dropdown-toggle{box-shadow:inset 0 3px 5px #00000020}.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:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:calc(-1 * var(--tblr-border-width))}.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:nth-child(n+3),.btn-group-vertical>:not(.btn-check)+.btn,.btn-group-vertical>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--tblr-nav-link-padding-x: .75rem;--tblr-nav-link-padding-y: .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:none;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}.nav-link:hover,.nav-link:focus{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:hover,.nav-tabs .nav-link:focus{isolation:isolate;border-color:var(--tblr-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{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: rgba(var(--tblr-secondary-rgb), .15)}.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: .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:hover,.nav-underline .nav-link:focus{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-link,.nav-fill .nav-item{flex:1 1 auto;text-align:center}.nav-justified>.nav-link,.nav-justified .nav-item{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: .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: .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: .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), .15);--tblr-navbar-toggler-border-radius: var(--tblr-border-radius);--tblr-navbar-toggler-focus-width: 0;--tblr-navbar-toggler-transition: box-shadow .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-sm,.navbar>.container-md,.navbar>.container-lg,.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:hover,.navbar-brand:focus{color:var(--tblr-navbar-brand-hover-color);text-decoration:none}.navbar-nav{--tblr-nav-link-padding-x: 0;--tblr-nav-link-padding-y: .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:hover,.navbar-text a:focus{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, .7);--tblr-navbar-hover-color: rgba(255, 255, 255, .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, .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;inset:0;padding:var(--tblr-card-img-overlay-padding);border-radius:var(--tblr-card-inner-border-radius)}.card-img,.card-img-top,.card-img-bottom{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-img-top,.card-group>.card:not(:last-child)>.card-header{border-top-right-radius:0}.card-group>.card:not(:last-child)>.card-img-bottom,.card-group>.card:not(:last-child)>.card-footer{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-img-top,.card-group>.card:not(:first-child)>.card-header{border-top-left-radius:0}.card-group>.card:not(:first-child)>.card-img-bottom,.card-group>.card:not(:first-child)>.card-footer{border-bottom-left-radius:0}}.pagination{--tblr-pagination-padding-x: .25rem;--tblr-pagination-padding-y: calc(.25rem + 1px) ;--tblr-pagination-font-size: .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 .25rem rgba(var(--tblr-primary-rgb), .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)}.page-link.active,.active>.page-link{z-index:3;color:var(--tblr-pagination-active-color);background-color:var(--tblr-pagination-active-bg);border-color:var(--tblr-pagination-active-border-color)}.page-link.disabled,.disabled>.page-link{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:-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: .75rem;--tblr-pagination-font-size: 1.09375rem;--tblr-pagination-border-radius: var(--tblr-border-radius-lg)}.pagination-sm{--tblr-pagination-padding-x: .5rem;--tblr-pagination-padding-y: .25rem;--tblr-pagination-font-size: .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: .5rem;--tblr-progress-font-size: .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 .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), .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):hover,.list-group-item-action:not(.active):focus{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: .75rem;--tblr-toast-padding-y: .5rem;--tblr-toast-spacing: calc(var(--tblr-page-padding) * 2);--tblr-toast-max-width: 350px;--tblr-toast-font-size: .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), .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: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: .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: .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:translateY(-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: .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-header,.modal-fullscreen .modal-footer{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-header,.modal-fullscreen-sm-down .modal-footer{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-header,.modal-fullscreen-md-down .modal-footer{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-header,.modal-fullscreen-lg-down .modal-footer{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-header,.modal-fullscreen-xl-down .modal-footer{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-header,.modal-fullscreen-xxl-down .modal-footer{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: .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: .9;--tblr-tooltip-arrow-width: .8rem;--tblr-tooltip-arrow-height: .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-top .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow{bottom:calc(-1 * var(--tblr-tooltip-arrow-height))}.bs-tooltip-top .tooltip-arrow:before,.bs-tooltip-auto[data-popper-placement^=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-end .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow{left:calc(-1 * var(--tblr-tooltip-arrow-height));width:var(--tblr-tooltip-arrow-height);height:var(--tblr-tooltip-arrow-width)}.bs-tooltip-end .tooltip-arrow:before,.bs-tooltip-auto[data-popper-placement^=right] .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-bottom .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow{top:calc(-1 * var(--tblr-tooltip-arrow-height))}.bs-tooltip-bottom .tooltip-arrow:before,.bs-tooltip-auto[data-popper-placement^=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-start .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow{right:calc(-1 * var(--tblr-tooltip-arrow-height));width:var(--tblr-tooltip-arrow-height);height:var(--tblr-tooltip-arrow-width)}.bs-tooltip-start .tooltip-arrow:before,.bs-tooltip-auto[data-popper-placement^=left] .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: .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: .5rem;--tblr-popover-header-font-size: .875rem;--tblr-popover-header-color: inherit;--tblr-popover-header-bg: transparent;--tblr-popover-body-padding-x: .5rem;--tblr-popover-body-padding-y: .5rem;--tblr-popover-body-color: inherit;--tblr-popover-arrow-width: 1rem;--tblr-popover-arrow-height: .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:before,.popover .popover-arrow:after{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-top>.popover-arrow,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow{bottom:calc(-1 * (var(--tblr-popover-arrow-height)) - var(--tblr-popover-border-width))}.bs-popover-top>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:before,.bs-popover-top>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:after{border-width:var(--tblr-popover-arrow-height) calc(var(--tblr-popover-arrow-width) * .5) 0}.bs-popover-top>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:before{bottom:0;border-top-color:var(--tblr-popover-arrow-border)}.bs-popover-top>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow:after{bottom:var(--tblr-popover-border-width);border-top-color:var(--tblr-popover-bg)}.bs-popover-end>.popover-arrow,.bs-popover-auto[data-popper-placement^=right]>.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-end>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:before,.bs-popover-end>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:after{border-width:calc(var(--tblr-popover-arrow-width) * .5) var(--tblr-popover-arrow-height) calc(var(--tblr-popover-arrow-width) * .5) 0}.bs-popover-end>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:before{left:0;border-right-color:var(--tblr-popover-arrow-border)}.bs-popover-end>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow:after{left:var(--tblr-popover-border-width);border-right-color:var(--tblr-popover-bg)}.bs-popover-bottom>.popover-arrow,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow{top:calc(-1 * (var(--tblr-popover-arrow-height)) - var(--tblr-popover-border-width))}.bs-popover-bottom>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:before,.bs-popover-bottom>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:after{border-width:0 calc(var(--tblr-popover-arrow-width) * .5) var(--tblr-popover-arrow-height)}.bs-popover-bottom>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:before{top:0;border-bottom-color:var(--tblr-popover-arrow-border)}.bs-popover-bottom>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow:after{top:var(--tblr-popover-border-width);border-bottom-color:var(--tblr-popover-bg)}.bs-popover-bottom .popover-header:before,.bs-popover-auto[data-popper-placement^=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-start>.popover-arrow,.bs-popover-auto[data-popper-placement^=left]>.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-start>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:before,.bs-popover-start>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:after{border-width:calc(var(--tblr-popover-arrow-width) * .5) 0 calc(var(--tblr-popover-arrow-width) * .5) var(--tblr-popover-arrow-height)}.bs-popover-start>.popover-arrow:before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow:before{right:0;border-left-color:var(--tblr-popover-arrow-border)}.bs-popover-start>.popover-arrow:after,.bs-popover-auto[data-popper-placement^=left]>.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.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-start),.active.carousel-item-end{transform:translate(100%)}.carousel-item-prev:not(.carousel-item-end),.active.carousel-item-start{transform:translate(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end{z-index:1;opacity:1}.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{z-index:0;opacity:0;transition:opacity 0s .6s}@media(prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{transition:none}}.carousel-control-prev,.carousel-control-next{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:none;filter:var(--tblr-carousel-control-icon-filter);border:0;opacity:.5;transition:opacity .15s ease}@media(prefers-reduced-motion:reduce){.carousel-control-prev,.carousel-control-next{transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-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-grow,.spinner-border{display:inline-block;flex-shrink:0;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: -.125em;--tblr-spinner-border-width: 2px;--tblr-spinner-animation-speed: .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: -.125em;--tblr-spinner-animation-speed: .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-xxl,.offcanvas-xl,.offcanvas-lg,.offcanvas-md,.offcanvas-sm{--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 .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:translate(-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:translate(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.showing,.offcanvas-sm.show:not(.hiding){transform:none}.offcanvas-sm.showing,.offcanvas-sm.hiding,.offcanvas-sm.show{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:translate(-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:translate(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.showing,.offcanvas-md.show:not(.hiding){transform:none}.offcanvas-md.showing,.offcanvas-md.hiding,.offcanvas-md.show{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:translate(-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:translate(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.showing,.offcanvas-lg.show:not(.hiding){transform:none}.offcanvas-lg.showing,.offcanvas-lg.hiding,.offcanvas-lg.show{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:translate(-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:translate(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.showing,.offcanvas-xl.show:not(.hiding){transform:none}.offcanvas-xl.showing,.offcanvas-xl.hiding,.offcanvas-xl.show{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:translate(-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:translate(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.showing,.offcanvas-xxl.show:not(.hiding){transform:none}.offcanvas-xxl.showing,.offcanvas-xxl.hiding,.offcanvas-xxl.show{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:translate(-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:translate(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.showing,.offcanvas.show:not(.hiding){transform:none}.offcanvas.showing,.offcanvas.hiding,.offcanvas.show{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{mask-image:linear-gradient(130deg,#000 55%,#000000e6,#000 95%);mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{to{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{object-fit:contain!important}.object-fit-cover{object-fit:cover!important}.object-fit-fill{object-fit:fill!important}.object-fit-scale{object-fit:scale-down!important}.object-fit-none{object-fit:none!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:translate(-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: .1}.border-opacity-25{--tblr-border-opacity: .25}.border-opacity-50{--tblr-border-opacity: .5}.border-opacity-75{--tblr-border-opacity: .75}.border-opacity-100{--tblr-border-opacity: 1}.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}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!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}.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{column-gap:0!important}.column-gap-1{column-gap:.25rem!important}.column-gap-2{column-gap:.5rem!important}.column-gap-3{column-gap:1rem!important}.column-gap-4{column-gap:1.5rem!important}.column-gap-5{column-gap:2rem!important}.column-gap-6{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,.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:#00000080!important}.text-white-50{--tblr-text-opacity: 1;color:#ffffff80!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: .25}.text-opacity-50{--tblr-text-opacity: .5}.text-opacity-75{--tblr-text-opacity: .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,.link-opacity-10-hover:hover{--tblr-link-opacity: .1}.link-opacity-25,.link-opacity-25-hover:hover{--tblr-link-opacity: .25}.link-opacity-50,.link-opacity-50-hover:hover{--tblr-link-opacity: .5}.link-opacity-75,.link-opacity-75-hover:hover{--tblr-link-opacity: .75}.link-opacity-100,.link-opacity-100-hover:hover{--tblr-link-opacity: 1}.link-offset-1,.link-offset-1-hover:hover{text-underline-offset:.125em!important}.link-offset-2,.link-offset-2-hover:hover{text-underline-offset:.25em!important}.link-offset-3,.link-offset-3-hover:hover{text-underline-offset:.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,.link-underline-opacity-0-hover:hover{--tblr-link-underline-opacity: 0}.link-underline-opacity-10,.link-underline-opacity-10-hover:hover{--tblr-link-underline-opacity: .1}.link-underline-opacity-25,.link-underline-opacity-25-hover:hover{--tblr-link-underline-opacity: .25}.link-underline-opacity-50,.link-underline-opacity-50-hover:hover{--tblr-link-underline-opacity: .5}.link-underline-opacity-75,.link-underline-opacity-75-hover:hover{--tblr-link-underline-opacity: .75}.link-underline-opacity-100,.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: .1}.bg-opacity-25{--tblr-bg-opacity: .25}.bg-opacity-50{--tblr-bg-opacity: .5}.bg-opacity-75{--tblr-bg-opacity: .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{user-select:all!important}.user-select-auto{user-select:auto!important}.user-select-none{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{object-fit:contain!important}.object-cover{object-fit:cover!important}.object-fill{object-fit:fill!important}.object-scale-down{object-fit:scale-down!important}.object-none{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{columns:2!important}.columns-3{columns:3!important}.columns-4{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{object-fit:contain!important}.object-fit-sm-cover{object-fit:cover!important}.object-fit-sm-fill{object-fit:fill!important}.object-fit-sm-scale{object-fit:scale-down!important}.object-fit-sm-none{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{column-gap:0!important}.column-gap-sm-1{column-gap:.25rem!important}.column-gap-sm-2{column-gap:.5rem!important}.column-gap-sm-3{column-gap:1rem!important}.column-gap-sm-4{column-gap:1.5rem!important}.column-gap-sm-5{column-gap:2rem!important}.column-gap-sm-6{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{columns:2!important}.columns-sm-3{columns:3!important}.columns-sm-4{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{object-fit:contain!important}.object-fit-md-cover{object-fit:cover!important}.object-fit-md-fill{object-fit:fill!important}.object-fit-md-scale{object-fit:scale-down!important}.object-fit-md-none{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{column-gap:0!important}.column-gap-md-1{column-gap:.25rem!important}.column-gap-md-2{column-gap:.5rem!important}.column-gap-md-3{column-gap:1rem!important}.column-gap-md-4{column-gap:1.5rem!important}.column-gap-md-5{column-gap:2rem!important}.column-gap-md-6{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{columns:2!important}.columns-md-3{columns:3!important}.columns-md-4{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{object-fit:contain!important}.object-fit-lg-cover{object-fit:cover!important}.object-fit-lg-fill{object-fit:fill!important}.object-fit-lg-scale{object-fit:scale-down!important}.object-fit-lg-none{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{column-gap:0!important}.column-gap-lg-1{column-gap:.25rem!important}.column-gap-lg-2{column-gap:.5rem!important}.column-gap-lg-3{column-gap:1rem!important}.column-gap-lg-4{column-gap:1.5rem!important}.column-gap-lg-5{column-gap:2rem!important}.column-gap-lg-6{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{columns:2!important}.columns-lg-3{columns:3!important}.columns-lg-4{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{object-fit:contain!important}.object-fit-xl-cover{object-fit:cover!important}.object-fit-xl-fill{object-fit:fill!important}.object-fit-xl-scale{object-fit:scale-down!important}.object-fit-xl-none{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{column-gap:0!important}.column-gap-xl-1{column-gap:.25rem!important}.column-gap-xl-2{column-gap:.5rem!important}.column-gap-xl-3{column-gap:1rem!important}.column-gap-xl-4{column-gap:1.5rem!important}.column-gap-xl-5{column-gap:2rem!important}.column-gap-xl-6{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{columns:2!important}.columns-xl-3{columns:3!important}.columns-xl-4{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{object-fit:contain!important}.object-fit-xxl-cover{object-fit:cover!important}.object-fit-xxl-fill{object-fit:fill!important}.object-fit-xxl-scale{object-fit:scale-down!important}.object-fit-xxl-none{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{column-gap:0!important}.column-gap-xxl-1{column-gap:.25rem!important}.column-gap-xxl-2{column-gap:.5rem!important}.column-gap-xxl-3{column-gap:1rem!important}.column-gap-xxl-4{column-gap:1.5rem!important}.column-gap-xxl-5{column-gap:2rem!important}.column-gap-xxl-6{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{columns:2!important}.columns-xxl-3{columns:3!important}.columns-xxl-4{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}}:root,:host{--tblr-font-monospace: Roboto Mono;--tblr-font-sans-serif: Inter, system-ui, 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-brand: #00857D;--tblr-primary: #00857D;--tblr-primary-rgb: 0, 133, 125;--tblr-primary-fg: var(--tblr-light);--tblr-primary-darken: rgb(0, 119.7, 112.5);--tblr-primary-darken: color-mix(in oklab, var(--tblr-primary), transparent 20%);--tblr-primary-lt: rgb(229.5, 242.8, 242);--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, 243, 242;--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: .25rem;--tblr-spacer-2: .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: .875rem;--tblr-font-size-h5: .75rem;--tblr-font-size-h6: .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), .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), .06);--tblr-shadow-card: 0 0 4px rgba(var(--tblr-body-color-rgb), .04);--tblr-shadow-card-hover: rgba(var(--tblr-body-color-rgb), .16) 0 2px 16px 0;--tblr-shadow-dropdown: 0 16px 24px 2px rgba(0, 0, 0, .07), 0 6px 30px 5px rgba(0, 0, 0, .06), 0 8px 10px -5px rgba(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))}:root,:host{font-size:16px;height:100%}@media(min-width:992px){:root,:host{margin-left:calc(100vw - 100%);margin-right:0}}:root,:host,[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, .1);--tblr-border-dark-color: #9ca3af;--tblr-border-dark-color-translucent: rgba(4, 32, 69, .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), .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, .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){:root,:host,[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:scaleZ(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:scaleZ(1)}}@keyframes rotate-360{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes blink{0%{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:transparent}}*{scrollbar-color:color-mix(in srgb,var(--tblr-scrollbar-color, var(--tblr-body-color)) 20%,transparent) transparent}*::-webkit-scrollbar{width:1rem;height:1rem;transition:background .3s}@media(prefers-reduced-motion:reduce){*::-webkit-scrollbar{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:transparent}*: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:transparent}.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: #00857D;--tblr-navbar-active-bg: rgba(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:transparent;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-right,.navbar-expand-sm.navbar-vertical.navbar-end{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:18rem}.navbar-expand-sm.navbar-vertical.navbar-right~.navbar,.navbar-expand-sm.navbar-vertical.navbar-right~.page-wrapper,.navbar-expand-sm.navbar-vertical.navbar-end~.navbar,.navbar-expand-sm.navbar-vertical.navbar-end~.page-wrapper{margin-left:0;margin-right:18rem}}@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:transparent;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-right,.navbar-expand-md.navbar-vertical.navbar-end{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:18rem}.navbar-expand-md.navbar-vertical.navbar-right~.navbar,.navbar-expand-md.navbar-vertical.navbar-right~.page-wrapper,.navbar-expand-md.navbar-vertical.navbar-end~.navbar,.navbar-expand-md.navbar-vertical.navbar-end~.page-wrapper{margin-left:0;margin-right:18rem}}@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:transparent;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-right,.navbar-expand-lg.navbar-vertical.navbar-end{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:18rem}.navbar-expand-lg.navbar-vertical.navbar-right~.navbar,.navbar-expand-lg.navbar-vertical.navbar-right~.page-wrapper,.navbar-expand-lg.navbar-vertical.navbar-end~.navbar,.navbar-expand-lg.navbar-vertical.navbar-end~.page-wrapper{margin-left:0;margin-right:18rem}}@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:transparent;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-right,.navbar-expand-xl.navbar-vertical.navbar-end{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:18rem}.navbar-expand-xl.navbar-vertical.navbar-right~.navbar,.navbar-expand-xl.navbar-vertical.navbar-right~.page-wrapper,.navbar-expand-xl.navbar-vertical.navbar-end~.navbar,.navbar-expand-xl.navbar-vertical.navbar-end~.page-wrapper{margin-left:0;margin-right:18rem}}@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:transparent;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-right,.navbar-expand-xxl.navbar-vertical.navbar-end{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:18rem}.navbar-expand-xxl.navbar-vertical.navbar-right~.navbar,.navbar-expand-xxl.navbar-vertical.navbar-right~.page-wrapper,.navbar-expand-xxl.navbar-vertical.navbar-end~.navbar,.navbar-expand-xxl.navbar-vertical.navbar-end~.page-wrapper{margin-left:0;margin-right:18rem}}.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:transparent;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-right,.navbar-expand.navbar-vertical.navbar-end{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:18rem}.navbar-expand.navbar-vertical.navbar-right~.navbar,.navbar-expand.navbar-vertical.navbar-right~.page-wrapper,.navbar-expand.navbar-vertical.navbar-end~.navbar,.navbar-expand.navbar-vertical.navbar-end~.page-wrapper{margin-left:0;margin-right:18rem}.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}@media(prefers-reduced-motion:reduce){.navbar-toggler-icon{transition:none}}.navbar-toggler-icon{position:relative}.navbar-toggler-icon:before,.navbar-toggler-icon:after{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:before,.navbar-toggler-icon:after{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:transparent!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:18rem;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-right,.navbar-vertical.navbar-expand-sm.navbar-end{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:18rem}.navbar-vertical.navbar-expand-sm~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand-sm.navbar-right~.page,.navbar-vertical.navbar-expand-sm.navbar-end~.page{padding-left:0;padding-right:18rem}.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:transparent;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:18rem;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-right,.navbar-vertical.navbar-expand-md.navbar-end{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:18rem}.navbar-vertical.navbar-expand-md~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand-md.navbar-right~.page,.navbar-vertical.navbar-expand-md.navbar-end~.page{padding-left:0;padding-right:18rem}.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:transparent;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:18rem;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-right,.navbar-vertical.navbar-expand-lg.navbar-end{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:18rem}.navbar-vertical.navbar-expand-lg~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand-lg.navbar-right~.page,.navbar-vertical.navbar-expand-lg.navbar-end~.page{padding-left:0;padding-right:18rem}.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:transparent;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:18rem;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-right,.navbar-vertical.navbar-expand-xl.navbar-end{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:18rem}.navbar-vertical.navbar-expand-xl~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand-xl.navbar-right~.page,.navbar-vertical.navbar-expand-xl.navbar-end~.page{padding-left:0;padding-right:18rem}.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:transparent;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:18rem;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-right,.navbar-vertical.navbar-expand-xxl.navbar-end{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:18rem}.navbar-vertical.navbar-expand-xxl~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand-xxl.navbar-right~.page,.navbar-vertical.navbar-expand-xxl.navbar-end~.page{padding-left:0;padding-right:18rem}.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:transparent;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:18rem;position:fixed;top:0;left:0;bottom:0;z-index:1030;align-items:flex-start;transition:transform .3s}@media(prefers-reduced-motion:reduce){.navbar-vertical.navbar-expand{transition:none}}.navbar-vertical.navbar-expand{overflow-y:scroll;padding:0}.navbar-vertical.navbar-expand.navbar-right,.navbar-vertical.navbar-expand.navbar-end{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:18rem}.navbar-vertical.navbar-expand~.page [class^=container]{padding-left:1.5rem;padding-right:1.5rem}.navbar-vertical.navbar-expand.navbar-right~.page,.navbar-vertical.navbar-expand.navbar-end~.page{padding-left:0;padding-right:18rem}.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:transparent;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;inset:0;background-image:linear-gradient(180deg,#0000,#0009)}.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));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:none;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: .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: .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,.alert-important .alert-icon{color:inherit}.alert-minor{background:transparent;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: .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;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: .25rem;--tblr-avatar-font-size: .5rem;--tblr-avatar-icon-size: .5rem;--tblr-avatar-brand-size: .5rem}.avatar-xxs .badge:empty{width:.25rem;height:.25rem}.avatar-xs{--tblr-avatar-size: 1.25rem;--tblr-avatar-status-size: .375rem;--tblr-avatar-font-size: .625rem;--tblr-avatar-icon-size: .75rem;--tblr-avatar-brand-size: .75rem}.avatar-xs .badge:empty{width:.375rem;height:.375rem}.avatar-sm{--tblr-avatar-size: 2rem;--tblr-avatar-status-size: .5rem;--tblr-avatar-font-size: .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: .75rem;--tblr-avatar-font-size: .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: .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: .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: .5em;--tblr-badge-padding-y: .25em;--tblr-badge-font-size: .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;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:empty,.badge-dot{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: .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: .71428571em;--tblr-badge-icon-size: 1em;--tblr-badge-padding-y: 2px;--tblr-badge-padding-x: .25rem}.badge-lg{--tblr-badge-font-size: 1em;--tblr-badge-icon-size: 1em;--tblr-badge-padding-y: .25rem;--tblr-badge-padding-x: .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: .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:transparent}.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: "\b7"}.breadcrumb-arrows{--tblr-breadcrumb-divider: "\203a"}.breadcrumb-bullets{--tblr-breadcrumb-divider: "\2022"}.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), .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-right,.btn .icon-end{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:#009f95;background-color:transparent;border-color:transparent;box-shadow:none}.btn-link .icon{color:inherit}.btn-link:hover{color:#006a64;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, .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,.btn-grey,.btn-gray{--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,.btn-outline.btn-grey,.btn-outline.btn-gray{--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,.btn-ghost.btn-grey,.btn-ghost.btn-gray{--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, .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, .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, .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, .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, .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,.btn-white{--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,.btn-outline.btn-white{--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,.btn-ghost.btn-white{--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, .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,.btn-black{--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,.btn-outline.btn-black{--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,.btn-ghost.btn-black{--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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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, .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-sm,.btn-group-sm>.btn{--tblr-btn-line-height: 1.3333333333;--tblr-btn-icon-size: 1rem}.btn-lg,.btn-group-lg>.btn{--tblr-btn-line-height: 1.5rem;--tblr-btn-icon-size: 1.5rem}.btn-xl,.btn-group-xl>.btn{--tblr-btn-line-height: 2;--tblr-btn-icon-size: 2rem;--tblr-btn-padding-y: .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-icon,.btn-action{padding-left:0;padding-right:0}.btn-icon .icon,.btn-action .icon{margin:calc(-1 * var(--tblr-btn-padding-x))}.btn-list{--tblr-list-gap: .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:transparent;box-shadow:none}.btn-action:after{content:none}.btn-action:focus{outline:none;box-shadow:none}.btn-action:hover,.btn-action.show{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:hover .icon,.btn-animate-icon:focus-visible .icon{transform:translate(4px)}.btn-animate-icon.btn-animate-icon-rotate:hover .icon,.btn-animate-icon.btn-animate-icon-rotate:focus-visible .icon{transform:rotate(90deg)}.btn-animate-icon.btn-animate-icon-move-start:hover .icon,.btn-animate-icon.btn-animate-icon-move-start:focus-visible .icon{transform:translate(-4px)}.btn-animate-icon.btn-animate-icon-pulse:hover .icon,.btn-animate-icon.btn-animate-icon-pulse:focus-visible .icon{transform:none;animation:pulse .9s}.btn-animate-icon.btn-animate-icon-shake:hover .icon,.btn-animate-icon.btn-animate-icon-shake:focus-visible .icon{transform:none;animation:shake .9s}.btn-animate-icon.btn-animate-icon-tada:hover .icon,.btn-animate-icon.btn-animate-icon-tada:focus-visible .icon{transform:none;animation:tada .9s}.btn-group,.btn-group-vertical{box-shadow:var(--tblr-shadow-input)}.btn-group>.btn-check:checked+.btn,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:5}.btn-group>.btn-check:focus+.btn,.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus{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.prev-month,.calendar-date.next-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-start .date-item,.calendar-range.range-end .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;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;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,#1f2937e6,#1f293700)}.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}.card .card{box-shadow:none}.card-borderless,.card-borderless .card-header,.card-borderless .card-footer{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,#0000,#0009)}.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}@media(prefers-reduced-motion:reduce){.card-btn{transition:none}}.card-btn{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)}.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;inset:0;content:"";background:#1f29377a}.card-cover:first-child,.card-cover:first-child:before{border-radius:6px 6px 0 0}.card-cover-blurred:before{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:transparent}.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-right,.card-rotate-end{transform:rotate(1.5deg)}.card-link{color:inherit}.card-link:hover{color:inherit;text-decoration:none;box-shadow:0 1px 6px #00000014}.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:transparent;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 thead tr:first-child,.card-table tbody tr:first-child,.card-table tfoot tr:first-child,.card-table thead tr:first-child td,.card-table thead tr:first-child th,.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{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,.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: .4;--tblr-btn-close-hover-opacity: .75;--tblr-btn-close-focus-shadow: 0 0 0 .25rem rgba(var(--tblr-primary-rgb), .25);--tblr-btn-close-focus-opacity: 1;--tblr-btn-close-disabled-opacity: .25;--tblr-btn-close-size: 1em;width:var(--tblr-btn-close-size);height:var(--tblr-btn-close-size);padding:.25em;color:var(--tblr-btn-close-color);mask:var(--tblr-btn-close-bg) no-repeat center/calc(var(--tblr-btn-close-size) * .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;user-select:none;opacity:var(--tblr-btn-close-disabled-opacity)}.dropdown-menu{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 .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(0px,9px,9px,0px)}.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-title,.empty-subtitle{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}.input-group+.form-hint,.form-control+.form-hint,.form-select+.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);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:#0000001a;color:#fff;border-color:transparent}.form-control-dark:focus{background-color:#0000001a;box-shadow:none;border-color:#ffffff3d}.form-control-dark::placeholder{color:#fff9}.form-control-rounded{border-radius:10rem}.form-control-flush{padding:0;background:none!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;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 .form-control,.input-group .btn{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:#80c2be!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 #0000000d}.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;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:"";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:hover .form-imagecheck-image,.form-imagecheck-input:focus~.form-imagecheck-figure .form-imagecheck-image,.form-imagecheck-input:checked~.form-imagecheck-figure .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:hover .form-imagecheck-caption,.form-imagecheck-input:focus~.form-imagecheck-figure .form-imagecheck-caption,.form-imagecheck-input:checked~.form-imagecheck-figure .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;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{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:0rem;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,.form-check-single .form-check-input{margin:0}.form-switch .form-check-input{height:1.25rem;margin-top:0rem}.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-select.is-invalid-lite,.form-control.is-invalid-lite,.form-select.is-valid-lite,.form-control.is-valid-lite{border-color:var(--tblr-border-color)!important}.legend{--tblr-legend-size: .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:#00857d;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:hover .list-group-item-actions,.list-group-hoverable .list-group-item-actions.show{opacity:1}.list-group-transparent{--tblr-list-group-border-radius: 0;margin:0 -1.25rem}.list-group-transparent .list-group-item{background:none;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:" \b7 ";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:translate(-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;transition:background .3s}@media(prefers-reduced-motion:reduce){.modal-body::-webkit-scrollbar{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:transparent}.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:transparent}.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:transparent;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{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-link.active,.nav-vertical .nav-item.show .nav-link{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-link.active,.nav-bordered .nav-item.show .nav-link{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}@media(prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link{align-items:center}.nav-link:hover,.nav-link:focus{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;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-prev,.page-item.page-next{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:#00857d}.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%}to,60%{right:-90%;left:100%}}.progress{position:relative;width:100%;line-height:.5rem;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;inset:0;z-index:0;height:100%;background:transparent;pointer-events:none}.progressbg-value{font-weight:var(--tblr-font-weight-medium);margin-left:auto;padding-left:2rem}.ribbon{--tblr-ribbon-margin: .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>.h1,.markdown>h2,.markdown>.h2,.markdown>h3,.markdown>.h3,.markdown>h4,.markdown>.h4,.markdown>h5,.markdown>.h5,.markdown>h6,.markdown>.h6{font-weight:var(--tblr-font-weight-bold)}.markdown>h2,.markdown>.h2,.markdown>h3,.markdown>.h3,.markdown>h4,.markdown>.h4,.markdown>h5,.markdown>.h5,.markdown>h6,.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 #0000000a}.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:transparent;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:#0000000a;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:translate(-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)}.steps-vertical .step-item:not(:last-child):after{position:absolute;content:"";transform:translate(-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)}60%{transform:scale(1.25)}}@keyframes status-pulsate-secondary{10%{transform:scale(1)}30%{transform:scale(3)}80%{transform:scale(3)}to{transform:scale(1)}}@keyframes status-pulsate-tertiary{25%{transform:scale(1)}80%{transform:scale(3);opacity:0}to{transform:scale(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:transparent!important;color:var(--tblr-body-color)!important}.status-primary{--tblr-status-color: #00857D;--tblr-status-color-rgb: 0, 133, 125}.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: .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:nth-child(1){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:nth-child(1){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:transparent;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:none}.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-up,.switch-icon-slide-left,.switch-icon-slide-start,.switch-icon-slide-right,.switch-icon-slide-end,.switch-icon-slide-down{overflow:hidden}.switch-icon-slide-up .switch-icon-a,.switch-icon-slide-up .switch-icon-b,.switch-icon-slide-left .switch-icon-a,.switch-icon-slide-left .switch-icon-b,.switch-icon-slide-start .switch-icon-a,.switch-icon-slide-start .switch-icon-b,.switch-icon-slide-right .switch-icon-a,.switch-icon-slide-right .switch-icon-b,.switch-icon-slide-end .switch-icon-a,.switch-icon-slide-end .switch-icon-b,.switch-icon-slide-down .switch-icon-a,.switch-icon-slide-down .switch-icon-b{transition:opacity .3s,transform .3s}@media(prefers-reduced-motion:reduce){.switch-icon-slide-up .switch-icon-a,.switch-icon-slide-up .switch-icon-b,.switch-icon-slide-left .switch-icon-a,.switch-icon-slide-left .switch-icon-b,.switch-icon-slide-start .switch-icon-a,.switch-icon-slide-start .switch-icon-b,.switch-icon-slide-right .switch-icon-a,.switch-icon-slide-right .switch-icon-b,.switch-icon-slide-end .switch-icon-a,.switch-icon-slide-end .switch-icon-b,.switch-icon-slide-down .switch-icon-a,.switch-icon-slide-down .switch-icon-b{transition:none}}.switch-icon-slide-up .switch-icon-a,.switch-icon-slide-left .switch-icon-a,.switch-icon-slide-start .switch-icon-a,.switch-icon-slide-right .switch-icon-a,.switch-icon-slide-end .switch-icon-a,.switch-icon-slide-down .switch-icon-a{transform:translateY(0)}.switch-icon-slide-up .switch-icon-b,.switch-icon-slide-left .switch-icon-b,.switch-icon-slide-start .switch-icon-b,.switch-icon-slide-right .switch-icon-b,.switch-icon-slide-end .switch-icon-b,.switch-icon-slide-down .switch-icon-b{transform:translateY(100%)}.switch-icon-slide-up.active .switch-icon-a,.switch-icon-slide-left.active .switch-icon-a,.switch-icon-slide-start.active .switch-icon-a,.switch-icon-slide-right.active .switch-icon-a,.switch-icon-slide-end.active .switch-icon-a,.switch-icon-slide-down.active .switch-icon-a{transform:translateY(-100%)}.switch-icon-slide-up.active .switch-icon-b,.switch-icon-slide-left.active .switch-icon-b,.switch-icon-slide-start.active .switch-icon-b,.switch-icon-slide-right.active .switch-icon-b,.switch-icon-slide-end.active .switch-icon-b,.switch-icon-slide-down.active .switch-icon-b{transform:translateY(0)}.switch-icon-slide-left .switch-icon-a,.switch-icon-slide-start .switch-icon-a{transform:translate(0)}.switch-icon-slide-left .switch-icon-b,.switch-icon-slide-start .switch-icon-b{transform:translate(100%)}.switch-icon-slide-left.active .switch-icon-a,.switch-icon-slide-start.active .switch-icon-a{transform:translate(-100%)}.switch-icon-slide-left.active .switch-icon-b,.switch-icon-slide-start.active .switch-icon-b,.switch-icon-slide-right .switch-icon-a,.switch-icon-slide-end .switch-icon-a{transform:translate(0)}.switch-icon-slide-right .switch-icon-b,.switch-icon-slide-end .switch-icon-b{transform:translate(-100%)}.switch-icon-slide-right.active .switch-icon-a,.switch-icon-slide-end.active .switch-icon-a{transform:translate(100%)}.switch-icon-slide-right.active .switch-icon-b,.switch-icon-slide-end.active .switch-icon-b{transform:translate(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)}.table thead th,.markdown>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{.table thead th,.markdown>table thead th{background:transparent}}.table-responsive .table,.table-responsive .markdown>table{margin-bottom:0}.table-responsive+.card-footer{border-top:0}.table-transparent thead th{background:transparent}.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:.5rem!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:.5rem!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:.5rem!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:.5rem!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:.5rem!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:.5rem!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}@media(prefers-reduced-motion:reduce){.table-sort{transition:none}}.table-sort{margin:-.5rem;padding:.5rem}.table-sort:hover,.table-sort.asc,.table-sort.desc{color:var(--tblr-body-color)}.table-sort:after{content:"";display:inline-flex;width:1rem;height:1rem;vertical-align:bottom;mask-image:url("data:image/svg+xml,");background:currentColor;margin-left:.25rem}.table-sort.asc:after{mask-image:url("data:image/svg+xml,")}.table-sort.desc:after{mask-image:url("data:image/svg+xml,")}.table-borderless thead th{background:transparent}.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: .625rem;--tblr-badge-padding-x: .25rem;--tblr-badge-padding-y: .125rem;margin-right:-.25rem}.tag-avatar,.tag-flag,.tag-payment,.tag-icon,.tag-check{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: .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:#1f29370a 0 2px 4px}.toast .toast-header{user-select:none}.toast button[data-bs-dismiss=toast]{outline:none}.toast-primary{--tblr-toast-color: #00857D}.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: .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-right:before,.hr-text.hr-text-end:before{content:""}.hr-text.hr-text-right:after,.hr-text.hr-text-end:after{content:none}.hr-text.hr-text-right>*:first-child,.hr-text.hr-text-end>*: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{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,.field-group h2 a,h4 a,h5 a,h6 a,.h1 a,.h2 a,.h3 a,.h4 a,.h5 a,.h6 a,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,.field-group h2,.field-group .h2,.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,.strong,b{font-weight:var(--tblr-font-weight-bold)}blockquote{padding: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:"\2014 "}ul,ol{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;transition:background .3s}@media(prefers-reduced-motion:reduce){pre::-webkit-scrollbar{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:transparent}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:transparent}pre code{background:transparent;padding:0}code{background:var(--tblr-code-bg);padding:2px 4px;border-radius:var(--tblr-border-radius)}abbr{text-decoration:underline dotted;cursor:help;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}::selection,.text-selected{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-avatar,.mention-app,.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:none;border-radius:0}.mention-count{color:var(--tblr-secondary);margin-left:.6666666667em}.text-incorrect{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);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 .field-group h2,.field-group .steps h2,.steps .field-group .h2,.field-group .steps .h2,.steps .h3{counter-increment:step}.steps h3:not(:first-child),.steps .field-group h2:not(:first-child),.field-group .steps h2:not(:first-child),.steps .field-group .h2:not(:first-child),.field-group .steps .h2:not(:first-child),.steps .h3:not(:first-child){margin-top:2.5rem!important}.steps h3:before,.steps .field-group h2:before,.field-group .steps h2:before,.steps .field-group .h2:before,.field-group .steps .h2: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;inset: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}.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;inset: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:not(caption),.visually-hidden-focusable:not(:focus):not(:focus-within):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:#f9fafb3d}.bg-dark-overlay{color:#fff;background-color:#1f29373d}.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:hover,.link-primary:focus{color:RGBA(0,106,100,var(--tblr-link-opacity, 1))!important;text-decoration-color:RGBA(0,106,100,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,.text-bg-gray{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:hover,.link-secondary:focus{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:hover,.link-success:focus{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:hover,.link-info:focus{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:hover,.link-warning:focus{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:hover,.link-danger:focus{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,.text-bg-white{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:hover,.link-light:focus{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,.text-bg-black{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:hover,.link-dark:focus{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:hover,.link-muted:focus{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:hover,.link-blue:focus{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:hover,.link-azure:focus{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:hover,.link-indigo:focus{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:hover,.link-purple:focus{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:hover,.link-pink:focus{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:hover,.link-red:focus{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:hover,.link-orange:focus{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:hover,.link-yellow:focus{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:hover,.link-lime:focus{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:hover,.link-green:focus{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:hover,.link-teal:focus{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:hover,.link-cyan:focus{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:hover,.link-white:focus{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:hover,.scrollable.hover:focus,.scrollable.hover:active{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:0!important}.opacity-5{opacity:.05!important}.opacity-10{opacity:.1!important}.opacity-15{opacity:.15!important}.opacity-20{opacity:.2!important}.opacity-25{opacity:.25!important}.opacity-30{opacity:.3!important}.opacity-35{opacity:calc(35 / 100)!important}.opacity-40{opacity:.4!important}.opacity-45{opacity:.45!important}.opacity-50{opacity:.5!important}.opacity-55{opacity:.55!important}.opacity-60{opacity:.6!important}.opacity-65{opacity:.65!important}.opacity-70{opacity:calc(70 / 100)!important}.opacity-75{opacity:.75!important}.opacity-80{opacity:.8!important}.opacity-85{opacity:.85!important}.opacity-90{opacity:.9!important}.opacity-95{opacity:calc(95 / 100)!important}.opacity-100{opacity:1!important}.hover-shadow-sm:hover{box-shadow:0 .125rem .25rem #00000013!important}.hover-shadow:hover{box-shadow:rgba(var(--tblr-body-color-rgb),.04) 0 2px 4px!important}.hover-shadow-lg:hover{box-shadow:0 1rem 3rem #0000002d!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-rotate-start,.hover-rotate-end,.hover-scale,.hover-elevate-down,.hover-elevate-up{transition:transform .3s ease}.hover-rotate-start:hover,.hover-rotate-end:hover,.hover-scale:hover,.hover-elevate-down:hover,.hover-elevate-up: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)}.ts-control{border:1px solid var(--tblr-border-color);padding:.5625rem 1rem;width:100%;overflow:hidden;position:relative;z-index:1;box-sizing:border-box;box-shadow:none;border-radius:var(--tblr-border-radius);display:flex;flex-wrap:wrap}.ts-wrapper.multi.has-items .ts-control{padding:calc(.5625rem - 1px) 1rem calc(.5625rem - 4px)}.full .ts-control{background-color:var(--tblr-bg-forms)}.disabled .ts-control,.disabled .ts-control *{cursor:default!important}.focus .ts-control{box-shadow:none}.ts-control>*{vertical-align:baseline;display:inline-block}.ts-wrapper.multi .ts-control>div{cursor:pointer;margin:0 3px 3px 0;padding:1px 5px;background:#efefef;color:#1f2937;border:0px solid #e5e7eb;overflow:auto}.ts-wrapper.multi .ts-control>div.active{background:#00857d;color:#fff;border:0px solid rgba(0,0,0,0)}.ts-wrapper.multi.disabled .ts-control>div,.ts-wrapper.multi.disabled .ts-control>div.active{color:#787878;background:#fff;border:0px solid hsl(0,0%,120.9803921569%)}.ts-control>input{flex:1 1 auto;min-width:7rem;display:inline-block!important;padding:0!important;min-height:0!important;max-height:none!important;max-width:100%!important;margin:0!important;text-indent:0!important;border:0 none!important;background:none!important;line-height:inherit!important;user-select:auto!important;box-shadow:none!important}.ts-control>input::-ms-clear{display:none}.ts-control>input:focus{outline:none!important}.has-items .ts-control>input{margin:0 4px!important}.ts-control.rtl{text-align:right}.ts-control.rtl.single .ts-control:after{left:calc(1rem + 5px);right:auto}.ts-control.rtl .ts-control>input{margin:0 4px 0 -2px!important}.disabled .ts-control{opacity:1;background-color:var(--tblr-bg-surface-secondary)}.input-hidden .ts-control>input{opacity:0;position:absolute;left:-10000px}.ts-dropdown{position:absolute;top:100%;left:0;width:100%;z-index:10;border:1px solid #d0d0d0;background:var(--tblr-bg-surface);margin:.25rem 0 0;border-top:0 none;box-sizing:border-box;box-shadow:0 1px 3px #0000001a;border-radius:0 0 var(--tblr-border-radius) var(--tblr-border-radius)}.ts-dropdown [data-selectable]{cursor:pointer;overflow:hidden}.ts-dropdown [data-selectable] .highlight{background:#ffed2866;border-radius:1px}.ts-dropdown .option,.ts-dropdown .optgroup-header,.ts-dropdown .no-results,.ts-dropdown .create{padding:3px 1rem}.ts-dropdown .option,.ts-dropdown [data-disabled],.ts-dropdown [data-disabled] [data-selectable].option{cursor:inherit;opacity:.5}.ts-dropdown [data-selectable].option{opacity:1;cursor:pointer}.ts-dropdown .optgroup:first-child .optgroup-header{border-top:0 none}.ts-dropdown .optgroup-header{color:#4b5563;background:var(--tblr-bg-surface);cursor:default}.ts-dropdown .active{background-color:rgba(var(--tblr-secondary-rgb),.08);color:inherit}.ts-dropdown .active.create{color:inherit}.ts-dropdown .create{color:#1f293780}.ts-dropdown .spinner{display:inline-block;width:30px;height:30px;margin:3px 1rem}.ts-dropdown .spinner:after{content:" ";display:block;width:24px;height:24px;margin:3px;border-radius:50%;border:5px solid #d0d0d0;border-color:#d0d0d0 transparent #d0d0d0 transparent;animation:lds-dual-ring 1.2s linear infinite}@keyframes lds-dual-ring{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.ts-dropdown-content{overflow:hidden auto;max-height:200px;scroll-behavior:smooth}.ts-wrapper.plugin-drag_drop .ts-dragging{color:transparent!important}.ts-wrapper.plugin-drag_drop .ts-dragging>*{visibility:hidden!important}.plugin-checkbox_options:not(.rtl) .option input{margin-right:.5rem}.plugin-checkbox_options.rtl .option input{margin-left:.5rem}.plugin-clear_button{--ts-pr-clear-button: 1em}.plugin-clear_button .clear-button{opacity:0;position:absolute;top:50%;transform:translateY(-50%);right:calc(1rem - 5px);margin-right:0!important;background:transparent!important;transition:opacity .5s;cursor:pointer}.plugin-clear_button.form-select .clear-button,.plugin-clear_button.single .clear-button{right:max(var(--ts-pr-caret),1rem)}.plugin-clear_button.focus.has-items .clear-button,.plugin-clear_button:not(.disabled):hover.has-items .clear-button{opacity:1}.ts-wrapper .dropdown-header{position:relative;padding:6px 1rem;border-bottom:1px solid #d0d0d0;background:color-mix(var(--tblr-bg-surface),#d0d0d0,85%);border-radius:var(--tblr-border-radius) var(--tblr-border-radius) 0 0}.ts-wrapper .dropdown-header-close{position:absolute;right:1rem;top:50%;color:#1f2937;opacity:.4;margin-top:-12px;line-height:20px;font-size:20px!important}.ts-wrapper .dropdown-header-close:hover{color:#000}.plugin-dropdown_input.focus.dropdown-active .ts-control{box-shadow:none;border:1px solid var(--tblr-border-color);box-shadow:var(--tblr-shadow-input)}.plugin-dropdown_input .dropdown-input{border:1px solid #d0d0d0;border-width:0 0 1px;display:block;padding:.5625rem 1rem;box-shadow:none;width:100%;background:transparent}.plugin-dropdown_input.focus .ts-dropdown .dropdown-input{border-color:#80c2be;outline:0;box-shadow:var(--tblr-shadow-input),0 0 0 .25rem rgba(var(--tblr-primary-rgb),.25)}.plugin-dropdown_input .items-placeholder{border:0 none!important;box-shadow:none!important;width:100%}.plugin-dropdown_input.has-items .items-placeholder,.plugin-dropdown_input.dropdown-active .items-placeholder{display:none!important}.ts-wrapper.plugin-input_autogrow.has-items .ts-control>input{min-width:0}.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control>input{flex:none;min-width:4px}.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control>input::placeholder{color:transparent}.ts-dropdown.plugin-optgroup_columns .ts-dropdown-content{display:flex}.ts-dropdown.plugin-optgroup_columns .optgroup{border-right:1px solid #f2f2f2;border-top:0 none;flex-grow:1;flex-basis:0;min-width:0}.ts-dropdown.plugin-optgroup_columns .optgroup:last-child{border-right:0 none}.ts-dropdown.plugin-optgroup_columns .optgroup:before{display:none}.ts-dropdown.plugin-optgroup_columns .optgroup-header{border-top:0 none}.ts-wrapper.plugin-remove_button .item{display:inline-flex;align-items:center}.ts-wrapper.plugin-remove_button .item .remove{color:inherit;text-decoration:none;vertical-align:middle;display:inline-block;padding:0 5px;border-radius:0 2px 2px 0;box-sizing:border-box}.ts-wrapper.plugin-remove_button .item .remove:hover{background:#0000000d}.ts-wrapper.plugin-remove_button.disabled .item .remove:hover{background:none}.ts-wrapper.plugin-remove_button .remove-single{position:absolute;right:0;top:0;font-size:23px}.ts-wrapper.plugin-remove_button:not(.rtl) .item{padding-right:0!important}.ts-wrapper.plugin-remove_button:not(.rtl) .item .remove{border-left:1px solid #e5e7eb;margin-left:5px}.ts-wrapper.plugin-remove_button:not(.rtl) .item.active .remove{border-left-color:#0000}.ts-wrapper.plugin-remove_button:not(.rtl).disabled .item .remove{border-left-color:#fff}.ts-wrapper.plugin-remove_button.rtl .item{padding-left:0!important}.ts-wrapper.plugin-remove_button.rtl .item .remove{border-right:1px solid #e5e7eb;margin-right:5px}.ts-wrapper.plugin-remove_button.rtl .item.active .remove{border-right-color:#0000}.ts-wrapper.plugin-remove_button.rtl.disabled .item .remove{border-right-color:#fff}:root{--ts-pr-clear-button: 0px;--ts-pr-caret: 0px;--ts-pr-min: .75rem}.ts-wrapper.single .ts-control,.ts-wrapper.single .ts-control input{cursor:pointer}.ts-control:not(.rtl){padding-right:max(var(--ts-pr-min),var(--ts-pr-clear-button) + var(--ts-pr-caret))!important}.ts-control.rtl{padding-left:max(var(--ts-pr-min),var(--ts-pr-clear-button) + var(--ts-pr-caret))!important}.ts-wrapper{position:relative}.ts-dropdown,.ts-control,.ts-control input{color:#1f2937;font-family:inherit;font-size:inherit;line-height:1.25rem}.ts-control,.ts-wrapper.single.input-active .ts-control{background:var(--tblr-bg-forms);cursor:text}.ts-hidden-accessible{border:0!important;clip:rect(0 0 0 0)!important;clip-path:inset(50%)!important;overflow:hidden!important;padding:0!important;position:absolute!important;width:1px!important;white-space:nowrap!important}.ts-dropdown,.ts-dropdown.form-control,.ts-dropdown.form-select{height:auto;padding:0;z-index:1000;background:var(--tblr-bg-surface);border:1px solid var(--tblr-border-color-translucent);border-radius:6px;box-shadow:0 6px 12px #0000002d}.ts-dropdown .optgroup-header{font-size:.765625rem;line-height:1.4285714286}.ts-dropdown .optgroup:first-child:before{display:none}.ts-dropdown .optgroup:before{content:" ";display:block;height:0;margin:var(--tblr-spacer-2) 0;overflow:hidden;border-top:1px solid var(--tblr-border-color-translucent);margin-left:-1rem;margin-right:-1rem}.ts-dropdown .create{padding-left:1rem}.ts-dropdown-content{padding:5px 0}.ts-control{display:flex;align-items:center;box-shadow:var(--tblr-shadow-input);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion:reduce){.ts-control{transition:none}}.focus .ts-control{border-color:#80c2be;outline:0;box-shadow:var(--tblr-shadow-input),0 0 0 .25rem rgba(var(--tblr-primary-rgb),.25)}.ts-control .item{display:flex;align-items:center}.ts-control input::placeholder{color:var(--bs-secondary-color, #a7aeb8);opacity:1}.ts-wrapper.is-invalid,select.tomselected.is-invalid+div.ts-wrapper,.was-validated .invalid,.was-validated :invalid+.ts-wrapper{border-color:var(--tblr-form-invalid-color)}.ts-wrapper.is-invalid:not(.single),select.tomselected.is-invalid+div.ts-wrapper:not(.single),.was-validated .invalid:not(.single),.was-validated :invalid+.ts-wrapper:not(.single){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-position:right 1.53125rem center;background-size:1.8125rem 1.8125rem;background-repeat:no-repeat}.ts-wrapper.is-invalid.single,select.tomselected.is-invalid+div.ts-wrapper.single,.was-validated .invalid.single,.was-validated :invalid+.ts-wrapper.single{background-image: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"),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-position:right 1rem center,center right 3rem;background-size:16px 12px,1.8125rem 1.8125rem;background-repeat:no-repeat}.ts-wrapper.is-invalid.focus .ts-control,select.tomselected.is-invalid+div.ts-wrapper.focus .ts-control,.was-validated .invalid.focus .ts-control,.was-validated :invalid+.ts-wrapper.focus .ts-control{border-color:var(--tblr-form-invalid-color);box-shadow:0 0 0 .25rem rgba(var(--tblr-form-invalid-color),.25)}.ts-wrapper.is-valid,.was-validated .valid,.was-validated :valid+.ts-wrapper{border-color:var(--tblr-form-valid-color)}.ts-wrapper.is-valid:not(.single),.was-validated .valid:not(.single),.was-validated :valid+.ts-wrapper:not(.single){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-position:right 1.53125rem center;background-size:1.8125rem 1.8125rem;background-repeat:no-repeat}.ts-wrapper.is-valid.single,.was-validated .valid.single,.was-validated :valid+.ts-wrapper.single{background-image: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"),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-position:right 1rem center,center right 3rem;background-size:16px 12px,1.8125rem 1.8125rem;background-repeat:no-repeat}.ts-wrapper.is-valid.focus .ts-control,.was-validated .valid.focus .ts-control,.was-validated :valid+.ts-wrapper.focus .ts-control{border-color:var(--tblr-form-valid-color);box-shadow:0 0 0 .25rem rgba(var(--tblr-form-valid-color),.25)}.ts-wrapper{min-height:calc(1.25rem + 1.125rem + calc(var(--tblr-border-width) * 2));display:flex}.input-group-sm>.ts-wrapper,.ts-wrapper.form-select-sm,.ts-wrapper.form-control-sm{min-height:calc(1.25rem + .625rem + calc(var(--tblr-border-width) * 2))}.input-group-sm>.ts-wrapper .ts-control,.ts-wrapper.form-select-sm .ts-control,.ts-wrapper.form-control-sm .ts-control{border-radius:var(--tblr-border-radius-sm);font-size:.75rem}.input-group-sm>.ts-wrapper.has-items .ts-control,.ts-wrapper.form-select-sm.has-items .ts-control,.ts-wrapper.form-control-sm.has-items .ts-control{font-size:.75rem}.input-group-sm>.ts-wrapper.multi.has-items .ts-control,.ts-wrapper.form-select-sm.multi.has-items .ts-control,.ts-wrapper.form-control-sm.multi.has-items .ts-control{padding-top:calc((calc(1.25rem + .625rem + calc(var(--tblr-border-width) * 2)) - 1.25rem * .75rem - calc((var(--tblr-border-width) + 1px) * 2)) / 2)!important}.ts-wrapper.multi.has-items .ts-control{padding-left:calc(1rem - 5px);--ts-pr-min: calc(1rem - 5px) }.ts-wrapper.multi .ts-control>div{border-radius:calc(var(--tblr-border-radius) - 1px)}.input-group-lg>.ts-wrapper,.ts-wrapper.form-control-lg,.ts-wrapper.form-select-lg{min-height:calc(1.25rem + 1.375rem + calc(var(--tblr-border-width) * 2))}.input-group-lg>.ts-wrapper .ts-control,.ts-wrapper.form-control-lg .ts-control,.ts-wrapper.form-select-lg .ts-control{border-radius:var(--tblr-border-radius-lg);font-size:1rem}.ts-wrapper:not(.form-control,.form-select){padding:0;border:none;height:auto;box-shadow:none;background:none}.ts-wrapper:not(.form-control,.form-select).single .ts-control{background-image: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");background-repeat:no-repeat;background-position:right 1rem center;background-size:16px 12px}.ts-wrapper.form-select,.ts-wrapper.single{--ts-pr-caret: 3rem}.ts-wrapper.form-control,.ts-wrapper.form-select{padding:0!important;height:auto;box-shadow:none;display:flex}.ts-wrapper.form-control .ts-control,.ts-wrapper.form-control.single.input-active .ts-control,.ts-wrapper.form-select .ts-control,.ts-wrapper.form-select.single.input-active .ts-control{border:none!important}.ts-wrapper.form-control:not(.disabled) .ts-control,.ts-wrapper.form-control:not(.disabled).single.input-active .ts-control,.ts-wrapper.form-select:not(.disabled) .ts-control,.ts-wrapper.form-select:not(.disabled).single.input-active .ts-control{background:transparent!important}.input-group>.ts-wrapper{flex-grow:1;width:1%}.input-group>.ts-wrapper:not(:nth-child(2))>.ts-control{border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.ts-wrapper:not(:last-child)>.ts-control{border-top-right-radius:0;border-bottom-right-radius:0}.form-select .ts-dropdown,.form-select .ts-control,.form-select .ts-control input{color:var(--tblr-body-color)}:root{--ts-pr-clear-button: 0rem;--ts-pr-caret: 0rem}.ts-input{color:inherit}.focus .ts-control{border-radius:var(--tblr-border-radius)}.ts-control{color:inherit}.ts-control .dropdown-menu{width:100%;height:auto}.ts-wrapper .form-control,.ts-wrapper .form-select,.ts-wrapper.form-control,.ts-wrapper.form-select{box-shadow:var(--tblr-shadow-input)}.ts-wrapper.is-invalid .ts-control,select.tomselected.is-invalid+div.ts-wrapper .ts-control,.ts-wrapper.is-valid .ts-control{--ts-pr-clear-button: 1.5rem}.ts-dropdown{background:var(--tblr-bg-surface);color:var(--tblr-body-color);box-shadow:var(--tblr-shadow-dropdown);z-index:1000}.ts-dropdown .option{padding:.5rem .75rem}.ts-control,.ts-control input{color:var(--tblr-body-color)}.ts-control input::placeholder{color:var(--tblr-tertiary)}.ts-wrapper.multi .ts-control>div,.ts-wrapper.multi.disabled .ts-control>div{background:var(--tblr-bg-surface-secondary);border:1px solid var(--tblr-border-color);color:var(--tblr-body-color)}.ts-wrapper.disabled .ts-control{opacity:1}.ts-wrapper.disabled .ts-control>div.item{color:var(--tblr-gray-500)}html{scroll-behavior:auto!important}mark,.mark{padding-left:0;padding-right:0}.table-responsive .dropdown,.table-responsive .btn-group,.table-responsive .btn-group-vertical{position:static}.progress{min-width:80px}hr.dropdown-divider,.dropdown-divider.hr{margin-bottom:.25rem;margin-top:.25rem}.dropdown-item{font-weight:400}*{font-feature-settings:"liga" 0;font-variant-ligatures:none}@media(min-width:992px){:root,:host{margin-left:0;scrollbar-gutter:stable}}pre{background-color:transparent;color:inherit}.alert{background:var(--tblr-bg-surface)}.badge{user-select:text}.btn{display:inline-block}.btn:focus{border:1px solid var(--tblr-primary-fg);outline:2px solid var(--tblr-primary)!important}.btn-sm,.btn-group-sm>.btn{border-radius:6px}.dropdown-item{display:inline-block}.footer .text-primary{color:#001423!important}.nav-tabs .nav-link{display:inline-block}.page,.page-tabs .nav-tabs .nav-link.active{background-color:var(--tblr-bg-surface-tertiary)!important}.navbar{--tblr-navbar-active-bg: rgba(0, 0, 0, .06)}[data-bs-theme=dark],body[data-bs-theme=dark] [data-bs-theme=light]{--tblr-alert-color: darken(var(--tblr-warning), 10%);--tblr-link-color: #00F2D4;--tblr-link-color-rgb: 0, 242, 212;--tblr-link-hover-color-rgb: 0, 242, 212;--tblr-secondary: #9ca3af;--tblr-primary: #00F2D4;--tblr-primary-fg: #001423;--tblr-primary-rgb: 0, 242, 212;--tblr-btn-active-color: #001423}html[data-bs-theme=dark] ::selection,body[data-bs-theme=dark] html[data-bs-theme=light] ::selection{background-color:rgba(var(--tblr-primary-rgb),.48)}html[data-bs-theme=dark] .btn-primary,body[data-bs-theme=dark] html[data-bs-theme=light] .btn-primary,html[data-bs-theme=dark] .bg-primary .card-title,body[data-bs-theme=dark] html[data-bs-theme=light] .bg-primary .card-title,html[data-bs-theme=dark] .bg-primary a,body[data-bs-theme=dark] html[data-bs-theme=light] .bg-primary a,html[data-bs-theme=dark] .bg-primary i,body[data-bs-theme=dark] html[data-bs-theme=light] .bg-primary i,html[data-bs-theme=dark] .text-bg-primary,body[data-bs-theme=dark] html[data-bs-theme=light] .text-bg-primary{color:#001423!important}html[data-bs-theme=dark] .card,body[data-bs-theme=dark] html[data-bs-theme=light] .card{background:#001423!important}html[data-bs-theme=dark],body[data-bs-theme=dark] html[data-bs-theme=light],html[data-bs-theme=dark] .navbar,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar,html[data-bs-theme=dark] .page-header,body[data-bs-theme=dark] html[data-bs-theme=light] .page-header{background-color:#001423}html[data-bs-theme=dark] .page,body[data-bs-theme=dark] html[data-bs-theme=light] .page,html[data-bs-theme=dark] .page-tabs .nav-tabs .nav-link.active,body[data-bs-theme=dark] html[data-bs-theme=light] .page-tabs .nav-tabs .nav-link.active{background-color:#081b2a!important}html[data-bs-theme=dark] .page-link.active,body[data-bs-theme=dark] html[data-bs-theme=light] .page-link.active,html[data-bs-theme=dark] .active>.page-link,body[data-bs-theme=dark] html[data-bs-theme=light] .active>.page-link{color:#001423}html[data-bs-theme=dark] .text-bg-primary,body[data-bs-theme=dark] html[data-bs-theme=light] .text-bg-primary{color:#001423!important}html[data-bs-theme=dark] .text-muted,body[data-bs-theme=dark] html[data-bs-theme=light] .text-muted{color:var(--tblr-secondary-color)!important}html[data-bs-theme=dark] .text-secondary,body[data-bs-theme=dark] html[data-bs-theme=light] .text-secondary{color:#9ca3af!important}html[data-bs-theme=dark] .footer .text-primary,body[data-bs-theme=dark] html[data-bs-theme=light] .footer .text-primary{color:#fff!important}html[data-bs-theme=dark] .toast,body[data-bs-theme=dark] html[data-bs-theme=light] .toast{color:var(--tblr-body-color)}html[data-bs-theme=dark] .table-primary,body[data-bs-theme=dark] html[data-bs-theme=light] .table-primary{--tblr-table-bg: rgba(var(--tblr-secondary-rgb), .48);--tblr-table-hover-bg: inherit;--tblr-table-hover-color: inherit}pre code{padding:unset}.dropdown-toggle:after{font-family:Material Design Icons;content:"\f0140";padding-right:9px;border-bottom:none;border-left:none;transform:none;vertical-align:.05em;height:auto}.accordion-button-toggle{align-items:center;justify-content:center;flex:0 0 auto}.accordion-button-toggle>i{display:block;line-height:1}:root:not(.dummy)[data-bs-theme=light] .hide-theme-light,:root:not(.dummy)[data-bs-theme=dark] .hide-theme-dark,body[data-bs-theme=dark] [data-bs-theme=light]:root:not(.dummy) .hide-theme-dark{display:none!important}:root:not(.dummy)[data-bs-theme=dark] .hide-theme-light,body[data-bs-theme=dark] [data-bs-theme=light]:root:not(.dummy) .hide-theme-light,:root:not(.dummy)[data-bs-theme=light] .hide-theme-dark{display:inline-flex!important}.ts-wrapper.multi .ts-control{padding:7px 7px 3px}.ts-wrapper.multi .ts-control div{margin:0 4px 4px 0}.badge a{color:inherit;text-decoration:none}.page-body .card{margin-bottom:1rem}.page-body .card .card-header,.page-body .card .card-body,.page-body .card .card-footer{padding:.75rem}.page-body .card .card-header{background:var(--tblr-bg-surface-tertiary)}.page-body .card h2.card-header,.page-body .card .card-header.h2{font-size:var(--tblr-font-size-h5);line-height:var(--tblr-line-height-h5);margin-bottom:0}.page-body .card .list-group-item{padding:.5rem .75rem}.page-body .card .table,.page-body .card .markdown>table{margin-bottom:0}form.object-edit{margin:auto;max-width:800px}.col-form-label.required{font-weight:700}.col-form-label.required:after{position:absolute;display:inline-block;margin-left:0;font-family:Material Design Icons;font-size:8px;content:"\f06c4"}.has-errors input,.has-errors select,.has-errors textarea{border:1px solid #d63939}select[multiple] optgroup{top:0;background-color:var(--bs-body-bg);font-style:normal;font-weight:700}select[multiple] option{padding-left:.5rem}.modifier-select{min-width:10rem;max-width:15rem;width:auto;white-space:nowrap}.page{background-color:var(--tblr-bg-surface-secondary)}.page-header{background-color:var(--tblr-bg-surface);min-height:0}.navbar-vertical.navbar-expand-lg .navbar-collapse .nav-link-icon,.navbar-vertical.navbar-expand-lg .navbar-collapse .nav-link-title{color:#001423}.navbar-vertical.navbar-expand-lg .navbar-collapse .text-secondary{color:#00857d!important}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item a{color:#001423}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item .btn-group{visibility:hidden}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item:hover,.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item.active{background-color:var(--tblr-navbar-active-bg)}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item:hover a,.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item.active a{text-decoration:none}.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item:hover .btn-group,.navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item.active .btn-group{visibility:visible}.navbar-vertical.navbar-expand-lg .navbar-nav{z-index:1}@media(max-width:991.98px){.navbar-vertical.navbar-expand-lg .navbar-brand{padding:.2rem 0}}.navbar-vertical.navbar-expand-lg .navbar-brand a:hover{text-decoration:none}.navbar-vertical.navbar-expand-lg img.motif{bottom:0;display:none;left:0;mask-image:linear-gradient(180deg,#0000,#0000004d);opacity:.5;position:fixed;user-drag:none;user-select:none;-moz-user-select:none;-webkit-user-drag:none;-webkit-user-select:none;-ms-user-select:none;width:18rem}@media(min-width:992px){.navbar-vertical.navbar-expand-lg img.motif{display:block}}html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg{background:linear-gradient(180deg,#00857d00,#00857d1a),#fff}html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg{background:linear-gradient(180deg,#00f2d400,#00f2d41a),#001423}html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg .nav-item.dropdown.active:after,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg .nav-item.dropdown.active:after{border-color:#00f2d4!important}html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg .nav-link-title,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg .nav-link-title,html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg .nav-link-icon,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg .nav-link-icon,html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg .dropdown-item a,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg .dropdown-item a{color:#fff!important}html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg .dropdown-item.active,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg .dropdown-item.active,html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg .dropdown-item:hover,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg .dropdown-item:hover{background-color:#ffffff0f!important}html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg .text-secondary,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg .text-secondary{color:#00f2d4!important}html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg img.motif,body[data-bs-theme=dark] html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg img.motif{opacity:.25}.progress{height:20px}.progress .progress-label{display:flex;flex-direction:column;justify-content:center;padding-left:.25rem}.table thead th,.markdown>table thead th{font-size:.625rem}table.object-list tbody>tr:last-child>td{border-bottom-width:0}table.object-list th.asc>a:after{content:"\f0140";font-family:Material Design Icons}table.object-list th.desc>a:after{content:"\f0143";font-family:Material Design Icons}table.attr-table th{font-weight:400;width:min-content}table.attr-table th,table.attr-table td{border-bottom-style:dashed}table.attr-table tr:last-child{border-bottom-style:hidden}table.attr-table td{overflow-wrap:anywhere}td pre{margin-bottom:0}table th.orderable a{color:var(--tblr-body-color)}html[data-bs-theme=dark] .table thead th,body[data-bs-theme=dark] html[data-bs-theme=light] .table thead th,body[data-bs-theme=dark] html[data-bs-theme=light] .markdown>table thead th,html[data-bs-theme=dark] .markdown>table thead th{background:#001423!important}.page-tabs{border-bottom:1px solid var(--tblr-border-color-translucent)}.page-tabs .nav-tabs{position:relative;border:none}.page-tabs .nav-tabs .nav-link.active,.page-tabs .nav-tabs .nav-link:active,.page-tabs .nav-tabs .nav-link:hover{border-color:var(--tblr-border-color-translucent);border-bottom-color:transparent}.page-tabs .nav-tabs .nav-link.active{color:inherit;background:var(--tblr-bg-surface-secondary);border-bottom-color:transparent}pre.change-data{border-radius:0;padding:0;margin-inline:-.75rem}pre.change-data>span{display:block;padding-inline:.5rem;max-width:100%;min-width:fit-content;border-left:.25rem solid transparent}pre.change-data>span.added{background-color:var(--tblr-green-200);border-left-color:var(--tblr-green-darken)}pre.change-data>span.removed{background-color:var(--tblr-red-200);border-left-color:var(--tblr-red-darken)}pre.change-diff{border:var(--tblr-border-width) solid transparent}pre.change-diff.change-added{background-color:var(--tblr-green-lt);border-color:var(--tblr-green)}pre.change-diff.change-removed{background-color:var(--tblr-red-lt);border-color:var(--tblr-red)}pre.block{padding:1rem;border:var(--tblr-border-width) solid #e5e7eb;border-radius:6px}.grid-stack .card-header.bg-default{background:var(--tblr-bg-surface-secondary)!important}.grid-stack .card-header a{color:inherit!important}tr[data-cable-status=connected]{--tblr-table-bg-state: rgba(47, 179, 68, .15)}tr[data-cable-status=planned]{--tblr-table-bg-state: rgba(6, 111, 209, .15)}tr[data-cable-status=decommissioning]{--tblr-table-bg-state: rgba(245, 159, 0, .15)}tr[data-mark-connected=true]{--tblr-table-bg-state: rgba(47, 179, 68, .15)}tr[data-virtual=true]{--tblr-table-bg-state: rgba(0, 133, 125, .15)}tr[data-enabled=disabled]{--tblr-table-bg-state: rgba(156, 163, 175, .15)}tr[data-cable-status]>*,tr[data-mark-connected=true]>*,tr[data-virtual=true]>*,tr[data-enabled=disabled]>*{background-color:var(--tblr-table-bg-state);border-bottom-color:var(--tblr-gray-300);box-shadow:none}tr[data-cable-status=connected] button.mark-installed{display:none}tr:not([data-cable-status=connected]) button.mark-planned{display:none}.rendered-markdown table{width:100%}.rendered-markdown table th{border-bottom:2px solid #dddddd;padding:8px}.rendered-markdown table td{border-top:1px solid #dddddd;padding:8px}.rendered-markdown table th[align=left]{text-align:left}.rendered-markdown table th[align=center]{text-align:center}.rendered-markdown table th[align=right]{text-align:right}.rendered-markdown p:last-child{margin-bottom:0}td>.rendered-markdown{max-height:200px;overflow-y:scroll}.markdown-widget .preview{border:1px solid #e5e7eb;border-radius:6px;min-height:200px}span.color-label{display:inline-block;width:5rem;height:1rem;padding:.25em .5em;border:1px solid #303030;border-radius:6px}.record-depth{display:inline;user-select:none;opacity:33%}.record-depth span:only-of-type,.record-depth span:last-of-type{margin-right:.25rem}.hide-last-child :last-child{visibility:hidden;opacity:0}.netbox-edition{letter-spacing:.15rem}.logo{height:80px}.sso-icon{height:24px}img.plugin-icon{max-width:1.4285em;height:auto}.thumbnail{max-width:200px}.thumbnail img{border:1px solid #606060}.image-preview-popover{--bs-popover-max-width: clamp(240px, 25vw, 640px)}.image-preview-popover .popover-header{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.image-preview-popover .popover-body{display:flex;justify-content:center;align-items:center}.image-preview-popover img{display:block;max-width:100%;max-height:clamp(160px,33vh,640px);height:auto}html[data-bs-theme=dark] img.plugin-icon,body[data-bs-theme=dark] html[data-bs-theme=light] img.plugin-icon{filter:grayscale(100%) invert(100%) brightness(80%)}.sticky-actions{z-index:4}.sticky-actions[data-sticky-when=always],.sticky-actions.is-sticky-active{position:sticky;bottom:.25rem}.sticky-actions[data-sticky-position=left],.sticky-actions[data-sticky-position=right]{display:flex;align-items:center;gap:.5rem;padding:.5rem;max-width:100%;width:fit-content}.sticky-actions[data-sticky-position=right]{margin-left:auto}.sticky-actions[data-sticky-position=full]{max-width:100%}.sticky-actions-footer{max-width:100%;margin-top:1.5rem;padding:1rem;padding-bottom:calc(1rem + env(safe-area-inset-bottom,0));background:var(--tblr-bg-surface-secondary);border-top:1px solid var(--tblr-border-color)}.sticky-actions-footer[data-sticky-when=always],.sticky-actions-footer.is-sticky-active{bottom:0}.sticky-actions-footer>.btn-list{justify-content:flex-end;margin-bottom:0}.object-edit--with-sticky-actions{padding-bottom:calc(1.5rem + env(safe-area-inset-bottom,0))}[data-sticky-when=selection] .bulk-action-buttons .btn:disabled,[data-sticky-when=selection] .bulk-action-buttons .btn.disabled{border-color:transparent;box-shadow:none}[data-sticky-when=selection] .bulk-action-buttons .btn.disabled{pointer-events:none}.btn-float-group-right{position:sticky;bottom:10px;z-index:4;margin-left:auto;width:fit-content}.btn-float-group-left{position:sticky;bottom:10px;z-index:4;width:fit-content}tr[data-read=True] td{background-color:var(--tblr-bg-surface-secondary);color:#6b7280}.rack-loading-container{min-height:200px;margin-left:30px} diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 2dac95f44..001c84bb1 100644 --- a/netbox/project-static/dist/netbox.js +++ b/netbox/project-static/dist/netbox.js @@ -1,11 +1,11 @@ -"use strict";(()=>{var au=Object.create;var Ni=Object.defineProperty,lu=Object.defineProperties,cu=Object.getOwnPropertyDescriptor,uu=Object.getOwnPropertyDescriptors,du=Object.getOwnPropertyNames,vs=Object.getOwnPropertySymbols,fu=Object.getPrototypeOf,Es=Object.prototype.hasOwnProperty,hu=Object.prototype.propertyIsEnumerable;var Gr=(n,e,t)=>e in n?Ni(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t,O=(n,e)=>{for(var t in e||(e={}))Es.call(e,t)&&Gr(n,t,e[t]);if(vs)for(var t of vs(e))hu.call(e,t)&&Gr(n,t,e[t]);return n},ae=(n,e)=>lu(n,uu(e));var pu=(n,e)=>()=>(e||n((e={exports:{}}).exports,e),e.exports),ys=(n,e)=>{for(var t in e)Ni(n,t,{get:e[t],enumerable:!0})},mu=(n,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of du(e))!Es.call(n,r)&&r!==t&&Ni(n,r,{get:()=>e[r],enumerable:!(i=cu(e,r))||i.enumerable});return n};var gu=(n,e,t)=>(t=n!=null?au(fu(n)):{},mu(e||!n||!n.__esModule?Ni(t,"default",{value:n,enumerable:!0}):t,n));var te=(n,e,t)=>Gr(n,typeof e!="symbol"?e+"":e,t);var at=(n,e,t)=>new Promise((i,r)=>{var o=l=>{try{a(t.next(l))}catch(c){r(c)}},s=l=>{try{a(t.throw(l))}catch(c){r(c)}},a=l=>l.done?i(l.value):Promise.resolve(l.value).then(o,s);a((t=t.apply(n,e)).next())});var wc=pu((Ei,rs)=>{(function(e,t){typeof Ei=="object"&&typeof rs=="object"?rs.exports=t():typeof define=="function"&&define.amd?define([],t):typeof Ei=="object"?Ei.ClipboardJS=t():e.ClipboardJS=t()})(Ei,function(){return(function(){var n={686:(function(i,r,o){"use strict";o.d(r,{default:function(){return Re}});var s=o(279),a=o.n(s),l=o(370),c=o.n(l),u=o(817),d=o.n(u);function p(q){try{return document.execCommand(q)}catch(M){return!1}}var E=function(M){var A=d()(M);return p("cut"),A},m=E;function v(q){var M=document.documentElement.getAttribute("dir")==="rtl",A=document.createElement("textarea");A.style.fontSize="12pt",A.style.border="0",A.style.padding="0",A.style.margin="0",A.style.position="absolute",A.style[M?"right":"left"]="-9999px";var $=window.pageYOffset||document.documentElement.scrollTop;return A.style.top="".concat($,"px"),A.setAttribute("readonly",""),A.value=q,A}var _=function(M,A){var $=v(M);A.container.appendChild($);var B=d()($);return p("copy"),$.remove(),B},C=function(M){var A=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},$="";return typeof M=="string"?$=_(M,A):M instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(M==null?void 0:M.type)?$=_(M.value,A):($=d()(M),p("copy")),$},D=C;function w(q){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?w=function(A){return typeof A}:w=function(A){return A&&typeof Symbol=="function"&&A.constructor===Symbol&&A!==Symbol.prototype?"symbol":typeof A},w(q)}var T=function(){var M=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},A=M.action,$=A===void 0?"copy":A,B=M.container,W=M.target,U=M.text;if($!=="copy"&&$!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(W!==void 0)if(W&&w(W)==="object"&&W.nodeType===1){if($==="copy"&&W.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if($==="cut"&&(W.hasAttribute("readonly")||W.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(U)return D(U,{container:B});if(W)return $==="cut"?m(W):D(W,{container:B})},K=T;function z(q){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?z=function(A){return typeof A}:z=function(A){return A&&typeof Symbol=="function"&&A.constructor===Symbol&&A!==Symbol.prototype?"symbol":typeof A},z(q)}function H(q,M){if(!(q instanceof M))throw new TypeError("Cannot call a class as a function")}function I(q,M){for(var A=0;A0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof B.action=="function"?B.action:this.defaultAction,this.target=typeof B.target=="function"?B.target:this.defaultTarget,this.text=typeof B.text=="function"?B.text:this.defaultText,this.container=z(B.container)==="object"?B.container:document.body}},{key:"listenClick",value:function(B){var W=this;this.listener=c()(B,"click",function(U){return W.onClick(U)})}},{key:"onClick",value:function(B){var W=B.delegateTarget||B.currentTarget,U=this.action(W)||"copy",ee=K({action:U,container:this.container,target:this.target(W),text:this.text(W)});this.emit(ee?"success":"error",{action:U,text:ee,trigger:W,clearSelection:function(){W&&W.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(B){return ie("action",B)}},{key:"defaultTarget",value:function(B){var W=ie("target",B);if(W)return document.querySelector(W)}},{key:"defaultText",value:function(B){return ie("text",B)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(B){var W=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return D(B,W)}},{key:"cut",value:function(B){return m(B)}},{key:"isSupported",value:function(){var B=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],W=typeof B=="string"?[B]:B,U=!!document.queryCommandSupported;return W.forEach(function(ee){U=U&&!!document.queryCommandSupported(ee)}),U}}]),A})(a()),Re=Ue}),828:(function(i){var r=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var o=Element.prototype;o.matches=o.matchesSelector||o.mozMatchesSelector||o.msMatchesSelector||o.oMatchesSelector||o.webkitMatchesSelector}function s(a,l){for(;a&&a.nodeType!==r;){if(typeof a.matches=="function"&&a.matches(l))return a;a=a.parentNode}}i.exports=s}),438:(function(i,r,o){var s=o(828);function a(u,d,p,E,m){var v=c.apply(this,arguments);return u.addEventListener(p,v,m),{destroy:function(){u.removeEventListener(p,v,m)}}}function l(u,d,p,E,m){return typeof u.addEventListener=="function"?a.apply(null,arguments):typeof p=="function"?a.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return a(v,d,p,E,m)}))}function c(u,d,p,E){return function(m){m.delegateTarget=s(m.target,d),m.delegateTarget&&E.call(u,m)}}i.exports=l}),879:(function(i,r){r.node=function(o){return o!==void 0&&o instanceof HTMLElement&&o.nodeType===1},r.nodeList=function(o){var s=Object.prototype.toString.call(o);return o!==void 0&&(s==="[object NodeList]"||s==="[object HTMLCollection]")&&"length"in o&&(o.length===0||r.node(o[0]))},r.string=function(o){return typeof o=="string"||o instanceof String},r.fn=function(o){var s=Object.prototype.toString.call(o);return s==="[object Function]"}}),370:(function(i,r,o){var s=o(879),a=o(438);function l(p,E,m){if(!p&&!E&&!m)throw new Error("Missing required arguments");if(!s.string(E))throw new TypeError("Second argument must be a String");if(!s.fn(m))throw new TypeError("Third argument must be a Function");if(s.node(p))return c(p,E,m);if(s.nodeList(p))return u(p,E,m);if(s.string(p))return d(p,E,m);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(p,E,m){return p.addEventListener(E,m),{destroy:function(){p.removeEventListener(E,m)}}}function u(p,E,m){return Array.prototype.forEach.call(p,function(v){v.addEventListener(E,m)}),{destroy:function(){Array.prototype.forEach.call(p,function(v){v.removeEventListener(E,m)})}}}function d(p,E,m){return a(document.body,p,E,m)}i.exports=l}),817:(function(i){function r(o){var s;if(o.nodeName==="SELECT")o.focus(),s=o.value;else if(o.nodeName==="INPUT"||o.nodeName==="TEXTAREA"){var a=o.hasAttribute("readonly");a||o.setAttribute("readonly",""),o.select(),o.setSelectionRange(0,o.value.length),a||o.removeAttribute("readonly"),s=o.value}else{o.hasAttribute("contenteditable")&&o.focus();var l=window.getSelection(),c=document.createRange();c.selectNodeContents(o),l.removeAllRanges(),l.addRange(c),s=l.toString()}return s}i.exports=r}),279:(function(i){function r(){}r.prototype={on:function(o,s,a){var l=this.e||(this.e={});return(l[o]||(l[o]=[])).push({fn:s,ctx:a}),this},once:function(o,s,a){var l=this;function c(){l.off(o,c),s.apply(a,arguments)}return c._=s,this.on(o,c,a)},emit:function(o){var s=[].slice.call(arguments,1),a=((this.e||(this.e={}))[o]||[]).slice(),l=0,c=a.length;for(l;lCs,afterRead:()=>ws,afterWrite:()=>Ds,applyStyles:()=>pn,arrow:()=>Hi,auto:()=>qn,basePlacements:()=>lt,beforeMain:()=>xs,beforeRead:()=>bs,beforeWrite:()=>Ss,bottom:()=>ge,clippingParents:()=>Kr,computeStyles:()=>gn,createPopper:()=>Zn,createPopperBase:()=>Ps,createPopperLite:()=>Fs,detectOverflow:()=>ke,end:()=>bt,eventListeners:()=>vn,flip:()=>Fi,hide:()=>$i,left:()=>pe,main:()=>Ts,modifierPhases:()=>Qr,offset:()=>Bi,placements:()=>Un,popper:()=>$t,popperGenerator:()=>Yt,popperOffsets:()=>bn,preventOverflow:()=>Vi,read:()=>_s,reference:()=>Xr,right:()=>me,start:()=>rt,top:()=>de,variationPlacements:()=>ki,viewport:()=>Wn,write:()=>As});var de="top",ge="bottom",me="right",pe="left",qn="auto",lt=[de,ge,me,pe],rt="start",bt="end",Kr="clippingParents",Wn="viewport",$t="popper",Xr="reference",ki=lt.reduce(function(n,e){return n.concat([e+"-"+rt,e+"-"+bt])},[]),Un=[].concat(lt,[qn]).reduce(function(n,e){return n.concat([e,e+"-"+rt,e+"-"+bt])},[]),bs="beforeRead",_s="read",ws="afterRead",xs="beforeMain",Ts="main",Cs="afterMain",Ss="beforeWrite",As="write",Ds="afterWrite",Qr=[bs,_s,ws,xs,Ts,Cs,Ss,As,Ds];function xe(n){return n?(n.nodeName||"").toLowerCase():null}function ce(n){if(n==null)return window;if(n.toString()!=="[object Window]"){var e=n.ownerDocument;return e&&e.defaultView||window}return n}function Ye(n){var e=ce(n).Element;return n instanceof e||n instanceof Element}function _e(n){var e=ce(n).HTMLElement;return n instanceof e||n instanceof HTMLElement}function hn(n){if(typeof ShadowRoot=="undefined")return!1;var e=ce(n).ShadowRoot;return n instanceof e||n instanceof ShadowRoot}function vu(n){var e=n.state;Object.keys(e.elements).forEach(function(t){var i=e.styles[t]||{},r=e.attributes[t]||{},o=e.elements[t];!_e(o)||!xe(o)||(Object.assign(o.style,i),Object.keys(r).forEach(function(s){var a=r[s];a===!1?o.removeAttribute(s):o.setAttribute(s,a===!0?"":a)}))})}function Eu(n){var e=n.state,t={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,t.popper),e.styles=t,e.elements.arrow&&Object.assign(e.elements.arrow.style,t.arrow),function(){Object.keys(e.elements).forEach(function(i){var r=e.elements[i],o=e.attributes[i]||{},s=Object.keys(e.styles.hasOwnProperty(i)?e.styles[i]:t[i]),a=s.reduce(function(l,c){return l[c]="",l},{});!_e(r)||!xe(r)||(Object.assign(r.style,a),Object.keys(o).forEach(function(l){r.removeAttribute(l)}))})}}var pn={name:"applyStyles",enabled:!0,phase:"write",fn:vu,effect:Eu,requires:["computeStyles"]};function Te(n){return n.split("-")[0]}var Ze=Math.max,Bt=Math.min,ct=Math.round;function mn(){var n=navigator.userAgentData;return n!=null&&n.brands&&Array.isArray(n.brands)?n.brands.map(function(e){return e.brand+"/"+e.version}).join(" "):navigator.userAgent}function Yn(){return!/^((?!chrome|android).)*safari/i.test(mn())}function Ge(n,e,t){e===void 0&&(e=!1),t===void 0&&(t=!1);var i=n.getBoundingClientRect(),r=1,o=1;e&&_e(n)&&(r=n.offsetWidth>0&&ct(i.width)/n.offsetWidth||1,o=n.offsetHeight>0&&ct(i.height)/n.offsetHeight||1);var s=Ye(n)?ce(n):window,a=s.visualViewport,l=!Yn()&&t,c=(i.left+(l&&a?a.offsetLeft:0))/r,u=(i.top+(l&&a?a.offsetTop:0))/o,d=i.width/r,p=i.height/o;return{width:d,height:p,top:u,right:c+d,bottom:u+p,left:c,x:c,y:u}}function Vt(n){var e=Ge(n),t=n.offsetWidth,i=n.offsetHeight;return Math.abs(e.width-t)<=1&&(t=e.width),Math.abs(e.height-i)<=1&&(i=e.height),{x:n.offsetLeft,y:n.offsetTop,width:t,height:i}}function Gn(n,e){var t=e.getRootNode&&e.getRootNode();if(n.contains(e))return!0;if(t&&hn(t)){var i=e;do{if(i&&n.isSameNode(i))return!0;i=i.parentNode||i.host}while(i)}return!1}function Ne(n){return ce(n).getComputedStyle(n)}function Jr(n){return["table","td","th"].indexOf(xe(n))>=0}function Se(n){return((Ye(n)?n.ownerDocument:n.document)||window.document).documentElement}function ut(n){return xe(n)==="html"?n:n.assignedSlot||n.parentNode||(hn(n)?n.host:null)||Se(n)}function Os(n){return!_e(n)||Ne(n).position==="fixed"?null:n.offsetParent}function yu(n){var e=/firefox/i.test(mn()),t=/Trident/i.test(mn());if(t&&_e(n)){var i=Ne(n);if(i.position==="fixed")return null}var r=ut(n);for(hn(r)&&(r=r.host);_e(r)&&["html","body"].indexOf(xe(r))<0;){var o=Ne(r);if(o.transform!=="none"||o.perspective!=="none"||o.contain==="paint"||["transform","perspective"].indexOf(o.willChange)!==-1||e&&o.willChange==="filter"||e&&o.filter&&o.filter!=="none")return r;r=r.parentNode}return null}function et(n){for(var e=ce(n),t=Os(n);t&&Jr(t)&&Ne(t).position==="static";)t=Os(t);return t&&(xe(t)==="html"||xe(t)==="body"&&Ne(t).position==="static")?e:t||yu(n)||e}function zt(n){return["top","bottom"].indexOf(n)>=0?"x":"y"}function jt(n,e,t){return Ze(n,Bt(e,t))}function Ls(n,e,t){var i=jt(n,e,t);return i>t?t:i}function Kn(){return{top:0,right:0,bottom:0,left:0}}function Xn(n){return Object.assign({},Kn(),n)}function Qn(n,e){return e.reduce(function(t,i){return t[i]=n,t},{})}var bu=function(e,t){return e=typeof e=="function"?e(Object.assign({},t.rects,{placement:t.placement})):e,Xn(typeof e!="number"?e:Qn(e,lt))};function _u(n){var e,t=n.state,i=n.name,r=n.options,o=t.elements.arrow,s=t.modifiersData.popperOffsets,a=Te(t.placement),l=zt(a),c=[pe,me].indexOf(a)>=0,u=c?"height":"width";if(!(!o||!s)){var d=bu(r.padding,t),p=Vt(o),E=l==="y"?de:pe,m=l==="y"?ge:me,v=t.rects.reference[u]+t.rects.reference[l]-s[l]-t.rects.popper[u],_=s[l]-t.rects.reference[l],C=et(o),D=C?l==="y"?C.clientHeight||0:C.clientWidth||0:0,w=v/2-_/2,T=d[E],K=D-p[u]-d[m],z=D/2-p[u]/2+w,H=jt(T,z,K),I=l;t.modifiersData[i]=(e={},e[I]=H,e.centerOffset=H-z,e)}}function wu(n){var e=n.state,t=n.options,i=t.element,r=i===void 0?"[data-popper-arrow]":i;r!=null&&(typeof r=="string"&&(r=e.elements.popper.querySelector(r),!r)||Gn(e.elements.popper,r)&&(e.elements.arrow=r))}var Hi={name:"arrow",enabled:!0,phase:"main",fn:_u,effect:wu,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Ke(n){return n.split("-")[1]}var xu={top:"auto",right:"auto",bottom:"auto",left:"auto"};function Tu(n,e){var t=n.x,i=n.y,r=e.devicePixelRatio||1;return{x:ct(t*r)/r||0,y:ct(i*r)/r||0}}function Ms(n){var e,t=n.popper,i=n.popperRect,r=n.placement,o=n.variation,s=n.offsets,a=n.position,l=n.gpuAcceleration,c=n.adaptive,u=n.roundOffsets,d=n.isFixed,p=s.x,E=p===void 0?0:p,m=s.y,v=m===void 0?0:m,_=typeof u=="function"?u({x:E,y:v}):{x:E,y:v};E=_.x,v=_.y;var C=s.hasOwnProperty("x"),D=s.hasOwnProperty("y"),w=pe,T=de,K=window;if(c){var z=et(t),H="clientHeight",I="clientWidth";if(z===ce(t)&&(z=Se(t),Ne(z).position!=="static"&&a==="absolute"&&(H="scrollHeight",I="scrollWidth")),z=z,r===de||(r===pe||r===me)&&o===bt){T=ge;var L=d&&z===K&&K.visualViewport?K.visualViewport.height:z[H];v-=L-i.height,v*=l?1:-1}if(r===pe||(r===de||r===ge)&&o===bt){w=me;var j=d&&z===K&&K.visualViewport?K.visualViewport.width:z[I];E-=j-i.width,E*=l?1:-1}}var Y=Object.assign({position:a},c&&xu),Q=u===!0?Tu({x:E,y:v},ce(t)):{x:E,y:v};if(E=Q.x,v=Q.y,l){var Z;return Object.assign({},Y,(Z={},Z[T]=D?"0":"",Z[w]=C?"0":"",Z.transform=(K.devicePixelRatio||1)<=1?"translate("+E+"px, "+v+"px)":"translate3d("+E+"px, "+v+"px, 0)",Z))}return Object.assign({},Y,(e={},e[T]=D?v+"px":"",e[w]=C?E+"px":"",e.transform="",e))}function Cu(n){var e=n.state,t=n.options,i=t.gpuAcceleration,r=i===void 0?!0:i,o=t.adaptive,s=o===void 0?!0:o,a=t.roundOffsets,l=a===void 0?!0:a,c={placement:Te(e.placement),variation:Ke(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:r,isFixed:e.options.strategy==="fixed"};e.modifiersData.popperOffsets!=null&&(e.styles.popper=Object.assign({},e.styles.popper,Ms(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:s,roundOffsets:l})))),e.modifiersData.arrow!=null&&(e.styles.arrow=Object.assign({},e.styles.arrow,Ms(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})}var gn={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:Cu,data:{}};var Ii={passive:!0};function Su(n){var e=n.state,t=n.instance,i=n.options,r=i.scroll,o=r===void 0?!0:r,s=i.resize,a=s===void 0?!0:s,l=ce(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach(function(u){u.addEventListener("scroll",t.update,Ii)}),a&&l.addEventListener("resize",t.update,Ii),function(){o&&c.forEach(function(u){u.removeEventListener("scroll",t.update,Ii)}),a&&l.removeEventListener("resize",t.update,Ii)}}var vn={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:Su,data:{}};var Au={left:"right",right:"left",bottom:"top",top:"bottom"};function En(n){return n.replace(/left|right|bottom|top/g,function(e){return Au[e]})}var Du={start:"end",end:"start"};function Ri(n){return n.replace(/start|end/g,function(e){return Du[e]})}function qt(n){var e=ce(n),t=e.pageXOffset,i=e.pageYOffset;return{scrollLeft:t,scrollTop:i}}function Wt(n){return Ge(Se(n)).left+qt(n).scrollLeft}function Zr(n,e){var t=ce(n),i=Se(n),r=t.visualViewport,o=i.clientWidth,s=i.clientHeight,a=0,l=0;if(r){o=r.width,s=r.height;var c=Yn();(c||!c&&e==="fixed")&&(a=r.offsetLeft,l=r.offsetTop)}return{width:o,height:s,x:a+Wt(n),y:l}}function eo(n){var e,t=Se(n),i=qt(n),r=(e=n.ownerDocument)==null?void 0:e.body,o=Ze(t.scrollWidth,t.clientWidth,r?r.scrollWidth:0,r?r.clientWidth:0),s=Ze(t.scrollHeight,t.clientHeight,r?r.scrollHeight:0,r?r.clientHeight:0),a=-i.scrollLeft+Wt(n),l=-i.scrollTop;return Ne(r||t).direction==="rtl"&&(a+=Ze(t.clientWidth,r?r.clientWidth:0)-o),{width:o,height:s,x:a,y:l}}function Ut(n){var e=Ne(n),t=e.overflow,i=e.overflowX,r=e.overflowY;return/auto|scroll|overlay|hidden/.test(t+r+i)}function Pi(n){return["html","body","#document"].indexOf(xe(n))>=0?n.ownerDocument.body:_e(n)&&Ut(n)?n:Pi(ut(n))}function _t(n,e){var t;e===void 0&&(e=[]);var i=Pi(n),r=i===((t=n.ownerDocument)==null?void 0:t.body),o=ce(i),s=r?[o].concat(o.visualViewport||[],Ut(i)?i:[]):i,a=e.concat(s);return r?a:a.concat(_t(ut(s)))}function yn(n){return Object.assign({},n,{left:n.x,top:n.y,right:n.x+n.width,bottom:n.y+n.height})}function Ou(n,e){var t=Ge(n,!1,e==="fixed");return t.top=t.top+n.clientTop,t.left=t.left+n.clientLeft,t.bottom=t.top+n.clientHeight,t.right=t.left+n.clientWidth,t.width=n.clientWidth,t.height=n.clientHeight,t.x=t.left,t.y=t.top,t}function Ns(n,e,t){return e===Wn?yn(Zr(n,t)):Ye(e)?Ou(e,t):yn(eo(Se(n)))}function Lu(n){var e=_t(ut(n)),t=["absolute","fixed"].indexOf(Ne(n).position)>=0,i=t&&_e(n)?et(n):n;return Ye(i)?e.filter(function(r){return Ye(r)&&Gn(r,i)&&xe(r)!=="body"}):[]}function to(n,e,t,i){var r=e==="clippingParents"?Lu(n):[].concat(e),o=[].concat(r,[t]),s=o[0],a=o.reduce(function(l,c){var u=Ns(n,c,i);return l.top=Ze(u.top,l.top),l.right=Bt(u.right,l.right),l.bottom=Bt(u.bottom,l.bottom),l.left=Ze(u.left,l.left),l},Ns(n,s,i));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}function Jn(n){var e=n.reference,t=n.element,i=n.placement,r=i?Te(i):null,o=i?Ke(i):null,s=e.x+e.width/2-t.width/2,a=e.y+e.height/2-t.height/2,l;switch(r){case de:l={x:s,y:e.y-t.height};break;case ge:l={x:s,y:e.y+e.height};break;case me:l={x:e.x+e.width,y:a};break;case pe:l={x:e.x-t.width,y:a};break;default:l={x:e.x,y:e.y}}var c=r?zt(r):null;if(c!=null){var u=c==="y"?"height":"width";switch(o){case rt:l[c]=l[c]-(e[u]/2-t[u]/2);break;case bt:l[c]=l[c]+(e[u]/2-t[u]/2);break;default:}}return l}function ke(n,e){e===void 0&&(e={});var t=e,i=t.placement,r=i===void 0?n.placement:i,o=t.strategy,s=o===void 0?n.strategy:o,a=t.boundary,l=a===void 0?Kr:a,c=t.rootBoundary,u=c===void 0?Wn:c,d=t.elementContext,p=d===void 0?$t:d,E=t.altBoundary,m=E===void 0?!1:E,v=t.padding,_=v===void 0?0:v,C=Xn(typeof _!="number"?_:Qn(_,lt)),D=p===$t?Xr:$t,w=n.rects.popper,T=n.elements[m?D:p],K=to(Ye(T)?T:T.contextElement||Se(n.elements.popper),l,u,s),z=Ge(n.elements.reference),H=Jn({reference:z,element:w,strategy:"absolute",placement:r}),I=yn(Object.assign({},w,H)),L=p===$t?I:z,j={top:K.top-L.top+C.top,bottom:L.bottom-K.bottom+C.bottom,left:K.left-L.left+C.left,right:L.right-K.right+C.right},Y=n.modifiersData.offset;if(p===$t&&Y){var Q=Y[r];Object.keys(j).forEach(function(Z){var he=[me,ge].indexOf(Z)>=0?1:-1,Ce=[de,ge].indexOf(Z)>=0?"y":"x";j[Z]+=Q[Ce]*he})}return j}function no(n,e){e===void 0&&(e={});var t=e,i=t.placement,r=t.boundary,o=t.rootBoundary,s=t.padding,a=t.flipVariations,l=t.allowedAutoPlacements,c=l===void 0?Un:l,u=Ke(i),d=u?a?ki:ki.filter(function(m){return Ke(m)===u}):lt,p=d.filter(function(m){return c.indexOf(m)>=0});p.length===0&&(p=d);var E=p.reduce(function(m,v){return m[v]=ke(n,{placement:v,boundary:r,rootBoundary:o,padding:s})[Te(v)],m},{});return Object.keys(E).sort(function(m,v){return E[m]-E[v]})}function Mu(n){if(Te(n)===qn)return[];var e=En(n);return[Ri(n),e,Ri(e)]}function Nu(n){var e=n.state,t=n.options,i=n.name;if(!e.modifiersData[i]._skip){for(var r=t.mainAxis,o=r===void 0?!0:r,s=t.altAxis,a=s===void 0?!0:s,l=t.fallbackPlacements,c=t.padding,u=t.boundary,d=t.rootBoundary,p=t.altBoundary,E=t.flipVariations,m=E===void 0?!0:E,v=t.allowedAutoPlacements,_=e.options.placement,C=Te(_),D=C===_,w=l||(D||!m?[En(_)]:Mu(_)),T=[_].concat(w).reduce(function(B,W){return B.concat(Te(W)===qn?no(e,{placement:W,boundary:u,rootBoundary:d,padding:c,flipVariations:m,allowedAutoPlacements:v}):W)},[]),K=e.rects.reference,z=e.rects.popper,H=new Map,I=!0,L=T[0],j=0;j=0,Ce=he?"width":"height",re=ke(e,{placement:Y,boundary:u,rootBoundary:d,altBoundary:p,padding:c}),ie=he?Z?me:pe:Z?ge:de;K[Ce]>z[Ce]&&(ie=En(ie));var Ue=En(ie),Re=[];if(o&&Re.push(re[Q]<=0),a&&Re.push(re[ie]<=0,re[Ue]<=0),Re.every(function(B){return B})){L=Y,I=!1;break}H.set(Y,Re)}if(I)for(var q=m?3:1,M=function(W){var U=T.find(function(ee){var se=H.get(ee);if(se)return se.slice(0,W).every(function(yt){return yt})});if(U)return L=U,"break"},A=q;A>0;A--){var $=M(A);if($==="break")break}e.placement!==L&&(e.modifiersData[i]._skip=!0,e.placement=L,e.reset=!0)}}var Fi={name:"flip",enabled:!0,phase:"main",fn:Nu,requiresIfExists:["offset"],data:{_skip:!1}};function ks(n,e,t){return t===void 0&&(t={x:0,y:0}),{top:n.top-e.height-t.y,right:n.right-e.width+t.x,bottom:n.bottom-e.height+t.y,left:n.left-e.width-t.x}}function Hs(n){return[de,me,ge,pe].some(function(e){return n[e]>=0})}function ku(n){var e=n.state,t=n.name,i=e.rects.reference,r=e.rects.popper,o=e.modifiersData.preventOverflow,s=ke(e,{elementContext:"reference"}),a=ke(e,{altBoundary:!0}),l=ks(s,i),c=ks(a,r,o),u=Hs(l),d=Hs(c);e.modifiersData[t]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:u,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":u,"data-popper-escaped":d})}var $i={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:ku};function Hu(n,e,t){var i=Te(n),r=[pe,de].indexOf(i)>=0?-1:1,o=typeof t=="function"?t(Object.assign({},e,{placement:n})):t,s=o[0],a=o[1];return s=s||0,a=(a||0)*r,[pe,me].indexOf(i)>=0?{x:a,y:s}:{x:s,y:a}}function Iu(n){var e=n.state,t=n.options,i=n.name,r=t.offset,o=r===void 0?[0,0]:r,s=Un.reduce(function(u,d){return u[d]=Hu(d,e.rects,o),u},{}),a=s[e.placement],l=a.x,c=a.y;e.modifiersData.popperOffsets!=null&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[i]=s}var Bi={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:Iu};function Ru(n){var e=n.state,t=n.name;e.modifiersData[t]=Jn({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})}var bn={name:"popperOffsets",enabled:!0,phase:"read",fn:Ru,data:{}};function io(n){return n==="x"?"y":"x"}function Pu(n){var e=n.state,t=n.options,i=n.name,r=t.mainAxis,o=r===void 0?!0:r,s=t.altAxis,a=s===void 0?!1:s,l=t.boundary,c=t.rootBoundary,u=t.altBoundary,d=t.padding,p=t.tether,E=p===void 0?!0:p,m=t.tetherOffset,v=m===void 0?0:m,_=ke(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:u}),C=Te(e.placement),D=Ke(e.placement),w=!D,T=zt(C),K=io(T),z=e.modifiersData.popperOffsets,H=e.rects.reference,I=e.rects.popper,L=typeof v=="function"?v(Object.assign({},e.rects,{placement:e.placement})):v,j=typeof L=="number"?{mainAxis:L,altAxis:L}:Object.assign({mainAxis:0,altAxis:0},L),Y=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,Q={x:0,y:0};if(z){if(o){var Z,he=T==="y"?de:pe,Ce=T==="y"?ge:me,re=T==="y"?"height":"width",ie=z[T],Ue=ie+_[he],Re=ie-_[Ce],q=E?-I[re]/2:0,M=D===rt?H[re]:I[re],A=D===rt?-I[re]:-H[re],$=e.elements.arrow,B=E&&$?Vt($):{width:0,height:0},W=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:Kn(),U=W[he],ee=W[Ce],se=jt(0,H[re],B[re]),yt=w?H[re]/2-q-se-U-j.mainAxis:M-se-U-j.mainAxis,zr=w?-H[re]/2+q+se+ee+j.mainAxis:A+se+ee+j.mainAxis,sn=e.elements.arrow&&et(e.elements.arrow),an=sn?T==="y"?sn.clientTop||0:sn.clientLeft||0:0,Ci=(Z=Y==null?void 0:Y[T])!=null?Z:0,jr=ie+yt-Ci-an,Si=ie+zr-Ci,Ai=jt(E?Bt(Ue,jr):Ue,ie,E?Ze(Re,Si):Re);z[T]=Ai,Q[T]=Ai-ie}if(a){var Bn,Di=T==="x"?de:pe,ln=T==="x"?ge:me,ot=z[K],cn=K==="y"?"height":"width",Vn=ot+_[Di],un=ot-_[ln],dn=[de,pe].indexOf(C)!==-1,Ft=(Bn=Y==null?void 0:Y[K])!=null?Bn:0,Oi=dn?Vn:ot-H[cn]-I[cn]-Ft+j.altAxis,zn=dn?ot+H[cn]+I[cn]-Ft-j.altAxis:un,Li=E&&dn?Ls(Oi,ot,zn):jt(E?Oi:Vn,ot,E?zn:un);z[K]=Li,Q[K]=Li-ot}e.modifiersData[i]=Q}}var Vi={name:"preventOverflow",enabled:!0,phase:"main",fn:Pu,requiresIfExists:["offset"]};function ro(n){return{scrollLeft:n.scrollLeft,scrollTop:n.scrollTop}}function oo(n){return n===ce(n)||!_e(n)?qt(n):ro(n)}function Fu(n){var e=n.getBoundingClientRect(),t=ct(e.width)/n.offsetWidth||1,i=ct(e.height)/n.offsetHeight||1;return t!==1||i!==1}function so(n,e,t){t===void 0&&(t=!1);var i=_e(e),r=_e(e)&&Fu(e),o=Se(e),s=Ge(n,r,t),a={scrollLeft:0,scrollTop:0},l={x:0,y:0};return(i||!i&&!t)&&((xe(e)!=="body"||Ut(o))&&(a=oo(e)),_e(e)?(l=Ge(e,!0),l.x+=e.clientLeft,l.y+=e.clientTop):o&&(l.x=Wt(o))),{x:s.left+a.scrollLeft-l.x,y:s.top+a.scrollTop-l.y,width:s.width,height:s.height}}function $u(n){var e=new Map,t=new Set,i=[];n.forEach(function(o){e.set(o.name,o)});function r(o){t.add(o.name);var s=[].concat(o.requires||[],o.requiresIfExists||[]);s.forEach(function(a){if(!t.has(a)){var l=e.get(a);l&&r(l)}}),i.push(o)}return n.forEach(function(o){t.has(o.name)||r(o)}),i}function ao(n){var e=$u(n);return Qr.reduce(function(t,i){return t.concat(e.filter(function(r){return r.phase===i}))},[])}function lo(n){var e;return function(){return e||(e=new Promise(function(t){Promise.resolve().then(function(){e=void 0,t(n())})})),e}}function co(n){var e=n.reduce(function(t,i){var r=t[i.name];return t[i.name]=r?Object.assign({},r,i,{options:Object.assign({},r.options,i.options),data:Object.assign({},r.data,i.data)}):i,t},{});return Object.keys(e).map(function(t){return e[t]})}var Is={placement:"bottom",modifiers:[],strategy:"absolute"};function Rs(){for(var n=arguments.length,e=new Array(n),t=0;t(n&&window.CSS&&window.CSS.escape&&(n=n.replace(/#([^\s"#']+)/g,(e,t)=>`#${CSS.escape(t)}`)),n),qu=n=>n==null?`${n}`:Object.prototype.toString.call(n).match(/\s([a-z]+)/i)[1].toLowerCase(),Wu=n=>{do n+=Math.floor(Math.random()*zu);while(document.getElementById(n));return n},Uu=n=>{if(!n)return 0;let{transitionDuration:e,transitionDelay:t}=window.getComputedStyle(n),i=Number.parseFloat(e),r=Number.parseFloat(t);return!i&&!r?0:(e=e.split(",")[0],t=t.split(",")[0],(Number.parseFloat(e)+Number.parseFloat(t))*ju)},pa=n=>{n.dispatchEvent(new Event(Ao))},dt=n=>!n||typeof n!="object"?!1:(typeof n.jquery!="undefined"&&(n=n[0]),typeof n.nodeType!="undefined"),xt=n=>dt(n)?n.jquery?n[0]:n:typeof n=="string"&&n.length>0?document.querySelector(ha(n)):null,An=n=>{if(!dt(n)||n.getClientRects().length===0)return!1;let e=getComputedStyle(n).getPropertyValue("visibility")==="visible",t=n.closest("details:not([open])");if(!t)return e;if(t!==n){let i=n.closest("summary");if(i&&i.parentNode!==t||i===null)return!1}return e},Tt=n=>!n||n.nodeType!==Node.ELEMENT_NODE||n.classList.contains("disabled")?!0:typeof n.disabled!="undefined"?n.disabled:n.hasAttribute("disabled")&&n.getAttribute("disabled")!=="false",ma=n=>{if(!document.documentElement.attachShadow)return null;if(typeof n.getRootNode=="function"){let e=n.getRootNode();return e instanceof ShadowRoot?e:null}return n instanceof ShadowRoot?n:n.parentNode?ma(n.parentNode):null},Qi=()=>{},ri=n=>{n.offsetHeight},ga=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,fo=[],Yu=n=>{document.readyState==="loading"?(fo.length||document.addEventListener("DOMContentLoaded",()=>{for(let e of fo)e()}),fo.push(n)):n()},Xe=()=>document.documentElement.dir==="rtl",Je=n=>{Yu(()=>{let e=ga();if(e){let t=n.NAME,i=e.fn[t];e.fn[t]=n.jQueryInterface,e.fn[t].Constructor=n,e.fn[t].noConflict=()=>(e.fn[t]=i,n.jQueryInterface)}})},Pe=(n,e=[],t=n)=>typeof n=="function"?n.call(...e):t,va=(n,e,t=!0)=>{if(!t){Pe(n);return}let r=Uu(e)+5,o=!1,s=({target:a})=>{a===e&&(o=!0,e.removeEventListener(Ao,s),Pe(n))};e.addEventListener(Ao,s),setTimeout(()=>{o||pa(e)},r)},Mo=(n,e,t,i)=>{let r=n.length,o=n.indexOf(e);return o===-1?!t&&i?n[r-1]:n[0]:(o+=t?1:-1,i&&(o=(o+r)%r),n[Math.max(0,Math.min(o,r-1))])},Gu=/[^.]*(?=\..*)\.|.*/,Ku=/\..*/,Xu=/::\d+$/,ho={},$s=1,Ea={mouseenter:"mouseover",mouseleave:"mouseout"},Qu=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 ya(n,e){return e&&`${e}::${$s++}`||n.uidEvent||$s++}function ba(n){let e=ya(n);return n.uidEvent=e,ho[e]=ho[e]||{},ho[e]}function Ju(n,e){return function t(i){return No(i,{delegateTarget:n}),t.oneOff&&x.off(n,i.type,e),e.apply(n,[i])}}function Zu(n,e,t){return function i(r){let o=n.querySelectorAll(e);for(let{target:s}=r;s&&s!==this;s=s.parentNode)for(let a of o)if(a===s)return No(r,{delegateTarget:s}),i.oneOff&&x.off(n,r.type,e,t),t.apply(s,[r])}}function _a(n,e,t=null){return Object.values(n).find(i=>i.callable===e&&i.delegationSelector===t)}function wa(n,e,t){let i=typeof e=="string",r=i?t:e||t,o=xa(n);return Qu.has(o)||(o=n),[i,r,o]}function Bs(n,e,t,i,r){if(typeof e!="string"||!n)return;let[o,s,a]=wa(e,t,i);e in Ea&&(s=(m=>function(v){if(!v.relatedTarget||v.relatedTarget!==v.delegateTarget&&!v.delegateTarget.contains(v.relatedTarget))return m.call(this,v)})(s));let l=ba(n),c=l[a]||(l[a]={}),u=_a(c,s,o?t:null);if(u){u.oneOff=u.oneOff&&r;return}let d=ya(s,e.replace(Gu,"")),p=o?Zu(n,t,s):Ju(n,s);p.delegationSelector=o?t:null,p.callable=s,p.oneOff=r,p.uidEvent=d,c[d]=p,n.addEventListener(a,p,o)}function Do(n,e,t,i,r){let o=_a(e[t],i,r);o&&(n.removeEventListener(t,o,!!r),delete e[t][o.uidEvent])}function ed(n,e,t,i){let r=e[t]||{};for(let[o,s]of Object.entries(r))o.includes(i)&&Do(n,e,t,s.callable,s.delegationSelector)}function xa(n){return n=n.replace(Ku,""),Ea[n]||n}var x={on(n,e,t,i){Bs(n,e,t,i,!1)},one(n,e,t,i){Bs(n,e,t,i,!0)},off(n,e,t,i){if(typeof e!="string"||!n)return;let[r,o,s]=wa(e,t,i),a=s!==e,l=ba(n),c=l[s]||{},u=e.startsWith(".");if(typeof o!="undefined"){if(!Object.keys(c).length)return;Do(n,l,s,o,r?t:null);return}if(u)for(let d of Object.keys(l))ed(n,l,d,e.slice(1));for(let[d,p]of Object.entries(c)){let E=d.replace(Xu,"");(!a||e.includes(E))&&Do(n,l,s,p.callable,p.delegationSelector)}},trigger(n,e,t){if(typeof e!="string"||!n)return null;let i=ga(),r=xa(e),o=e!==r,s=null,a=!0,l=!0,c=!1;o&&i&&(s=i.Event(e,t),i(n).trigger(s),a=!s.isPropagationStopped(),l=!s.isImmediatePropagationStopped(),c=s.isDefaultPrevented());let u=No(new Event(e,{bubbles:a,cancelable:!0}),t);return c&&u.preventDefault(),l&&n.dispatchEvent(u),u.defaultPrevented&&s&&s.preventDefault(),u}};function No(n,e={}){for(let[t,i]of Object.entries(e))try{n[t]=i}catch(r){Object.defineProperty(n,t,{configurable:!0,get(){return i}})}return n}function Vs(n){if(n==="true")return!0;if(n==="false")return!1;if(n===Number(n).toString())return Number(n);if(n===""||n==="null")return null;if(typeof n!="string")return n;try{return JSON.parse(decodeURIComponent(n))}catch(e){return n}}function po(n){return n.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}var ft={setDataAttribute(n,e,t){n.setAttribute(`data-bs-${po(e)}`,t)},removeDataAttribute(n,e){n.removeAttribute(`data-bs-${po(e)}`)},getDataAttributes(n){if(!n)return{};let e={},t=Object.keys(n.dataset).filter(i=>i.startsWith("bs")&&!i.startsWith("bsConfig"));for(let i of t){let r=i.replace(/^bs/,"");r=r.charAt(0).toLowerCase()+r.slice(1),e[r]=Vs(n.dataset[i])}return e},getDataAttribute(n,e){return Vs(n.getAttribute(`data-bs-${po(e)}`))}},Xt=class{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(e){return e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e}_mergeConfigObj(e,t){let i=dt(t)?ft.getDataAttribute(t,"config"):{};return O(O(O(O({},this.constructor.Default),typeof i=="object"?i:{}),dt(t)?ft.getDataAttributes(t):{}),typeof e=="object"?e:{})}_typeCheckConfig(e,t=this.constructor.DefaultType){for(let[i,r]of Object.entries(t)){let o=e[i],s=dt(o)?"element":qu(o);if(!new RegExp(r).test(s))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${i}" provided type "${s}" but expected type "${r}".`)}}},td="5.3.8",We=class extends Xt{constructor(e,t){super(),e=xt(e),e&&(this._element=e,this._config=this._getConfig(t),uo.set(this._element,this.constructor.DATA_KEY,this))}dispose(){uo.remove(this._element,this.constructor.DATA_KEY),x.off(this._element,this.constructor.EVENT_KEY);for(let e of Object.getOwnPropertyNames(this))this[e]=null}_queueCallback(e,t,i=!0){va(e,t,i)}_getConfig(e){return e=this._mergeConfigObj(e,this._element),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}static getInstance(e){return uo.get(xt(e),this.DATA_KEY)}static getOrCreateInstance(e,t={}){return this.getInstance(e)||new this(e,typeof t=="object"?t:null)}static get VERSION(){return td}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(e){return`${e}${this.EVENT_KEY}`}},mo=n=>{let e=n.getAttribute("data-bs-target");if(!e||e==="#"){let t=n.getAttribute("href");if(!t||!t.includes("#")&&!t.startsWith("."))return null;t.includes("#")&&!t.startsWith("#")&&(t=`#${t.split("#")[1]}`),e=t&&t!=="#"?t.trim():null}return e?e.split(",").map(t=>ha(t)).join(","):null},V={find(n,e=document.documentElement){return[].concat(...Element.prototype.querySelectorAll.call(e,n))},findOne(n,e=document.documentElement){return Element.prototype.querySelector.call(e,n)},children(n,e){return[].concat(...n.children).filter(t=>t.matches(e))},parents(n,e){let t=[],i=n.parentNode.closest(e);for(;i;)t.push(i),i=i.parentNode.closest(e);return t},prev(n,e){let t=n.previousElementSibling;for(;t;){if(t.matches(e))return[t];t=t.previousElementSibling}return[]},next(n,e){let t=n.nextElementSibling;for(;t;){if(t.matches(e))return[t];t=t.nextElementSibling}return[]},focusableChildren(n){let e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(t=>`${t}:not([tabindex^="-"])`).join(",");return this.find(e,n).filter(t=>!Tt(t)&&An(t))},getSelectorFromElement(n){let e=mo(n);return e&&V.findOne(e)?e:null},getElementFromSelector(n){let e=mo(n);return e?V.findOne(e):null},getMultipleElementsFromSelector(n){let e=mo(n);return e?V.find(e):[]}},or=(n,e="hide")=>{let t=`click.dismiss${n.EVENT_KEY}`,i=n.NAME;x.on(document,t,`[data-bs-dismiss="${i}"]`,function(r){if(["A","AREA"].includes(this.tagName)&&r.preventDefault(),Tt(this))return;let o=V.getElementFromSelector(this)||this.closest(`.${i}`);n.getOrCreateInstance(o)[e]()})},nd="alert",id="bs.alert",Ta=`.${id}`,rd=`close${Ta}`,od=`closed${Ta}`,sd="fade",ad="show",Ji=class n extends We{static get NAME(){return nd}close(){if(x.trigger(this._element,rd).defaultPrevented)return;this._element.classList.remove(ad);let t=this._element.classList.contains(sd);this._queueCallback(()=>this._destroyElement(),this._element,t)}_destroyElement(){this._element.remove(),x.trigger(this._element,od),this.dispose()}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this);if(typeof e=="string"){if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e](this)}})}};or(Ji,"close");Je(Ji);var ld="button",cd="bs.button",ud=`.${cd}`,dd=".data-api",fd="active",zs='[data-bs-toggle="button"]',hd=`click${ud}${dd}`,Zi=class n extends We{static get NAME(){return ld}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle(fd))}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this);e==="toggle"&&t[e]()})}};x.on(document,hd,zs,n=>{n.preventDefault();let e=n.target.closest(zs);Zi.getOrCreateInstance(e).toggle()});Je(Zi);var pd="swipe",Dn=".bs.swipe",md=`touchstart${Dn}`,gd=`touchmove${Dn}`,vd=`touchend${Dn}`,Ed=`pointerdown${Dn}`,yd=`pointerup${Dn}`,bd="touch",_d="pen",wd="pointer-event",xd=40,Td={endCallback:null,leftCallback:null,rightCallback:null},Cd={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"},er=class n extends Xt{constructor(e,t){super(),this._element=e,!(!e||!n.isSupported())&&(this._config=this._getConfig(t),this._deltaX=0,this._supportPointerEvents=!!window.PointerEvent,this._initEvents())}static get Default(){return Td}static get DefaultType(){return Cd}static get NAME(){return pd}dispose(){x.off(this._element,Dn)}_start(e){if(!this._supportPointerEvents){this._deltaX=e.touches[0].clientX;return}this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX)}_end(e){this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX-this._deltaX),this._handleSwipe(),Pe(this._config.endCallback)}_move(e){this._deltaX=e.touches&&e.touches.length>1?0:e.touches[0].clientX-this._deltaX}_handleSwipe(){let e=Math.abs(this._deltaX);if(e<=xd)return;let t=e/this._deltaX;this._deltaX=0,t&&Pe(t>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(x.on(this._element,Ed,e=>this._start(e)),x.on(this._element,yd,e=>this._end(e)),this._element.classList.add(wd)):(x.on(this._element,md,e=>this._start(e)),x.on(this._element,gd,e=>this._move(e)),x.on(this._element,vd,e=>this._end(e)))}_eventIsPointerPenTouch(e){return this._supportPointerEvents&&(e.pointerType===_d||e.pointerType===bd)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}},Sd="carousel",Ad="bs.carousel",Dt=`.${Ad}`,Ca=".data-api",Dd="ArrowLeft",Od="ArrowRight",Ld=500,ei="next",_n="prev",xn="left",Ki="right",Md=`slide${Dt}`,go=`slid${Dt}`,Nd=`keydown${Dt}`,kd=`mouseenter${Dt}`,Hd=`mouseleave${Dt}`,Id=`dragstart${Dt}`,Rd=`load${Dt}${Ca}`,Pd=`click${Dt}${Ca}`,Sa="carousel",ji="active",Fd="slide",$d="carousel-item-end",Bd="carousel-item-start",Vd="carousel-item-next",zd="carousel-item-prev",Aa=".active",Da=".carousel-item",jd=Aa+Da,qd=".carousel-item img",Wd=".carousel-indicators",Ud="[data-bs-slide], [data-bs-slide-to]",Yd='[data-bs-ride="carousel"]',Gd={[Dd]:Ki,[Od]:xn},Kd={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Xd={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"},ni=class n extends We{constructor(e,t){super(e,t),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=V.findOne(Wd,this._element),this._addEventListeners(),this._config.ride===Sa&&this.cycle()}static get Default(){return Kd}static get DefaultType(){return Xd}static get NAME(){return Sd}next(){this._slide(ei)}nextWhenVisible(){!document.hidden&&An(this._element)&&this.next()}prev(){this._slide(_n)}pause(){this._isSliding&&pa(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval(()=>this.nextWhenVisible(),this._config.interval)}_maybeEnableCycle(){if(this._config.ride){if(this._isSliding){x.one(this._element,go,()=>this.cycle());return}this.cycle()}}to(e){let t=this._getItems();if(e>t.length-1||e<0)return;if(this._isSliding){x.one(this._element,go,()=>this.to(e));return}let i=this._getItemIndex(this._getActive());if(i===e)return;let r=e>i?ei:_n;this._slide(r,t[e])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(e){return e.defaultInterval=e.interval,e}_addEventListeners(){this._config.keyboard&&x.on(this._element,Nd,e=>this._keydown(e)),this._config.pause==="hover"&&(x.on(this._element,kd,()=>this.pause()),x.on(this._element,Hd,()=>this._maybeEnableCycle())),this._config.touch&&er.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(let i of V.find(qd,this._element))x.on(i,Id,r=>r.preventDefault());let t={leftCallback:()=>this._slide(this._directionToOrder(xn)),rightCallback:()=>this._slide(this._directionToOrder(Ki)),endCallback:()=>{this._config.pause==="hover"&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(()=>this._maybeEnableCycle(),Ld+this._config.interval))}};this._swipeHelper=new er(this._element,t)}_keydown(e){if(/input|textarea/i.test(e.target.tagName))return;let t=Gd[e.key];t&&(e.preventDefault(),this._slide(this._directionToOrder(t)))}_getItemIndex(e){return this._getItems().indexOf(e)}_setActiveIndicatorElement(e){if(!this._indicatorsElement)return;let t=V.findOne(Aa,this._indicatorsElement);t.classList.remove(ji),t.removeAttribute("aria-current");let i=V.findOne(`[data-bs-slide-to="${e}"]`,this._indicatorsElement);i&&(i.classList.add(ji),i.setAttribute("aria-current","true"))}_updateInterval(){let e=this._activeElement||this._getActive();if(!e)return;let t=Number.parseInt(e.getAttribute("data-bs-interval"),10);this._config.interval=t||this._config.defaultInterval}_slide(e,t=null){if(this._isSliding)return;let i=this._getActive(),r=e===ei,o=t||Mo(this._getItems(),i,r,this._config.wrap);if(o===i)return;let s=this._getItemIndex(o),a=E=>x.trigger(this._element,E,{relatedTarget:o,direction:this._orderToDirection(e),from:this._getItemIndex(i),to:s});if(a(Md).defaultPrevented||!i||!o)return;let c=!!this._interval;this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(s),this._activeElement=o;let u=r?Bd:$d,d=r?Vd:zd;o.classList.add(d),ri(o),i.classList.add(u),o.classList.add(u);let p=()=>{o.classList.remove(u,d),o.classList.add(ji),i.classList.remove(ji,d,u),this._isSliding=!1,a(go)};this._queueCallback(p,i,this._isAnimated()),c&&this.cycle()}_isAnimated(){return this._element.classList.contains(Fd)}_getActive(){return V.findOne(jd,this._element)}_getItems(){return V.find(Da,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(e){return Xe()?e===xn?_n:ei:e===xn?ei:_n}_orderToDirection(e){return Xe()?e===_n?xn:Ki:e===_n?Ki:xn}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this,e);if(typeof e=="number"){t.to(e);return}if(typeof e=="string"){if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e]()}})}};x.on(document,Pd,Ud,function(n){let e=V.getElementFromSelector(this);if(!e||!e.classList.contains(Sa))return;n.preventDefault();let t=ni.getOrCreateInstance(e),i=this.getAttribute("data-bs-slide-to");if(i){t.to(i),t._maybeEnableCycle();return}if(ft.getDataAttribute(this,"slide")==="next"){t.next(),t._maybeEnableCycle();return}t.prev(),t._maybeEnableCycle()});x.on(window,Rd,()=>{let n=V.find(Yd);for(let e of n)ni.getOrCreateInstance(e)});Je(ni);var Qd="collapse",Jd="bs.collapse",oi=`.${Jd}`,Zd=".data-api",ef=`show${oi}`,tf=`shown${oi}`,nf=`hide${oi}`,rf=`hidden${oi}`,of=`click${oi}${Zd}`,vo="show",Cn="collapse",qi="collapsing",sf="collapsed",af=`:scope .${Cn} .${Cn}`,lf="collapse-horizontal",cf="width",uf="height",df=".collapse.show, .collapse.collapsing",Oo='[data-bs-toggle="collapse"]',ff={parent:null,toggle:!0},hf={parent:"(null|element)",toggle:"boolean"},Ct=class n extends We{constructor(e,t){super(e,t),this._isTransitioning=!1,this._triggerArray=[];let i=V.find(Oo);for(let r of i){let o=V.getSelectorFromElement(r),s=V.find(o).filter(a=>a===this._element);o!==null&&s.length&&this._triggerArray.push(r)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return ff}static get DefaultType(){return hf}static get NAME(){return Qd}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let e=[];if(this._config.parent&&(e=this._getFirstLevelChildren(df).filter(a=>a!==this._element).map(a=>n.getOrCreateInstance(a,{toggle:!1}))),e.length&&e[0]._isTransitioning||x.trigger(this._element,ef).defaultPrevented)return;for(let a of e)a.hide();let i=this._getDimension();this._element.classList.remove(Cn),this._element.classList.add(qi),this._element.style[i]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;let r=()=>{this._isTransitioning=!1,this._element.classList.remove(qi),this._element.classList.add(Cn,vo),this._element.style[i]="",x.trigger(this._element,tf)},s=`scroll${i[0].toUpperCase()+i.slice(1)}`;this._queueCallback(r,this._element,!0),this._element.style[i]=`${this._element[s]}px`}hide(){if(this._isTransitioning||!this._isShown()||x.trigger(this._element,nf).defaultPrevented)return;let t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,ri(this._element),this._element.classList.add(qi),this._element.classList.remove(Cn,vo);for(let r of this._triggerArray){let o=V.getElementFromSelector(r);o&&!this._isShown(o)&&this._addAriaAndCollapsedClass([r],!1)}this._isTransitioning=!0;let i=()=>{this._isTransitioning=!1,this._element.classList.remove(qi),this._element.classList.add(Cn),x.trigger(this._element,rf)};this._element.style[t]="",this._queueCallback(i,this._element,!0)}_isShown(e=this._element){return e.classList.contains(vo)}_configAfterMerge(e){return e.toggle=!!e.toggle,e.parent=xt(e.parent),e}_getDimension(){return this._element.classList.contains(lf)?cf:uf}_initializeChildren(){if(!this._config.parent)return;let e=this._getFirstLevelChildren(Oo);for(let t of e){let i=V.getElementFromSelector(t);i&&this._addAriaAndCollapsedClass([t],this._isShown(i))}}_getFirstLevelChildren(e){let t=V.find(af,this._config.parent);return V.find(e,this._config.parent).filter(i=>!t.includes(i))}_addAriaAndCollapsedClass(e,t){if(e.length)for(let i of e)i.classList.toggle(sf,!t),i.setAttribute("aria-expanded",t)}static jQueryInterface(e){let t={};return typeof e=="string"&&/show|hide/.test(e)&&(t.toggle=!1),this.each(function(){let i=n.getOrCreateInstance(this,t);if(typeof e=="string"){if(typeof i[e]=="undefined")throw new TypeError(`No method named "${e}"`);i[e]()}})}};x.on(document,of,Oo,function(n){(n.target.tagName==="A"||n.delegateTarget&&n.delegateTarget.tagName==="A")&&n.preventDefault();for(let e of V.getMultipleElementsFromSelector(this))Ct.getOrCreateInstance(e,{toggle:!1}).toggle()});Je(Ct);var js="dropdown",pf="bs.dropdown",Jt=`.${pf}`,ko=".data-api",mf="Escape",qs="Tab",gf="ArrowUp",Ws="ArrowDown",vf=2,Ef=`hide${Jt}`,yf=`hidden${Jt}`,bf=`show${Jt}`,_f=`shown${Jt}`,Oa=`click${Jt}${ko}`,La=`keydown${Jt}${ko}`,wf=`keyup${Jt}${ko}`,Tn="show",xf="dropup",Tf="dropend",Cf="dropstart",Sf="dropup-center",Af="dropdown-center",Gt='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',Df=`${Gt}.${Tn}`,Xi=".dropdown-menu",Of=".navbar",Lf=".navbar-nav",Mf=".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",Nf=Xe()?"top-end":"top-start",kf=Xe()?"top-start":"top-end",Hf=Xe()?"bottom-end":"bottom-start",If=Xe()?"bottom-start":"bottom-end",Rf=Xe()?"left-start":"right-start",Pf=Xe()?"right-start":"left-start",Ff="top",$f="bottom",Bf={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},Vf={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"},St=class n extends We{constructor(e,t){super(e,t),this._popper=null,this._parent=this._element.parentNode,this._menu=V.next(this._element,Xi)[0]||V.prev(this._element,Xi)[0]||V.findOne(Xi,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return Bf}static get DefaultType(){return Vf}static get NAME(){return js}toggle(){return this._isShown()?this.hide():this.show()}show(){if(Tt(this._element)||this._isShown())return;let e={relatedTarget:this._element};if(!x.trigger(this._element,bf,e).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(Lf))for(let i of[].concat(...document.body.children))x.on(i,"mouseover",Qi);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Tn),this._element.classList.add(Tn),x.trigger(this._element,_f,e)}}hide(){if(Tt(this._element)||!this._isShown())return;let e={relatedTarget:this._element};this._completeHide(e)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(e){if(!x.trigger(this._element,Ef,e).defaultPrevented){if("ontouchstart"in document.documentElement)for(let i of[].concat(...document.body.children))x.off(i,"mouseover",Qi);this._popper&&this._popper.destroy(),this._menu.classList.remove(Tn),this._element.classList.remove(Tn),this._element.setAttribute("aria-expanded","false"),ft.removeDataAttribute(this._menu,"popper"),x.trigger(this._element,yf,e)}}_getConfig(e){if(e=super._getConfig(e),typeof e.reference=="object"&&!dt(e.reference)&&typeof e.reference.getBoundingClientRect!="function")throw new TypeError(`${js.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return e}_createPopper(){if(typeof zi=="undefined")throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org/docs/v2/)");let e=this._element;this._config.reference==="parent"?e=this._parent:dt(this._config.reference)?e=xt(this._config.reference):typeof this._config.reference=="object"&&(e=this._config.reference);let t=this._getPopperConfig();this._popper=Zn(e,this._menu,t)}_isShown(){return this._menu.classList.contains(Tn)}_getPlacement(){let e=this._parent;if(e.classList.contains(Tf))return Rf;if(e.classList.contains(Cf))return Pf;if(e.classList.contains(Sf))return Ff;if(e.classList.contains(Af))return $f;let t=getComputedStyle(this._menu).getPropertyValue("--bs-position").trim()==="end";return e.classList.contains(xf)?t?kf:Nf:t?If:Hf}_detectNavbar(){return this._element.closest(Of)!==null}_getOffset(){let{offset:e}=this._config;return typeof e=="string"?e.split(",").map(t=>Number.parseInt(t,10)):typeof e=="function"?t=>e(t,this._element):e}_getPopperConfig(){let e={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||this._config.display==="static")&&(ft.setDataAttribute(this._menu,"popper","static"),e.modifiers=[{name:"applyStyles",enabled:!1}]),O(O({},e),Pe(this._config.popperConfig,[void 0,e]))}_selectMenuItem({key:e,target:t}){let i=V.find(Mf,this._menu).filter(r=>An(r));i.length&&Mo(i,t,e===Ws,!i.includes(t)).focus()}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this,e);if(typeof e=="string"){if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e]()}})}static clearMenus(e){if(e.button===vf||e.type==="keyup"&&e.key!==qs)return;let t=V.find(Df);for(let i of t){let r=n.getInstance(i);if(!r||r._config.autoClose===!1)continue;let o=e.composedPath(),s=o.includes(r._menu);if(o.includes(r._element)||r._config.autoClose==="inside"&&!s||r._config.autoClose==="outside"&&s||r._menu.contains(e.target)&&(e.type==="keyup"&&e.key===qs||/input|select|option|textarea|form/i.test(e.target.tagName)))continue;let a={relatedTarget:r._element};e.type==="click"&&(a.clickEvent=e),r._completeHide(a)}}static dataApiKeydownHandler(e){let t=/input|textarea/i.test(e.target.tagName),i=e.key===mf,r=[gf,Ws].includes(e.key);if(!r&&!i||t&&!i)return;e.preventDefault();let o=this.matches(Gt)?this:V.prev(this,Gt)[0]||V.next(this,Gt)[0]||V.findOne(Gt,e.delegateTarget.parentNode),s=n.getOrCreateInstance(o);if(r){e.stopPropagation(),s.show(),s._selectMenuItem(e);return}s._isShown()&&(e.stopPropagation(),s.hide(),o.focus())}};x.on(document,La,Gt,St.dataApiKeydownHandler);x.on(document,La,Xi,St.dataApiKeydownHandler);x.on(document,Oa,St.clearMenus);x.on(document,wf,St.clearMenus);x.on(document,Oa,Gt,function(n){n.preventDefault(),St.getOrCreateInstance(this).toggle()});Je(St);var Ma="backdrop",zf="fade",Us="show",Ys=`mousedown.bs.${Ma}`,jf={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},qf={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"},tr=class extends Xt{constructor(e){super(),this._config=this._getConfig(e),this._isAppended=!1,this._element=null}static get Default(){return jf}static get DefaultType(){return qf}static get NAME(){return Ma}show(e){if(!this._config.isVisible){Pe(e);return}this._append();let t=this._getElement();this._config.isAnimated&&ri(t),t.classList.add(Us),this._emulateAnimation(()=>{Pe(e)})}hide(e){if(!this._config.isVisible){Pe(e);return}this._getElement().classList.remove(Us),this._emulateAnimation(()=>{this.dispose(),Pe(e)})}dispose(){this._isAppended&&(x.off(this._element,Ys),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){let e=document.createElement("div");e.className=this._config.className,this._config.isAnimated&&e.classList.add(zf),this._element=e}return this._element}_configAfterMerge(e){return e.rootElement=xt(e.rootElement),e}_append(){if(this._isAppended)return;let e=this._getElement();this._config.rootElement.append(e),x.on(e,Ys,()=>{Pe(this._config.clickCallback)}),this._isAppended=!0}_emulateAnimation(e){va(e,this._getElement(),this._config.isAnimated)}},Wf="focustrap",Uf="bs.focustrap",nr=`.${Uf}`,Yf=`focusin${nr}`,Gf=`keydown.tab${nr}`,Kf="Tab",Xf="forward",Gs="backward",Qf={autofocus:!0,trapElement:null},Jf={autofocus:"boolean",trapElement:"element"},ir=class extends Xt{constructor(e){super(),this._config=this._getConfig(e),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return Qf}static get DefaultType(){return Jf}static get NAME(){return Wf}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),x.off(document,nr),x.on(document,Yf,e=>this._handleFocusin(e)),x.on(document,Gf,e=>this._handleKeydown(e)),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,x.off(document,nr))}_handleFocusin(e){let{trapElement:t}=this._config;if(e.target===document||e.target===t||t.contains(e.target))return;let i=V.focusableChildren(t);i.length===0?t.focus():this._lastTabNavDirection===Gs?i[i.length-1].focus():i[0].focus()}_handleKeydown(e){e.key===Kf&&(this._lastTabNavDirection=e.shiftKey?Gs:Xf)}},Ks=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",Xs=".sticky-top",Wi="padding-right",Qs="margin-right",ii=class{constructor(){this._element=document.body}getWidth(){let e=document.documentElement.clientWidth;return Math.abs(window.innerWidth-e)}hide(){let e=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,Wi,t=>t+e),this._setElementAttributes(Ks,Wi,t=>t+e),this._setElementAttributes(Xs,Qs,t=>t-e)}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,Wi),this._resetElementAttributes(Ks,Wi),this._resetElementAttributes(Xs,Qs)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(e,t,i){let r=this.getWidth(),o=s=>{if(s!==this._element&&window.innerWidth>s.clientWidth+r)return;this._saveInitialAttribute(s,t);let a=window.getComputedStyle(s).getPropertyValue(t);s.style.setProperty(t,`${i(Number.parseFloat(a))}px`)};this._applyManipulationCallback(e,o)}_saveInitialAttribute(e,t){let i=e.style.getPropertyValue(t);i&&ft.setDataAttribute(e,t,i)}_resetElementAttributes(e,t){let i=r=>{let o=ft.getDataAttribute(r,t);if(o===null){r.style.removeProperty(t);return}ft.removeDataAttribute(r,t),r.style.setProperty(t,o)};this._applyManipulationCallback(e,i)}_applyManipulationCallback(e,t){if(dt(e)){t(e);return}for(let i of V.find(e,this._element))t(i)}},Zf="modal",eh="bs.modal",Qe=`.${eh}`,th=".data-api",nh="Escape",ih=`hide${Qe}`,rh=`hidePrevented${Qe}`,Na=`hidden${Qe}`,ka=`show${Qe}`,oh=`shown${Qe}`,sh=`resize${Qe}`,ah=`click.dismiss${Qe}`,lh=`mousedown.dismiss${Qe}`,ch=`keydown.dismiss${Qe}`,uh=`click${Qe}${th}`,Js="modal-open",dh="fade",Zs="show",Eo="modal-static",fh=".modal.show",hh=".modal-dialog",ph=".modal-body",mh='[data-bs-toggle="modal"]',gh={backdrop:!0,focus:!0,keyboard:!0},vh={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"},tt=class n extends We{constructor(e,t){super(e,t),this._dialog=V.findOne(hh,this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new ii,this._addEventListeners()}static get Default(){return gh}static get DefaultType(){return vh}static get NAME(){return Zf}toggle(e){return this._isShown?this.hide():this.show(e)}show(e){this._isShown||this._isTransitioning||x.trigger(this._element,ka,{relatedTarget:e}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Js),this._adjustDialog(),this._backdrop.show(()=>this._showElement(e)))}hide(){!this._isShown||this._isTransitioning||x.trigger(this._element,ih).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Zs),this._queueCallback(()=>this._hideModal(),this._element,this._isAnimated()))}dispose(){x.off(window,Qe),x.off(this._dialog,Qe),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new tr({isVisible:!!this._config.backdrop,isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new ir({trapElement:this._element})}_showElement(e){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;let t=V.findOne(ph,this._dialog);t&&(t.scrollTop=0),ri(this._element),this._element.classList.add(Zs);let i=()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,x.trigger(this._element,oh,{relatedTarget:e})};this._queueCallback(i,this._dialog,this._isAnimated())}_addEventListeners(){x.on(this._element,ch,e=>{if(e.key===nh){if(this._config.keyboard){this.hide();return}this._triggerBackdropTransition()}}),x.on(window,sh,()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()}),x.on(this._element,lh,e=>{x.one(this._element,ah,t=>{if(!(this._element!==e.target||this._element!==t.target)){if(this._config.backdrop==="static"){this._triggerBackdropTransition();return}this._config.backdrop&&this.hide()}})})}_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(Js),this._resetAdjustments(),this._scrollBar.reset(),x.trigger(this._element,Na)})}_isAnimated(){return this._element.classList.contains(dh)}_triggerBackdropTransition(){if(x.trigger(this._element,rh).defaultPrevented)return;let t=this._element.scrollHeight>document.documentElement.clientHeight,i=this._element.style.overflowY;i==="hidden"||this._element.classList.contains(Eo)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Eo),this._queueCallback(()=>{this._element.classList.remove(Eo),this._queueCallback(()=>{this._element.style.overflowY=i},this._dialog)},this._dialog),this._element.focus())}_adjustDialog(){let e=this._element.scrollHeight>document.documentElement.clientHeight,t=this._scrollBar.getWidth(),i=t>0;if(i&&!e){let r=Xe()?"paddingLeft":"paddingRight";this._element.style[r]=`${t}px`}if(!i&&e){let r=Xe()?"paddingRight":"paddingLeft";this._element.style[r]=`${t}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(e,t){return this.each(function(){let i=n.getOrCreateInstance(this,e);if(typeof e=="string"){if(typeof i[e]=="undefined")throw new TypeError(`No method named "${e}"`);i[e](t)}})}};x.on(document,uh,mh,function(n){let e=V.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&n.preventDefault(),x.one(e,ka,r=>{r.defaultPrevented||x.one(e,Na,()=>{An(this)&&this.focus()})});let t=V.findOne(fh);t&&tt.getInstance(t).hide(),tt.getOrCreateInstance(e).toggle(this)});or(tt);Je(tt);var Eh="offcanvas",yh="bs.offcanvas",mt=`.${yh}`,Ha=".data-api",bh=`load${mt}${Ha}`,_h="Escape",ea="show",ta="showing",na="hiding",wh="offcanvas-backdrop",Ia=".offcanvas.show",xh=`show${mt}`,Th=`shown${mt}`,Ch=`hide${mt}`,ia=`hidePrevented${mt}`,Ra=`hidden${mt}`,Sh=`resize${mt}`,Ah=`click${mt}${Ha}`,Dh=`keydown.dismiss${mt}`,Oh='[data-bs-toggle="offcanvas"]',Lh={backdrop:!0,keyboard:!0,scroll:!1},Mh={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"},At=class n extends We{constructor(e,t){super(e,t),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Lh}static get DefaultType(){return Mh}static get NAME(){return Eh}toggle(e){return this._isShown?this.hide():this.show(e)}show(e){if(this._isShown||x.trigger(this._element,xh,{relatedTarget:e}).defaultPrevented)return;this._isShown=!0,this._backdrop.show(),this._config.scroll||new ii().hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(ta);let i=()=>{(!this._config.scroll||this._config.backdrop)&&this._focustrap.activate(),this._element.classList.add(ea),this._element.classList.remove(ta),x.trigger(this._element,Th,{relatedTarget:e})};this._queueCallback(i,this._element,!0)}hide(){if(!this._isShown||x.trigger(this._element,Ch).defaultPrevented)return;this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(na),this._backdrop.hide();let t=()=>{this._element.classList.remove(ea,na),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||new ii().reset(),x.trigger(this._element,Ra)};this._queueCallback(t,this._element,!0)}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){let e=()=>{if(this._config.backdrop==="static"){x.trigger(this._element,ia);return}this.hide()},t=!!this._config.backdrop;return new tr({className:wh,isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?e:null})}_initializeFocusTrap(){return new ir({trapElement:this._element})}_addEventListeners(){x.on(this._element,Dh,e=>{if(e.key===_h){if(this._config.keyboard){this.hide();return}x.trigger(this._element,ia)}})}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this,e);if(typeof e=="string"){if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e](this)}})}};x.on(document,Ah,Oh,function(n){let e=V.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&n.preventDefault(),Tt(this))return;x.one(e,Ra,()=>{An(this)&&this.focus()});let t=V.findOne(Ia);t&&t!==e&&At.getInstance(t).hide(),At.getOrCreateInstance(e).toggle(this)});x.on(window,bh,()=>{for(let n of V.find(Ia))At.getOrCreateInstance(n).show()});x.on(window,Sh,()=>{for(let n of V.find("[aria-modal][class*=show][class*=offcanvas-]"))getComputedStyle(n).position!=="fixed"&&At.getOrCreateInstance(n).hide()});or(At);Je(At);var Nh=/^aria-[\w-]*$/i,Pa={"*":["class","dir","id","lang","role",Nh],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:[]},kh=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Hh=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Ih=(n,e)=>{let t=n.nodeName.toLowerCase();return e.includes(t)?kh.has(t)?!!Hh.test(n.nodeValue):!0:e.filter(i=>i instanceof RegExp).some(i=>i.test(t))};function Rh(n,e,t){if(!n.length)return n;if(t&&typeof t=="function")return t(n);let r=new window.DOMParser().parseFromString(n,"text/html"),o=[].concat(...r.body.querySelectorAll("*"));for(let s of o){let a=s.nodeName.toLowerCase();if(!Object.keys(e).includes(a)){s.remove();continue}let l=[].concat(...s.attributes),c=[].concat(e["*"]||[],e[a]||[]);for(let u of l)Ih(u,c)||s.removeAttribute(u.nodeName)}return r.body.innerHTML}var Ph="TemplateFactory",Fh={allowList:Pa,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},$h={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Bh={entry:"(string|element|function|null)",selector:"(string|element)"},Lo=class extends Xt{constructor(e){super(),this._config=this._getConfig(e)}static get Default(){return Fh}static get DefaultType(){return $h}static get NAME(){return Ph}getContent(){return Object.values(this._config.content).map(e=>this._resolvePossibleFunction(e)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(e){return this._checkContent(e),this._config.content=O(O({},this._config.content),e),this}toHtml(){let e=document.createElement("div");e.innerHTML=this._maybeSanitize(this._config.template);for(let[r,o]of Object.entries(this._config.content))this._setContent(e,o,r);let t=e.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&t.classList.add(...i.split(" ")),t}_typeCheckConfig(e){super._typeCheckConfig(e),this._checkContent(e.content)}_checkContent(e){for(let[t,i]of Object.entries(e))super._typeCheckConfig({selector:t,entry:i},Bh)}_setContent(e,t,i){let r=V.findOne(i,e);if(r){if(t=this._resolvePossibleFunction(t),!t){r.remove();return}if(dt(t)){this._putElementInTemplate(xt(t),r);return}if(this._config.html){r.innerHTML=this._maybeSanitize(t);return}r.textContent=t}}_maybeSanitize(e){return this._config.sanitize?Rh(e,this._config.allowList,this._config.sanitizeFn):e}_resolvePossibleFunction(e){return Pe(e,[void 0,this])}_putElementInTemplate(e,t){if(this._config.html){t.innerHTML="",t.append(e);return}t.textContent=e.textContent}},Vh="tooltip",zh=new Set(["sanitize","allowList","sanitizeFn"]),yo="fade",jh="modal",Ui="show",qh=".tooltip-inner",ra=`.${jh}`,oa="hide.bs.modal",ti="hover",bo="focus",_o="click",Wh="manual",Uh="hide",Yh="hidden",Gh="show",Kh="shown",Xh="inserted",Qh="click",Jh="focusin",Zh="focusout",ep="mouseenter",tp="mouseleave",np={AUTO:"auto",TOP:"top",RIGHT:Xe()?"left":"right",BOTTOM:"bottom",LEFT:Xe()?"right":"left"},ip={allowList:Pa,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"},rp={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"},ht=class n extends We{constructor(e,t){if(typeof zi=="undefined")throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)");super(e,t),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 ip}static get DefaultType(){return rp}static get NAME(){return Vh}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){if(this._isEnabled){if(this._isShown()){this._leave();return}this._enter()}}dispose(){clearTimeout(this._timeout),x.off(this._element.closest(ra),oa,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(this._element.style.display==="none")throw new Error("Please use show on visible elements");if(!(this._isWithContent()&&this._isEnabled))return;let e=x.trigger(this._element,this.constructor.eventName(Gh)),i=(ma(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(e.defaultPrevented||!i)return;this._disposePopper();let r=this._getTipElement();this._element.setAttribute("aria-describedby",r.getAttribute("id"));let{container:o}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(o.append(r),x.trigger(this._element,this.constructor.eventName(Xh))),this._popper=this._createPopper(r),r.classList.add(Ui),"ontouchstart"in document.documentElement)for(let a of[].concat(...document.body.children))x.on(a,"mouseover",Qi);let s=()=>{x.trigger(this._element,this.constructor.eventName(Kh)),this._isHovered===!1&&this._leave(),this._isHovered=!1};this._queueCallback(s,this.tip,this._isAnimated())}hide(){if(!this._isShown()||x.trigger(this._element,this.constructor.eventName(Uh)).defaultPrevented)return;if(this._getTipElement().classList.remove(Ui),"ontouchstart"in document.documentElement)for(let r of[].concat(...document.body.children))x.off(r,"mouseover",Qi);this._activeTrigger[_o]=!1,this._activeTrigger[bo]=!1,this._activeTrigger[ti]=!1,this._isHovered=null;let i=()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),x.trigger(this._element,this.constructor.eventName(Yh)))};this._queueCallback(i,this.tip,this._isAnimated())}update(){this._popper&&this._popper.update()}_isWithContent(){return!!this._getTitle()}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(e){let t=this._getTemplateFactory(e).toHtml();if(!t)return null;t.classList.remove(yo,Ui),t.classList.add(`bs-${this.constructor.NAME}-auto`);let i=Wu(this.constructor.NAME).toString();return t.setAttribute("id",i),this._isAnimated()&&t.classList.add(yo),t}setContent(e){this._newContent=e,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(e){return this._templateFactory?this._templateFactory.changeContent(e):this._templateFactory=new Lo(ae(O({},this._config),{content:e,extraClass:this._resolvePossibleFunction(this._config.customClass)})),this._templateFactory}_getContentForTemplate(){return{[qh]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(e){return this.constructor.getOrCreateInstance(e.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(yo)}_isShown(){return this.tip&&this.tip.classList.contains(Ui)}_createPopper(e){let t=Pe(this._config.placement,[this,e,this._element]),i=np[t.toUpperCase()];return Zn(this._element,e,this._getPopperConfig(i))}_getOffset(){let{offset:e}=this._config;return typeof e=="string"?e.split(",").map(t=>Number.parseInt(t,10)):typeof e=="function"?t=>e(t,this._element):e}_resolvePossibleFunction(e){return Pe(e,[this._element,this._element])}_getPopperConfig(e){let t={placement:e,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:i=>{this._getTipElement().setAttribute("data-popper-placement",i.state.placement)}}]};return O(O({},t),Pe(this._config.popperConfig,[void 0,t]))}_setListeners(){let e=this._config.trigger.split(" ");for(let t of e)if(t==="click")x.on(this._element,this.constructor.eventName(Qh),this._config.selector,i=>{let r=this._initializeOnDelegatedTarget(i);r._activeTrigger[_o]=!(r._isShown()&&r._activeTrigger[_o]),r.toggle()});else if(t!==Wh){let i=t===ti?this.constructor.eventName(ep):this.constructor.eventName(Jh),r=t===ti?this.constructor.eventName(tp):this.constructor.eventName(Zh);x.on(this._element,i,this._config.selector,o=>{let s=this._initializeOnDelegatedTarget(o);s._activeTrigger[o.type==="focusin"?bo:ti]=!0,s._enter()}),x.on(this._element,r,this._config.selector,o=>{let s=this._initializeOnDelegatedTarget(o);s._activeTrigger[o.type==="focusout"?bo:ti]=s._element.contains(o.relatedTarget),s._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},x.on(this._element.closest(ra),oa,this._hideModalHandler)}_fixTitle(){let e=this._element.getAttribute("title");e&&(!this._element.getAttribute("aria-label")&&!this._element.textContent.trim()&&this._element.setAttribute("aria-label",e),this._element.setAttribute("data-bs-original-title",e),this._element.removeAttribute("title"))}_enter(){if(this._isShown()||this._isHovered){this._isHovered=!0;return}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(e,t){clearTimeout(this._timeout),this._timeout=setTimeout(e,t)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(e){let t=ft.getDataAttributes(this._element);for(let i of Object.keys(t))zh.has(i)&&delete t[i];return e=O(O({},t),typeof e=="object"&&e?e:{}),e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e.container=e.container===!1?document.body:xt(e.container),typeof e.delay=="number"&&(e.delay={show:e.delay,hide:e.delay}),typeof e.title=="number"&&(e.title=e.title.toString()),typeof e.content=="number"&&(e.content=e.content.toString()),e}_getDelegateConfig(){let e={};for(let[t,i]of Object.entries(this._config))this.constructor.Default[t]!==i&&(e[t]=i);return e.selector=!1,e.trigger="manual",e}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this,e);if(typeof e=="string"){if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e]()}})}};Je(ht);var op="popover",sp=".popover-header",ap=".popover-body",lp=ae(O({},ht.Default),{content:"",offset:[0,8],placement:"right",template:'',trigger:"click"}),cp=ae(O({},ht.DefaultType),{content:"(null|string|element|function)"}),Sn=class n extends ht{static get Default(){return lp}static get DefaultType(){return cp}static get NAME(){return op}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[sp]:this._getTitle(),[ap]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this,e);if(typeof e=="string"){if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e]()}})}};Je(Sn);var up="scrollspy",dp="bs.scrollspy",Ho=`.${dp}`,fp=".data-api",hp=`activate${Ho}`,sa=`click${Ho}`,pp=`load${Ho}${fp}`,mp="dropdown-item",wn="active",gp='[data-bs-spy="scroll"]',wo="[href]",vp=".nav, .list-group",aa=".nav-link",Ep=".nav-item",yp=".list-group-item",bp=`${aa}, ${Ep} > ${aa}, ${yp}`,_p=".dropdown",wp=".dropdown-toggle",xp={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},Tp={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"},rr=class n extends We{constructor(e,t){super(e,t),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement=getComputedStyle(this._element).overflowY==="visible"?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return xp}static get DefaultType(){return Tp}static get NAME(){return up}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(let e of this._observableSections.values())this._observer.observe(e)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(e){return e.target=xt(e.target)||document.body,e.rootMargin=e.offset?`${e.offset}px 0px -30%`:e.rootMargin,typeof e.threshold=="string"&&(e.threshold=e.threshold.split(",").map(t=>Number.parseFloat(t))),e}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(x.off(this._config.target,sa),x.on(this._config.target,sa,wo,e=>{let t=this._observableSections.get(e.target.hash);if(t){e.preventDefault();let i=this._rootElement||window,r=t.offsetTop-this._element.offsetTop;if(i.scrollTo){i.scrollTo({top:r,behavior:"smooth"});return}i.scrollTop=r}}))}_getNewObserver(){let e={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver(t=>this._observerCallback(t),e)}_observerCallback(e){let t=s=>this._targetLinks.get(`#${s.target.id}`),i=s=>{this._previousScrollData.visibleEntryTop=s.target.offsetTop,this._process(t(s))},r=(this._rootElement||document.documentElement).scrollTop,o=r>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=r;for(let s of e){if(!s.isIntersecting){this._activeTarget=null,this._clearActiveClass(t(s));continue}let a=s.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(o&&a){if(i(s),!r)return;continue}!o&&!a&&i(s)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;let e=V.find(wo,this._config.target);for(let t of e){if(!t.hash||Tt(t))continue;let i=V.findOne(decodeURI(t.hash),this._element);An(i)&&(this._targetLinks.set(decodeURI(t.hash),t),this._observableSections.set(t.hash,i))}}_process(e){this._activeTarget!==e&&(this._clearActiveClass(this._config.target),this._activeTarget=e,e.classList.add(wn),this._activateParents(e),x.trigger(this._element,hp,{relatedTarget:e}))}_activateParents(e){if(e.classList.contains(mp)){V.findOne(wp,e.closest(_p)).classList.add(wn);return}for(let t of V.parents(e,vp))for(let i of V.prev(t,bp))i.classList.add(wn)}_clearActiveClass(e){e.classList.remove(wn);let t=V.find(`${wo}.${wn}`,e);for(let i of t)i.classList.remove(wn)}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this,e);if(typeof e=="string"){if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e]()}})}};x.on(window,pp,()=>{for(let n of V.find(gp))rr.getOrCreateInstance(n)});Je(rr);var Cp="tab",Sp="bs.tab",Zt=`.${Sp}`,Ap=`hide${Zt}`,Dp=`hidden${Zt}`,Op=`show${Zt}`,Lp=`shown${Zt}`,Mp=`click${Zt}`,Np=`keydown${Zt}`,kp=`load${Zt}`,Hp="ArrowLeft",la="ArrowRight",Ip="ArrowUp",ca="ArrowDown",xo="Home",ua="End",Kt="active",da="fade",To="show",Rp="dropdown",Fa=".dropdown-toggle",Pp=".dropdown-menu",Co=`:not(${Fa})`,Fp='.list-group, .nav, [role="tablist"]',$p=".nav-item, .list-group-item",Bp=`.nav-link${Co}, .list-group-item${Co}, [role="tab"]${Co}`,$a='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',So=`${Bp}, ${$a}`,Vp=`.${Kt}[data-bs-toggle="tab"], .${Kt}[data-bs-toggle="pill"], .${Kt}[data-bs-toggle="list"]`,Qt=class n extends We{constructor(e){super(e),this._parent=this._element.closest(Fp),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),x.on(this._element,Np,t=>this._keydown(t)))}static get NAME(){return Cp}show(){let e=this._element;if(this._elemIsActive(e))return;let t=this._getActiveElem(),i=t?x.trigger(t,Ap,{relatedTarget:e}):null;x.trigger(e,Op,{relatedTarget:t}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(t,e),this._activate(e,t))}_activate(e,t){if(!e)return;e.classList.add(Kt),this._activate(V.getElementFromSelector(e));let i=()=>{if(e.getAttribute("role")!=="tab"){e.classList.add(To);return}e.removeAttribute("tabindex"),e.setAttribute("aria-selected",!0),this._toggleDropDown(e,!0),x.trigger(e,Lp,{relatedTarget:t})};this._queueCallback(i,e,e.classList.contains(da))}_deactivate(e,t){if(!e)return;e.classList.remove(Kt),e.blur(),this._deactivate(V.getElementFromSelector(e));let i=()=>{if(e.getAttribute("role")!=="tab"){e.classList.remove(To);return}e.setAttribute("aria-selected",!1),e.setAttribute("tabindex","-1"),this._toggleDropDown(e,!1),x.trigger(e,Dp,{relatedTarget:t})};this._queueCallback(i,e,e.classList.contains(da))}_keydown(e){if(![Hp,la,Ip,ca,xo,ua].includes(e.key))return;e.stopPropagation(),e.preventDefault();let t=this._getChildren().filter(r=>!Tt(r)),i;if([xo,ua].includes(e.key))i=t[e.key===xo?0:t.length-1];else{let r=[la,ca].includes(e.key);i=Mo(t,e.target,r,!0)}i&&(i.focus({preventScroll:!0}),n.getOrCreateInstance(i).show())}_getChildren(){return V.find(So,this._parent)}_getActiveElem(){return this._getChildren().find(e=>this._elemIsActive(e))||null}_setInitialAttributes(e,t){this._setAttributeIfNotExists(e,"role","tablist");for(let i of t)this._setInitialAttributesOnChild(i)}_setInitialAttributesOnChild(e){e=this._getInnerElement(e);let t=this._elemIsActive(e),i=this._getOuterElement(e);e.setAttribute("aria-selected",t),i!==e&&this._setAttributeIfNotExists(i,"role","presentation"),t||e.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(e,"role","tab"),this._setInitialAttributesOnTargetPanel(e)}_setInitialAttributesOnTargetPanel(e){let t=V.getElementFromSelector(e);t&&(this._setAttributeIfNotExists(t,"role","tabpanel"),e.id&&this._setAttributeIfNotExists(t,"aria-labelledby",`${e.id}`))}_toggleDropDown(e,t){let i=this._getOuterElement(e);if(!i.classList.contains(Rp))return;let r=(o,s)=>{let a=V.findOne(o,i);a&&a.classList.toggle(s,t)};r(Fa,Kt),r(Pp,To),i.setAttribute("aria-expanded",t)}_setAttributeIfNotExists(e,t,i){e.hasAttribute(t)||e.setAttribute(t,i)}_elemIsActive(e){return e.classList.contains(Kt)}_getInnerElement(e){return e.matches(So)?e:V.findOne(So,e)}_getOuterElement(e){return e.closest($p)||e}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this);if(typeof e=="string"){if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e]()}})}};x.on(document,Mp,$a,function(n){["A","AREA"].includes(this.tagName)&&n.preventDefault(),!Tt(this)&&Qt.getOrCreateInstance(this).show()});x.on(window,kp,()=>{for(let n of V.find(Vp))Qt.getOrCreateInstance(n)});Je(Qt);var zp="toast",jp="bs.toast",Ot=`.${jp}`,qp=`mouseover${Ot}`,Wp=`mouseout${Ot}`,Up=`focusin${Ot}`,Yp=`focusout${Ot}`,Gp=`hide${Ot}`,Kp=`hidden${Ot}`,Xp=`show${Ot}`,Qp=`shown${Ot}`,Jp="fade",fa="hide",Yi="show",Gi="showing",Zp={animation:"boolean",autohide:"boolean",delay:"number"},em={animation:!0,autohide:!0,delay:5e3},pt=class n extends We{constructor(e,t){super(e,t),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return em}static get DefaultType(){return Zp}static get NAME(){return zp}show(){if(x.trigger(this._element,Xp).defaultPrevented)return;this._clearTimeout(),this._config.animation&&this._element.classList.add(Jp);let t=()=>{this._element.classList.remove(Gi),x.trigger(this._element,Qp),this._maybeScheduleHide()};this._element.classList.remove(fa),ri(this._element),this._element.classList.add(Yi,Gi),this._queueCallback(t,this._element,this._config.animation)}hide(){if(!this.isShown()||x.trigger(this._element,Gp).defaultPrevented)return;let t=()=>{this._element.classList.add(fa),this._element.classList.remove(Gi,Yi),x.trigger(this._element,Kp)};this._element.classList.add(Gi),this._queueCallback(t,this._element,this._config.animation)}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(Yi),super.dispose()}isShown(){return this._element.classList.contains(Yi)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(e,t){switch(e.type){case"mouseover":case"mouseout":{this._hasMouseInteraction=t;break}case"focusin":case"focusout":{this._hasKeyboardInteraction=t;break}}if(t){this._clearTimeout();return}let i=e.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){x.on(this._element,qp,e=>this._onInteraction(e,!0)),x.on(this._element,Wp,e=>this._onInteraction(e,!1)),x.on(this._element,Up,e=>this._onInteraction(e,!0)),x.on(this._element,Yp,e=>this._onInteraction(e,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this,e);if(typeof e=="string"){if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e](this)}})}};or(pt);Je(pt);var tm=(function(){"use strict";let htmx={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(n,e){return getInputValues(n,e||"post").values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:!0,historyCacheSize:10,refreshOnHistoryMiss:!1,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:!0,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:!0,allowScriptTags:!0,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:!1,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:!1,getCacheBusterParam:!1,globalViewTransitions:!1,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:!0,ignoreTitle:!1,scrollIntoViewOnBoost:!0,triggerSpecsCache:null,disableInheritance:!1,responseHandling:[{code:"204",swap:!1},{code:"[23]..",swap:!0},{code:"[45]..",swap:!1,error:!0}],allowNestedOobSwaps:!0,historyRestoreAsHxRequest:!0,reportValidityOfForms:!1},parseInterval:null,location,_:null,version:"2.0.8"};htmx.onLoad=onLoadHelper,htmx.process=processNode,htmx.on=addEventListenerImpl,htmx.off=removeEventListenerImpl,htmx.trigger=triggerEvent,htmx.ajax=ajaxHelper,htmx.find=find,htmx.findAll=findAll,htmx.closest=closest,htmx.remove=removeElement,htmx.addClass=addClassToElement,htmx.removeClass=removeClassFromElement,htmx.toggleClass=toggleClassOnElement,htmx.takeClass=takeClassForElement,htmx.swap=swap,htmx.defineExtension=defineExtension,htmx.removeExtension=removeExtension,htmx.logAll=logAll,htmx.logNone=logNone,htmx.parseInterval=parseInterval,htmx._=internalEval;let internalAPI={addTriggerHandler,bodyContains,canAccessLocalStorage,findThisElement,filterValues,swap,hasAttribute,getAttributeValue,getClosestAttributeValue,getClosestMatch,getExpressionVars,getHeaders,getInputValues,getInternalData,getSwapSpecification,getTriggerSpecs,getTarget,makeFragment,mergeObjects,makeSettleInfo,oobSwap,querySelectorExt,settleImmediately,shouldCancel,triggerEvent,triggerErrorEvent,withExtensions},VERBS=["get","post","put","delete","patch"],VERB_SELECTOR=VERBS.map(function(n){return"[hx-"+n+"], [data-hx-"+n+"]"}).join(", ");function parseInterval(n){if(n==null)return;let e=NaN;return n.slice(-2)=="ms"?e=parseFloat(n.slice(0,-2)):n.slice(-1)=="s"?e=parseFloat(n.slice(0,-1))*1e3:n.slice(-1)=="m"?e=parseFloat(n.slice(0,-1))*1e3*60:e=parseFloat(n),isNaN(e)?void 0:e}function getRawAttribute(n,e){return n instanceof Element&&n.getAttribute(e)}function hasAttribute(n,e){return!!n.hasAttribute&&(n.hasAttribute(e)||n.hasAttribute("data-"+e))}function getAttributeValue(n,e){return getRawAttribute(n,e)||getRawAttribute(n,"data-"+e)}function parentElt(n){let e=n.parentElement;return!e&&n.parentNode instanceof ShadowRoot?n.parentNode:e}function getDocument(){return document}function getRootNode(n,e){return n.getRootNode?n.getRootNode({composed:e}):getDocument()}function getClosestMatch(n,e){for(;n&&!e(n);)n=parentElt(n);return n||null}function getAttributeValueWithDisinheritance(n,e,t){let i=getAttributeValue(e,t),r=getAttributeValue(e,"hx-disinherit");var o=getAttributeValue(e,"hx-inherit");if(n!==e){if(htmx.config.disableInheritance)return o&&(o==="*"||o.split(" ").indexOf(t)>=0)?i:null;if(r&&(r==="*"||r.split(" ").indexOf(t)>=0))return"unset"}return i}function getClosestAttributeValue(n,e){let t=null;if(getClosestMatch(n,function(i){return!!(t=getAttributeValueWithDisinheritance(n,asElement(i),e))}),t!=="unset")return t}function matches(n,e){return n instanceof Element&&n.matches(e)}function getStartTag(n){let t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i.exec(n);return t?t[1].toLowerCase():""}function parseHTML(n){return"parseHTMLUnsafe"in Document?Document.parseHTMLUnsafe(n):new DOMParser().parseFromString(n,"text/html")}function takeChildrenFor(n,e){for(;e.childNodes.length>0;)n.append(e.childNodes[0])}function duplicateScript(n){let e=getDocument().createElement("script");return forEach(n.attributes,function(t){e.setAttribute(t.name,t.value)}),e.textContent=n.textContent,e.async=!1,htmx.config.inlineScriptNonce&&(e.nonce=htmx.config.inlineScriptNonce),e}function isJavaScriptScriptNode(n){return n.matches("script")&&(n.type==="text/javascript"||n.type==="module"||n.type==="")}function normalizeScriptTags(n){Array.from(n.querySelectorAll("script")).forEach(e=>{if(isJavaScriptScriptNode(e)){let t=duplicateScript(e),i=e.parentNode;try{i.insertBefore(t,e)}catch(r){logError(r)}finally{e.remove()}}})}function makeFragment(n){let e=n.replace(/]*)?>[\s\S]*?<\/head>/i,""),t=getStartTag(e),i;if(t==="html"){i=new DocumentFragment;let o=parseHTML(n);takeChildrenFor(i,o.body),i.title=o.title}else if(t==="body"){i=new DocumentFragment;let o=parseHTML(e);takeChildrenFor(i,o.body),i.title=o.title}else{let o=parseHTML('");i=o.querySelector("template").content,i.title=o.title;var r=i.querySelector("title");r&&r.parentNode===i&&(r.remove(),i.title=r.innerText)}return i&&(htmx.config.allowScriptTags?normalizeScriptTags(i):i.querySelectorAll("script").forEach(o=>o.remove())),i}function maybeCall(n){n&&n()}function isType(n,e){return Object.prototype.toString.call(n)==="[object "+e+"]"}function isFunction(n){return typeof n=="function"}function isRawObject(n){return isType(n,"Object")}function getInternalData(n){let e="htmx-internal-data",t=n[e];return t||(t=n[e]={}),t}function toArray(n){let e=[];if(n)for(let t=0;t=0}function bodyContains(n){return n.getRootNode({composed:!0})===document}function splitOnWhitespace(n){return n.trim().split(/\s+/)}function mergeObjects(n,e){for(let t in e)e.hasOwnProperty(t)&&(n[t]=e[t]);return n}function parseJSON(n){try{return JSON.parse(n)}catch(e){return logError(e),null}}function canAccessLocalStorage(){let n="htmx:sessionStorageTest";try{return sessionStorage.setItem(n,n),sessionStorage.removeItem(n),!0}catch(e){return!1}}function normalizePath(n){let e=new URL(n,"http://x");return e&&(n=e.pathname+e.search),n!="/"&&(n=n.replace(/\/+$/,"")),n}function internalEval(str){return maybeEval(getDocument().body,function(){return eval(str)})}function onLoadHelper(n){return htmx.on("htmx:load",function(t){n(t.detail.elt)})}function logAll(){htmx.logger=function(n,e,t){console&&console.log(e,n,t)}}function logNone(){htmx.logger=null}function find(n,e){return typeof n!="string"?n.querySelector(e):find(getDocument(),n)}function findAll(n,e){return typeof n!="string"?n.querySelectorAll(e):findAll(getDocument(),n)}function getWindow(){return window}function removeElement(n,e){n=resolveTarget(n),e?getWindow().setTimeout(function(){removeElement(n),n=null},e):parentElt(n).removeChild(n)}function asElement(n){return n instanceof Element?n:null}function asHtmlElement(n){return n instanceof HTMLElement?n:null}function asString(n){return typeof n=="string"?n:null}function asParentNode(n){return n instanceof Element||n instanceof Document||n instanceof DocumentFragment?n:null}function addClassToElement(n,e,t){n=asElement(resolveTarget(n)),n&&(t?getWindow().setTimeout(function(){addClassToElement(n,e),n=null},t):n.classList&&n.classList.add(e))}function removeClassFromElement(n,e,t){let i=asElement(resolveTarget(n));i&&(t?getWindow().setTimeout(function(){removeClassFromElement(i,e),i=null},t):i.classList&&(i.classList.remove(e),i.classList.length===0&&i.removeAttribute("class")))}function toggleClassOnElement(n,e){n=resolveTarget(n),n.classList.toggle(e)}function takeClassForElement(n,e){n=resolveTarget(n),forEach(n.parentElement.children,function(t){removeClassFromElement(t,e)}),addClassToElement(asElement(n),e)}function closest(n,e){return n=asElement(resolveTarget(n)),n?n.closest(e):null}function startsWith(n,e){return n.substring(0,e.length)===e}function endsWith(n,e){return n.substring(n.length-e.length)===e}function normalizeSelector(n){let e=n.trim();return startsWith(e,"<")&&endsWith(e,"/>")?e.substring(1,e.length-2):e}function querySelectorAllExt(n,e,t){if(e.indexOf("global ")===0)return querySelectorAllExt(n,e.slice(7),!0);n=resolveTarget(n);let i=[];{let s=0,a=0;for(let l=0;l"&&s--}a0;){let s=normalizeSelector(i.shift()),a;s.indexOf("closest ")===0?a=closest(asElement(n),normalizeSelector(s.slice(8))):s.indexOf("find ")===0?a=find(asParentNode(n),normalizeSelector(s.slice(5))):s==="next"||s==="nextElementSibling"?a=asElement(n).nextElementSibling:s.indexOf("next ")===0?a=scanForwardQuery(n,normalizeSelector(s.slice(5)),!!t):s==="previous"||s==="previousElementSibling"?a=asElement(n).previousElementSibling:s.indexOf("previous ")===0?a=scanBackwardsQuery(n,normalizeSelector(s.slice(9)),!!t):s==="document"?a=document:s==="window"?a=window:s==="body"?a=document.body:s==="root"?a=getRootNode(n,!!t):s==="host"?a=n.getRootNode().host:o.push(s),a&&r.push(a)}if(o.length>0){let s=o.join(","),a=asParentNode(getRootNode(n,!!t));r.push(...toArray(a.querySelectorAll(s)))}return r}var scanForwardQuery=function(n,e,t){let i=asParentNode(getRootNode(n,t)).querySelectorAll(e);for(let r=0;r=0;r--){let o=i[r];if(o.compareDocumentPosition(n)===Node.DOCUMENT_POSITION_FOLLOWING)return o}};function querySelectorExt(n,e){return typeof n!="string"?querySelectorAllExt(n,e)[0]:querySelectorAllExt(getDocument().body,n)[0]}function resolveTarget(n,e){return typeof n=="string"?find(asParentNode(e)||document,n):n}function processEventArgs(n,e,t,i){return isFunction(e)?{target:getDocument().body,event:asString(n),listener:e,options:t}:{target:resolveTarget(n),event:asString(e),listener:t,options:i}}function addEventListenerImpl(n,e,t,i){return ready(function(){let o=processEventArgs(n,e,t,i);o.target.addEventListener(o.event,o.listener,o.options)}),isFunction(e)?e:t}function removeEventListenerImpl(n,e,t){return ready(function(){let i=processEventArgs(n,e,t);i.target.removeEventListener(i.event,i.listener)}),isFunction(e)?e:t}let DUMMY_ELT=getDocument().createElement("output");function findAttributeTargets(n,e){let t=getClosestAttributeValue(n,e);if(t){if(t==="this")return[findThisElement(n,e)];{let i=querySelectorAllExt(n,t);if(/(^|,)(\s*)inherit(\s*)($|,)/.test(t)){let o=asElement(getClosestMatch(n,function(s){return s!==n&&hasAttribute(asElement(s),e)}));o&&i.push(...findAttributeTargets(o,e))}return i.length===0?(logError('The selector "'+t+'" on '+e+" returned no matches!"),[DUMMY_ELT]):i}}}function findThisElement(n,e){return asElement(getClosestMatch(n,function(t){return getAttributeValue(asElement(t),e)!=null}))}function getTarget(n){let e=getClosestAttributeValue(n,"hx-target");return e?e==="this"?findThisElement(n,"hx-target"):querySelectorExt(n,e):getInternalData(n).boosted?getDocument().body:n}function shouldSettleAttribute(n){return htmx.config.attributesToSettle.includes(n)}function cloneAttributes(n,e){forEach(Array.from(n.attributes),function(t){!e.hasAttribute(t.name)&&shouldSettleAttribute(t.name)&&n.removeAttribute(t.name)}),forEach(e.attributes,function(t){shouldSettleAttribute(t.name)&&n.setAttribute(t.name,t.value)})}function isInlineSwap(n,e){let t=getExtensions(e);for(let i=0;i0?(o=n.substring(0,n.indexOf(":")),r=n.substring(n.indexOf(":")+1)):o=n),e.removeAttribute("hx-swap-oob"),e.removeAttribute("data-hx-swap-oob");let s=querySelectorAllExt(i,r,!1);return s.length?(forEach(s,function(a){let l,c=e.cloneNode(!0);l=getDocument().createDocumentFragment(),l.appendChild(c),isInlineSwap(o,a)||(l=asParentNode(c));let u={shouldSwap:!0,target:a,fragment:l};triggerEvent(a,"htmx:oobBeforeSwap",u)&&(a=u.target,u.shouldSwap&&(handlePreservedElements(l),swapWithStyle(o,a,a,l,t),restorePreservedElements()),forEach(t.elts,function(d){triggerEvent(d,"htmx:oobAfterSwap",u)}))}),e.parentNode.removeChild(e)):(e.parentNode.removeChild(e),triggerErrorEvent(getDocument().body,"htmx:oobErrorNoTarget",{content:e})),n}function restorePreservedElements(){let n=find("#--htmx-preserve-pantry--");if(n){for(let e of[...n.children]){let t=find("#"+e.id);t.parentNode.moveBefore(e,t),t.remove()}n.remove()}}function handlePreservedElements(n){forEach(findAll(n,"[hx-preserve], [data-hx-preserve]"),function(e){let t=getAttributeValue(e,"id"),i=getDocument().getElementById(t);if(i!=null)if(e.moveBefore){let r=find("#--htmx-preserve-pantry--");r==null&&(getDocument().body.insertAdjacentHTML("afterend","
"),r=find("#--htmx-preserve-pantry--")),r.moveBefore(i,null)}else e.parentNode.replaceChild(i,e)})}function handleAttributes(n,e,t){forEach(e.querySelectorAll("[id]"),function(i){let r=getRawAttribute(i,"id");if(r&&r.length>0){let o=r.replace("'","\\'"),s=i.tagName.replace(":","\\:"),a=asParentNode(n),l=a&&a.querySelector(s+"[id='"+o+"']");if(l&&l!==a){let c=i.cloneNode();cloneAttributes(i,l),t.tasks.push(function(){cloneAttributes(i,c)})}}})}function makeAjaxLoadTask(n){return function(){removeClassFromElement(n,htmx.config.addedClass),processNode(asElement(n)),processFocus(asParentNode(n)),triggerEvent(n,"htmx:load")}}function processFocus(n){let e="[autofocus]",t=asHtmlElement(matches(n,e)?n:n.querySelector(e));t!=null&&t.focus()}function insertNodesBefore(n,e,t,i){for(handleAttributes(n,t,i);t.childNodes.length>0;){let r=t.firstChild;addClassToElement(asElement(r),htmx.config.addedClass),n.insertBefore(r,e),r.nodeType!==Node.TEXT_NODE&&r.nodeType!==Node.COMMENT_NODE&&i.tasks.push(makeAjaxLoadTask(r))}}function stringHash(n,e){let t=0;for(;t0}function swap(n,e,t,i){i||(i={});let r=null,o=null,s=function(){maybeCall(i.beforeSwapCallback),n=resolveTarget(n);let c=i.contextElement?getRootNode(i.contextElement,!1):getDocument(),u=document.activeElement,d={};d={elt:u,start:u?u.selectionStart:null,end:u?u.selectionEnd:null};let p=makeSettleInfo(n);if(t.swapStyle==="textContent")n.textContent=e;else{let m=makeFragment(e);if(p.title=i.title||m.title,i.historyRequest&&(m=m.querySelector("[hx-history-elt],[data-hx-history-elt]")||m),i.selectOOB){let v=i.selectOOB.split(",");for(let _=0;_0?getWindow().setTimeout(E,t.settleDelay):E()},a=htmx.config.globalViewTransitions;t.hasOwnProperty("transition")&&(a=t.transition);let l=i.contextElement||getDocument();if(a&&triggerEvent(l,"htmx:beforeTransition",i.eventInfo)&&typeof Promise!="undefined"&&document.startViewTransition){let c=new Promise(function(d,p){r=d,o=p}),u=s;s=function(){document.startViewTransition(function(){return u(),c})}}try{t!=null&&t.swapDelay&&t.swapDelay>0?getWindow().setTimeout(s,t.swapDelay):s()}catch(c){throw triggerErrorEvent(l,"htmx:swapError",i.eventInfo),maybeCall(o),c}}function handleTriggerHeader(n,e,t){let i=n.getResponseHeader(e);if(i.indexOf("{")===0){let r=parseJSON(i);for(let o in r)if(r.hasOwnProperty(o)){let s=r[o];isRawObject(s)?t=s.target!==void 0?s.target:t:s={value:s},triggerEvent(t,o,s)}}else{let r=i.split(",");for(let o=0;o0;){let s=e[0];if(s==="]"){if(i--,i===0){o===null&&(r=r+"true"),e.shift(),r+=")})";try{let a=maybeEval(n,function(){return Function(r)()},function(){return!0});return a.source=r,a}catch(a){return triggerErrorEvent(getDocument().body,"htmx:syntax:error",{error:a,source:r}),null}}}else s==="["&&i++;isPossibleRelativeReference(s,o,t)?r+="(("+t+"."+s+") ? ("+t+"."+s+") : (window."+s+"))":r=r+s,o=e.shift()}}}function consumeUntil(n,e){let t="";for(;n.length>0&&!e.test(n[0]);)t+=n.shift();return t}function consumeCSSSelector(n){let e;return n.length>0&&COMBINED_SELECTOR_START.test(n[0])?(n.shift(),e=consumeUntil(n,COMBINED_SELECTOR_END).trim(),n.shift()):e=consumeUntil(n,WHITESPACE_OR_COMMA),e}let INPUT_SELECTOR="input, textarea, select";function parseAndCacheTrigger(n,e,t){let i=[],r=tokenizeString(e);do{consumeUntil(r,NOT_WHITESPACE);let a=r.length,l=consumeUntil(r,/[,\[\s]/);if(l!=="")if(l==="every"){let c={trigger:"every"};consumeUntil(r,NOT_WHITESPACE),c.pollInterval=parseInterval(consumeUntil(r,/[,\[\s]/)),consumeUntil(r,NOT_WHITESPACE);var o=maybeGenerateConditional(n,r,"event");o&&(c.eventFilter=o),i.push(c)}else{let c={trigger:l};var o=maybeGenerateConditional(n,r,"event");for(o&&(c.eventFilter=o),consumeUntil(r,NOT_WHITESPACE);r.length>0&&r[0]!==",";){let d=r.shift();if(d==="changed")c.changed=!0;else if(d==="once")c.once=!0;else if(d==="consume")c.consume=!0;else if(d==="delay"&&r[0]===":")r.shift(),c.delay=parseInterval(consumeUntil(r,WHITESPACE_OR_COMMA));else if(d==="from"&&r[0]===":"){if(r.shift(),COMBINED_SELECTOR_START.test(r[0]))var s=consumeCSSSelector(r);else{var s=consumeUntil(r,WHITESPACE_OR_COMMA);if(s==="closest"||s==="find"||s==="next"||s==="previous"){r.shift();let E=consumeCSSSelector(r);E.length>0&&(s+=" "+E)}}c.from=s}else d==="target"&&r[0]===":"?(r.shift(),c.target=consumeCSSSelector(r)):d==="throttle"&&r[0]===":"?(r.shift(),c.throttle=parseInterval(consumeUntil(r,WHITESPACE_OR_COMMA))):d==="queue"&&r[0]===":"?(r.shift(),c.queue=consumeUntil(r,WHITESPACE_OR_COMMA)):d==="root"&&r[0]===":"?(r.shift(),c[d]=consumeCSSSelector(r)):d==="threshold"&&r[0]===":"?(r.shift(),c[d]=consumeUntil(r,WHITESPACE_OR_COMMA)):triggerErrorEvent(n,"htmx:syntax:error",{token:r.shift()});consumeUntil(r,NOT_WHITESPACE)}i.push(c)}r.length===a&&triggerErrorEvent(n,"htmx:syntax:error",{token:r.shift()}),consumeUntil(r,NOT_WHITESPACE)}while(r[0]===","&&r.shift());return t&&(t[e]=i),i}function getTriggerSpecs(n){let e=getAttributeValue(n,"hx-trigger"),t=[];if(e){let i=htmx.config.triggerSpecsCache;t=i&&i[e]||parseAndCacheTrigger(n,e,i)}return t.length>0?t:matches(n,"form")?[{trigger:"submit"}]:matches(n,'input[type="button"], input[type="submit"]')?[{trigger:"click"}]:matches(n,INPUT_SELECTOR)?[{trigger:"change"}]:[{trigger:"click"}]}function cancelPolling(n){getInternalData(n).cancelled=!0}function processPolling(n,e,t){let i=getInternalData(n);i.timeout=getWindow().setTimeout(function(){bodyContains(n)&&i.cancelled!==!0&&(maybeFilterEvent(t,n,makeEvent("hx:poll:trigger",{triggerSpec:t,target:n}))||e(n),processPolling(n,e,t))},t.pollInterval)}function isLocalLink(n){return location.hostname===n.hostname&&getRawAttribute(n,"href")&&getRawAttribute(n,"href").indexOf("#")!==0}function eltIsDisabled(n){return closest(n,htmx.config.disableSelector)}function boostElement(n,e,t){if(n instanceof HTMLAnchorElement&&isLocalLink(n)&&(n.target===""||n.target==="_self")||n.tagName==="FORM"&&String(getRawAttribute(n,"method")).toLowerCase()!=="dialog"){e.boosted=!0;let i,r;if(n.tagName==="A")i="get",r=getRawAttribute(n,"href");else{let o=getRawAttribute(n,"method");i=o?o.toLowerCase():"get",r=getRawAttribute(n,"action"),(r==null||r==="")&&(r=location.href),i==="get"&&r.includes("?")&&(r=r.replace(/\?[^#]+/,""))}t.forEach(function(o){addEventListener(n,function(s,a){let l=asElement(s);if(eltIsDisabled(l)){cleanUpElement(l);return}issueAjaxRequest(i,r,l,a)},e,o,!0)})}}function shouldCancel(n,e){if(n.type==="submit"&&e.tagName==="FORM")return!0;if(n.type==="click"){let t=e.closest('input[type="submit"], button');if(t&&t.form&&t.type==="submit")return!0;let i=e.closest("a"),r=/^#.+/;if(i&&i.href&&!r.test(i.getAttribute("href")))return!0}return!1}function ignoreBoostedAnchorCtrlClick(n,e){return getInternalData(n).boosted&&n instanceof HTMLAnchorElement&&e.type==="click"&&(e.ctrlKey||e.metaKey)}function maybeFilterEvent(n,e,t){let i=n.eventFilter;if(i)try{return i.call(e,t)!==!0}catch(r){let o=i.source;return triggerErrorEvent(getDocument().body,"htmx:eventFilter:error",{error:r,source:o}),!0}return!1}function addEventListener(n,e,t,i,r){let o=getInternalData(n),s;i.from?s=querySelectorAllExt(n,i.from):s=[n],i.changed&&("lastValue"in o||(o.lastValue=new WeakMap),s.forEach(function(a){o.lastValue.has(i)||o.lastValue.set(i,new WeakMap),o.lastValue.get(i).set(a,a.value)})),forEach(s,function(a){let l=function(c){if(!bodyContains(n)){a.removeEventListener(i.trigger,l);return}if(ignoreBoostedAnchorCtrlClick(n,c)||((r||shouldCancel(c,a))&&c.preventDefault(),maybeFilterEvent(i,n,c)))return;let u=getInternalData(c);if(u.triggerSpec=i,u.handledFor==null&&(u.handledFor=[]),u.handledFor.indexOf(n)<0){if(u.handledFor.push(n),i.consume&&c.stopPropagation(),i.target&&c.target&&!matches(asElement(c.target),i.target))return;if(i.once){if(o.triggeredOnce)return;o.triggeredOnce=!0}if(i.changed){let d=c.target,p=d.value,E=o.lastValue.get(i);if(E.has(d)&&E.get(d)===p)return;E.set(d,p)}if(o.delayed&&clearTimeout(o.delayed),o.throttle)return;i.throttle>0?o.throttle||(triggerEvent(n,"htmx:trigger"),e(n,c),o.throttle=getWindow().setTimeout(function(){o.throttle=null},i.throttle)):i.delay>0?o.delayed=getWindow().setTimeout(function(){triggerEvent(n,"htmx:trigger"),e(n,c)},i.delay):(triggerEvent(n,"htmx:trigger"),e(n,c))}};t.listenerInfos==null&&(t.listenerInfos=[]),t.listenerInfos.push({trigger:i.trigger,listener:l,on:a}),a.addEventListener(i.trigger,l)})}let windowIsScrolling=!1,scrollHandler=null;function initScrollHandler(){scrollHandler||(scrollHandler=function(){windowIsScrolling=!0},window.addEventListener("scroll",scrollHandler),window.addEventListener("resize",scrollHandler),setInterval(function(){windowIsScrolling&&(windowIsScrolling=!1,forEach(getDocument().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(n){maybeReveal(n)}))},200))}function maybeReveal(n){!hasAttribute(n,"data-hx-revealed")&&isScrolledIntoView(n)&&(n.setAttribute("data-hx-revealed","true"),getInternalData(n).initHash?triggerEvent(n,"revealed"):n.addEventListener("htmx:afterProcessNode",function(){triggerEvent(n,"revealed")},{once:!0}))}function loadImmediately(n,e,t,i){let r=function(){t.loaded||(t.loaded=!0,triggerEvent(n,"htmx:trigger"),e(n))};i>0?getWindow().setTimeout(r,i):r()}function processVerbs(n,e,t){let i=!1;return forEach(VERBS,function(r){if(hasAttribute(n,"hx-"+r)){let o=getAttributeValue(n,"hx-"+r);i=!0,e.path=o,e.verb=r,t.forEach(function(s){addTriggerHandler(n,s,e,function(a,l){let c=asElement(a);if(eltIsDisabled(c)){cleanUpElement(c);return}issueAjaxRequest(r,o,c,l)})})}}),i}function addTriggerHandler(n,e,t,i){if(e.trigger==="revealed")initScrollHandler(),addEventListener(n,i,t,e),maybeReveal(asElement(n));else if(e.trigger==="intersect"){let r={};e.root&&(r.root=querySelectorExt(n,e.root)),e.threshold&&(r.threshold=parseFloat(e.threshold)),new IntersectionObserver(function(s){for(let a=0;a0?(t.polling=!0,processPolling(asElement(n),i,e)):addEventListener(n,i,t,e)}function shouldProcessHxOn(n){let e=asElement(n);if(!e)return!1;let t=e.attributes;for(let i=0;i", "+o).join(""))}else return[]}function maybeSetLastButtonClicked(n){let e=getTargetButton(n.target),t=getRelatedFormData(n);t&&(t.lastButtonClicked=e)}function maybeUnsetLastButtonClicked(n){let e=getRelatedFormData(n);e&&(e.lastButtonClicked=null)}function getTargetButton(n){return closest(asElement(n),"button, input[type='submit']")}function getRelatedForm(n){return n.form||closest(n,"form")}function getRelatedFormData(n){let e=getTargetButton(n.target);if(!e)return;let t=getRelatedForm(e);if(t)return getInternalData(t)}function initButtonTracking(n){n.addEventListener("click",maybeSetLastButtonClicked),n.addEventListener("focusin",maybeSetLastButtonClicked),n.addEventListener("focusout",maybeUnsetLastButtonClicked)}function addHxOnEventHandler(n,e,t){let i=getInternalData(n);Array.isArray(i.onHandlers)||(i.onHandlers=[]);let r,o=function(s){maybeEval(n,function(){eltIsDisabled(n)||(r||(r=new Function("event",t)),r.call(n,s))})};n.addEventListener(e,o),i.onHandlers.push({event:e,listener:o})}function processHxOnWildcard(n){deInitOnHandlers(n);for(let e=0;ehtmx.config.historyCacheSize;)o.shift();for(;o.length>0;)try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(o));break}catch(a){triggerErrorEvent(getDocument().body,"htmx:historyCacheError",{cause:a,cache:o}),o.shift()}}function getCachedHistory(n){if(!canAccessLocalStorage())return null;n=normalizePath(n);let e=parseJSON(sessionStorage.getItem("htmx-history-cache"))||[];for(let t=0;t=200&&this.status<400?(i.response=this.response,triggerEvent(getDocument().body,"htmx:historyCacheMissLoad",i),swap(i.historyElt,i.response,t,{contextElement:i.historyElt,historyRequest:!0}),setCurrentPathForHistory(i.path),triggerEvent(getDocument().body,"htmx:historyRestore",{path:n,cacheMiss:!0,serverResponse:i.response})):triggerErrorEvent(getDocument().body,"htmx:historyCacheMissLoadError",i)},triggerEvent(getDocument().body,"htmx:historyCacheMiss",i)&&e.send()}function restoreHistory(n){saveCurrentPageToHistory(),n=n||location.pathname+location.search;let e=getCachedHistory(n);if(e){let t={swapStyle:"innerHTML",swapDelay:0,settleDelay:0,scroll:e.scroll},i={path:n,item:e,historyElt:getHistoryElement(),swapSpec:t};triggerEvent(getDocument().body,"htmx:historyCacheHit",i)&&(swap(i.historyElt,e.content,t,{contextElement:i.historyElt,title:e.title}),setCurrentPathForHistory(i.path),triggerEvent(getDocument().body,"htmx:historyRestore",i))}else htmx.config.refreshOnHistoryMiss?htmx.location.reload(!0):loadHistoryFromServer(n)}function addRequestIndicatorClasses(n){let e=findAttributeTargets(n,"hx-indicator");return e==null&&(e=[n]),forEach(e,function(t){let i=getInternalData(t);i.requestCount=(i.requestCount||0)+1,t.classList.add.call(t.classList,htmx.config.requestClass)}),e}function disableElements(n){let e=findAttributeTargets(n,"hx-disabled-elt");return e==null&&(e=[]),forEach(e,function(t){let i=getInternalData(t);i.requestCount=(i.requestCount||0)+1,t.setAttribute("disabled",""),t.setAttribute("data-disabled-by-htmx","")}),e}function removeRequestIndicators(n,e){forEach(n.concat(e),function(t){let i=getInternalData(t);i.requestCount=(i.requestCount||1)-1}),forEach(n,function(t){getInternalData(t).requestCount===0&&t.classList.remove.call(t.classList,htmx.config.requestClass)}),forEach(e,function(t){getInternalData(t).requestCount===0&&(t.removeAttribute("disabled"),t.removeAttribute("data-disabled-by-htmx"))})}function haveSeenNode(n,e){for(let t=0;te.indexOf(r)<0):i=i.filter(r=>r!==e),t.delete(n),forEach(i,r=>t.append(n,r))}}function getValueFromInput(n){return n instanceof HTMLSelectElement&&n.multiple?toArray(n.querySelectorAll("option:checked")).map(function(e){return e.value}):n instanceof HTMLInputElement&&n.files?toArray(n.files):n.value}function processInputValue(n,e,t,i,r){if(!(i==null||haveSeenNode(n,i))){if(n.push(i),shouldInclude(i)){let o=getRawAttribute(i,"name");addValueToFormData(o,getValueFromInput(i),e),r&&validateElement(i,t)}i instanceof HTMLFormElement&&(forEach(i.elements,function(o){n.indexOf(o)>=0?removeValueFromFormData(o.name,getValueFromInput(o),e):n.push(o),r&&validateElement(o,t)}),new FormData(i).forEach(function(o,s){o instanceof File&&o.name===""||addValueToFormData(s,o,e)}))}}function validateElement(n,e){let t=n;t.willValidate&&(triggerEvent(t,"htmx:validation:validate"),t.checkValidity()||(triggerEvent(t,"htmx:validation:failed",{message:t.validationMessage,validity:t.validity})&&!e.length&&htmx.config.reportValidityOfForms&&t.reportValidity(),e.push({elt:t,message:t.validationMessage,validity:t.validity})))}function overrideFormData(n,e){for(let t of e.keys())n.delete(t);return e.forEach(function(t,i){n.append(i,t)}),n}function getInputValues(n,e){let t=[],i=new FormData,r=new FormData,o=[],s=getInternalData(n);s.lastButtonClicked&&!bodyContains(s.lastButtonClicked)&&(s.lastButtonClicked=null);let a=n instanceof HTMLFormElement&&n.noValidate!==!0||getAttributeValue(n,"hx-validate")==="true";if(s.lastButtonClicked&&(a=a&&s.lastButtonClicked.formNoValidate!==!0),e!=="get"&&processInputValue(t,r,o,getRelatedForm(n),a),processInputValue(t,i,o,n,a),s.lastButtonClicked||n.tagName==="BUTTON"||n.tagName==="INPUT"&&getRawAttribute(n,"type")==="submit"){let c=s.lastButtonClicked||n,u=getRawAttribute(c,"name");addValueToFormData(u,c.value,r)}let l=findAttributeTargets(n,"hx-include");return forEach(l,function(c){processInputValue(t,i,o,asElement(c),a),matches(c,"form")||forEach(asParentNode(c).querySelectorAll(INPUT_SELECTOR),function(u){processInputValue(t,i,o,u,a)})}),overrideFormData(i,r),{errors:o,formData:i,values:formDataProxy(i)}}function appendParam(n,e,t){n!==""&&(n+="&"),String(t)==="[object Object]"&&(t=JSON.stringify(t));let i=encodeURIComponent(t);return n+=encodeURIComponent(e)+"="+i,n}function urlEncode(n){n=formDataFromObject(n);let e="";return n.forEach(function(t,i){e=appendParam(e,i,t)}),e}function getHeaders(n,e,t){let i={"HX-Request":"true","HX-Trigger":getRawAttribute(n,"id"),"HX-Trigger-Name":getRawAttribute(n,"name"),"HX-Target":getAttributeValue(e,"id"),"HX-Current-URL":location.href};return getValuesForElement(n,"hx-headers",!1,i),t!==void 0&&(i["HX-Prompt"]=t),getInternalData(n).boosted&&(i["HX-Boosted"]="true"),i}function filterValues(n,e){let t=getClosestAttributeValue(e,"hx-params");if(t){if(t==="none")return new FormData;if(t==="*")return n;if(t.indexOf("not ")===0)return forEach(t.slice(4).split(","),function(i){i=i.trim(),n.delete(i)}),n;{let i=new FormData;return forEach(t.split(","),function(r){r=r.trim(),n.has(r)&&n.getAll(r).forEach(function(o){i.append(r,o)})}),i}}else return n}function isAnchorLink(n){return!!getRawAttribute(n,"href")&&getRawAttribute(n,"href").indexOf("#")>=0}function getSwapSpecification(n,e){let t=e||getClosestAttributeValue(n,"hx-swap"),i={swapStyle:getInternalData(n).boosted?"innerHTML":htmx.config.defaultSwapStyle,swapDelay:htmx.config.defaultSwapDelay,settleDelay:htmx.config.defaultSettleDelay};if(htmx.config.scrollIntoViewOnBoost&&getInternalData(n).boosted&&!isAnchorLink(n)&&(i.show="top"),t){let s=splitOnWhitespace(t);if(s.length>0)for(let a=0;a0?r.join(":"):null;i.scroll=u,i.scrollTarget=o}else if(l.indexOf("show:")===0){var r=l.slice(5).split(":");let d=r.pop();var o=r.length>0?r.join(":"):null;i.show=d,i.showTarget=o}else if(l.indexOf("focus-scroll:")===0){let c=l.slice(13);i.focusScroll=c=="true"}else a==0?i.swapStyle=l:logError("Unknown modifier in hx-swap: "+l)}}return i}function usesFormData(n){return getClosestAttributeValue(n,"hx-encoding")==="multipart/form-data"||matches(n,"form")&&getRawAttribute(n,"enctype")==="multipart/form-data"}function encodeParamsForBody(n,e,t){let i=null;return withExtensions(e,function(r){i==null&&(i=r.encodeParameters(n,t,e))}),i!=null?i:usesFormData(e)?overrideFormData(new FormData,formDataFromObject(t)):urlEncode(t)}function makeSettleInfo(n){return{tasks:[],elts:[n]}}function updateScrollState(n,e){let t=n[0],i=n[n.length-1];if(e.scroll){var r=null;e.scrollTarget&&(r=asElement(querySelectorExt(t,e.scrollTarget))),e.scroll==="top"&&(t||r)&&(r=r||t,r.scrollTop=0),e.scroll==="bottom"&&(i||r)&&(r=r||i,r.scrollTop=r.scrollHeight),typeof e.scroll=="number"&&getWindow().setTimeout(function(){window.scrollTo(0,e.scroll)},0)}if(e.show){var r=null;if(e.showTarget){let s=e.showTarget;e.showTarget==="window"&&(s="body"),r=asElement(querySelectorExt(t,s))}e.show==="top"&&(t||r)&&(r=r||t,r.scrollIntoView({block:"start",behavior:htmx.config.scrollBehavior})),e.show==="bottom"&&(i||r)&&(r=r||i,r.scrollIntoView({block:"end",behavior:htmx.config.scrollBehavior}))}}function getValuesForElement(n,e,t,i,r){if(i==null&&(i={}),n==null)return i;let o=getAttributeValue(n,e);if(o){let s=o.trim(),a=t;if(s==="unset")return null;s.indexOf("javascript:")===0?(s=s.slice(11),a=!0):s.indexOf("js:")===0&&(s=s.slice(3),a=!0),s.indexOf("{")!==0&&(s="{"+s+"}");let l;a?l=maybeEval(n,function(){return r?Function("event","return ("+s+")").call(n,r):Function("return ("+s+")").call(n)},{}):l=parseJSON(s);for(let c in l)l.hasOwnProperty(c)&&i[c]==null&&(i[c]=l[c])}return getValuesForElement(asElement(parentElt(n)),e,t,i,r)}function maybeEval(n,e,t){return htmx.config.allowEval?e():(triggerErrorEvent(n,"htmx:evalDisallowedError"),t)}function getHXVarsForElement(n,e,t){return getValuesForElement(n,"hx-vars",!0,t,e)}function getHXValsForElement(n,e,t){return getValuesForElement(n,"hx-vals",!1,t,e)}function getExpressionVars(n,e){return mergeObjects(getHXVarsForElement(n,e),getHXValsForElement(n,e))}function safelySetHeaderValue(n,e,t){if(t!==null)try{n.setRequestHeader(e,t)}catch(i){n.setRequestHeader(e,encodeURIComponent(t)),n.setRequestHeader(e+"-URI-AutoEncoded","true")}}function getPathFromResponse(n){if(n.responseURL)try{let e=new URL(n.responseURL);return e.pathname+e.search}catch(e){triggerErrorEvent(getDocument().body,"htmx:badResponseUrl",{url:n.responseURL})}}function hasHeader(n,e){return e.test(n.getAllResponseHeaders())}function ajaxHelper(n,e,t){if(n=n.toLowerCase(),t){if(t instanceof Element||typeof t=="string")return issueAjaxRequest(n,e,null,null,{targetOverride:resolveTarget(t)||DUMMY_ELT,returnPromise:!0});{let i=resolveTarget(t.target);return(t.target&&!i||t.source&&!i&&!resolveTarget(t.source))&&(i=DUMMY_ELT),issueAjaxRequest(n,e,resolveTarget(t.source),t.event,{handler:t.handler,headers:t.headers,values:t.values,targetOverride:i,swapOverride:t.swap,select:t.select,returnPromise:!0,push:t.push,replace:t.replace,selectOOB:t.selectOOB})}}else return issueAjaxRequest(n,e,null,null,{returnPromise:!0})}function hierarchyForElt(n){let e=[];for(;n;)e.push(n),n=n.parentElement;return e}function verifyPath(n,e,t){let i=new URL(e,location.protocol!=="about:"?location.href:window.origin),o=(location.protocol!=="about:"?location.origin:window.origin)===i.origin;return htmx.config.selfRequestsOnly&&!o?!1:triggerEvent(n,"htmx:validateUrl",mergeObjects({url:i,sameHost:o},t))}function formDataFromObject(n){if(n instanceof FormData)return n;let e=new FormData;for(let t in n)n.hasOwnProperty(t)&&(n[t]&&typeof n[t].forEach=="function"?n[t].forEach(function(i){e.append(t,i)}):typeof n[t]=="object"&&!(n[t]instanceof Blob)?e.append(t,JSON.stringify(n[t])):e.append(t,n[t]));return e}function formDataArrayProxy(n,e,t){return new Proxy(t,{get:function(i,r){return typeof r=="number"?i[r]:r==="length"?i.length:r==="push"?function(o){i.push(o),n.append(e,o)}:typeof i[r]=="function"?function(){i[r].apply(i,arguments),n.delete(e),i.forEach(function(o){n.append(e,o)})}:i[r]&&i[r].length===1?i[r][0]:i[r]},set:function(i,r,o){return i[r]=o,n.delete(e),i.forEach(function(s){n.append(e,s)}),!0}})}function formDataProxy(n){return new Proxy(n,{get:function(e,t){if(typeof t=="symbol"){let r=Reflect.get(e,t);return typeof r=="function"?function(){return r.apply(n,arguments)}:r}if(t==="toJSON")return()=>Object.fromEntries(n);if(t in e&&typeof e[t]=="function")return function(){return n[t].apply(n,arguments)};let i=n.getAll(t);if(i.length!==0)return i.length===1?i[0]:formDataArrayProxy(e,t,i)},set:function(e,t,i){return typeof t!="string"?!1:(e.delete(t),i&&typeof i.forEach=="function"?i.forEach(function(r){e.append(t,r)}):typeof i=="object"&&!(i instanceof Blob)?e.append(t,JSON.stringify(i)):e.append(t,i),!0)},deleteProperty:function(e,t){return typeof t=="string"&&e.delete(t),!0},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function issueAjaxRequest(n,e,t,i,r,o){let s=null,a=null;if(r=r!=null?r:{},r.returnPromise&&typeof Promise!="undefined")var l=new Promise(function(U,ee){s=U,a=ee});t==null&&(t=getDocument().body);let c=r.handler||handleAjaxResponse,u=r.select||null;if(!bodyContains(t))return maybeCall(s),l;let d=r.targetOverride||asElement(getTarget(t));if(d==null||d==DUMMY_ELT)return triggerErrorEvent(t,"htmx:targetError",{target:getClosestAttributeValue(t,"hx-target")}),maybeCall(a),l;let p=getInternalData(t),E=p.lastButtonClicked;if(E){let U=getRawAttribute(E,"formaction");U!=null&&(e=U);let ee=getRawAttribute(E,"formmethod");if(ee!=null)if(VERBS.includes(ee.toLowerCase()))n=ee;else return maybeCall(s),l}let m=getClosestAttributeValue(t,"hx-confirm");if(o===void 0&&triggerEvent(t,"htmx:confirm",{target:d,elt:t,path:e,verb:n,triggeringEvent:i,etc:r,issueRequest:function(se){return issueAjaxRequest(n,e,t,i,r,!!se)},question:m})===!1)return maybeCall(s),l;let v=t,_=getClosestAttributeValue(t,"hx-sync"),C=null,D=!1;if(_){let U=_.split(":"),ee=U[0].trim();if(ee==="this"?v=findThisElement(t,"hx-sync"):v=asElement(querySelectorExt(t,ee)),_=(U[1]||"drop").trim(),p=getInternalData(v),_==="drop"&&p.xhr&&p.abortable!==!0)return maybeCall(s),l;if(_==="abort"){if(p.xhr)return maybeCall(s),l;D=!0}else _==="replace"?triggerEvent(v,"htmx:abort"):_.indexOf("queue")===0&&(C=(_.split(" ")[1]||"last").trim())}if(p.xhr)if(p.abortable)triggerEvent(v,"htmx:abort");else{if(C==null){if(i){let U=getInternalData(i);U&&U.triggerSpec&&U.triggerSpec.queue&&(C=U.triggerSpec.queue)}C==null&&(C="last")}return p.queuedRequests==null&&(p.queuedRequests=[]),C==="first"&&p.queuedRequests.length===0?p.queuedRequests.push(function(){issueAjaxRequest(n,e,t,i,r)}):C==="all"?p.queuedRequests.push(function(){issueAjaxRequest(n,e,t,i,r)}):C==="last"&&(p.queuedRequests=[],p.queuedRequests.push(function(){issueAjaxRequest(n,e,t,i,r)})),maybeCall(s),l}let w=new XMLHttpRequest;p.xhr=w,p.abortable=D;let T=function(){p.xhr=null,p.abortable=!1,p.queuedRequests!=null&&p.queuedRequests.length>0&&p.queuedRequests.shift()()},K=getClosestAttributeValue(t,"hx-prompt");if(K){var z=prompt(K);if(z===null||!triggerEvent(t,"htmx:prompt",{prompt:z,target:d}))return maybeCall(s),T(),l}if(m&&!o&&!confirm(m))return maybeCall(s),T(),l;let H=getHeaders(t,d,z);n!=="get"&&!usesFormData(t)&&(H["Content-Type"]="application/x-www-form-urlencoded"),r.headers&&(H=mergeObjects(H,r.headers));let I=getInputValues(t,n),L=I.errors,j=I.formData;r.values&&overrideFormData(j,formDataFromObject(r.values));let Y=formDataFromObject(getExpressionVars(t,i)),Q=overrideFormData(j,Y),Z=filterValues(Q,t);htmx.config.getCacheBusterParam&&n==="get"&&Z.set("org.htmx.cache-buster",getRawAttribute(d,"id")||"true"),(e==null||e==="")&&(e=location.href);let he=getValuesForElement(t,"hx-request"),Ce=getInternalData(t).boosted,re=htmx.config.methodsThatUseUrlParams.indexOf(n)>=0,ie={boosted:Ce,useUrlParams:re,formData:Z,parameters:formDataProxy(Z),unfilteredFormData:Q,unfilteredParameters:formDataProxy(Q),headers:H,elt:t,target:d,verb:n,errors:L,withCredentials:r.credentials||he.credentials||htmx.config.withCredentials,timeout:r.timeout||he.timeout||htmx.config.timeout,path:e,triggeringEvent:i};if(!triggerEvent(t,"htmx:configRequest",ie))return maybeCall(s),T(),l;if(e=ie.path,n=ie.verb,H=ie.headers,Z=formDataFromObject(ie.parameters),L=ie.errors,re=ie.useUrlParams,L&&L.length>0)return triggerEvent(t,"htmx:validation:halted",ie),maybeCall(s),T(),l;let Ue=e.split("#"),Re=Ue[0],q=Ue[1],M=e;if(re&&(M=Re,!Z.keys().next().done&&(M.indexOf("?")<0?M+="?":M+="&",M+=urlEncode(Z),q&&(M+="#"+q))),!verifyPath(t,M,ie))return triggerErrorEvent(t,"htmx:invalidPath",ie),maybeCall(a),T(),l;if(w.open(n.toUpperCase(),M,!0),w.overrideMimeType("text/html"),w.withCredentials=ie.withCredentials,w.timeout=ie.timeout,!he.noHeaders){for(let U in H)if(H.hasOwnProperty(U)){let ee=H[U];safelySetHeaderValue(w,U,ee)}}let A={xhr:w,target:d,requestConfig:ie,etc:r,boosted:Ce,select:u,pathInfo:{requestPath:e,finalRequestPath:M,responsePath:null,anchor:q}};if(w.onload=function(){try{let U=hierarchyForElt(t);if(A.pathInfo.responsePath=getPathFromResponse(w),c(t,A),A.keepIndicators!==!0&&removeRequestIndicators($,B),triggerEvent(t,"htmx:afterRequest",A),triggerEvent(t,"htmx:afterOnLoad",A),!bodyContains(t)){let ee=null;for(;U.length>0&&ee==null;){let se=U.shift();bodyContains(se)&&(ee=se)}ee&&(triggerEvent(ee,"htmx:afterRequest",A),triggerEvent(ee,"htmx:afterOnLoad",A))}maybeCall(s)}catch(U){throw triggerErrorEvent(t,"htmx:onLoadError",mergeObjects({error:U},A)),U}finally{T()}},w.onerror=function(){removeRequestIndicators($,B),triggerErrorEvent(t,"htmx:afterRequest",A),triggerErrorEvent(t,"htmx:sendError",A),maybeCall(a),T()},w.onabort=function(){removeRequestIndicators($,B),triggerErrorEvent(t,"htmx:afterRequest",A),triggerErrorEvent(t,"htmx:sendAbort",A),maybeCall(a),T()},w.ontimeout=function(){removeRequestIndicators($,B),triggerErrorEvent(t,"htmx:afterRequest",A),triggerErrorEvent(t,"htmx:timeout",A),maybeCall(a),T()},!triggerEvent(t,"htmx:beforeRequest",A))return maybeCall(s),T(),l;var $=addRequestIndicatorClasses(t),B=disableElements(t);forEach(["loadstart","loadend","progress","abort"],function(U){forEach([w,w.upload],function(ee){ee.addEventListener(U,function(se){triggerEvent(t,"htmx:xhr:"+U,{lengthComputable:se.lengthComputable,loaded:se.loaded,total:se.total})})})}),triggerEvent(t,"htmx:beforeSend",A);let W=re?null:encodeParamsForBody(w,t,Z);return w.send(W),l}function determineHistoryUpdates(n,e){let t=e.xhr,i=null,r=null;if(hasHeader(t,/HX-Push:/i)?(i=t.getResponseHeader("HX-Push"),r="push"):hasHeader(t,/HX-Push-Url:/i)?(i=t.getResponseHeader("HX-Push-Url"),r="push"):hasHeader(t,/HX-Replace-Url:/i)&&(i=t.getResponseHeader("HX-Replace-Url"),r="replace"),i)return i==="false"?{}:{type:r,path:i};let o=e.pathInfo.finalRequestPath,s=e.pathInfo.responsePath,a=e.etc.push||getClosestAttributeValue(n,"hx-push-url"),l=e.etc.replace||getClosestAttributeValue(n,"hx-replace-url"),c=getInternalData(n).boosted,u=null,d=null;return a?(u="push",d=a):l?(u="replace",d=l):c&&(u="push",d=s||o),d?d==="false"?{}:(d==="true"&&(d=s||o),e.pathInfo.anchor&&d.indexOf("#")===-1&&(d=d+"#"+e.pathInfo.anchor),{type:u,path:d}):{}}function codeMatches(n,e){var t=new RegExp(n.code);return t.test(e.toString(10))}function resolveResponseHandling(n){for(var e=0;e.${e}{opacity:0;visibility: hidden} .${t} .${e}, .${t}.${e}{opacity:1;visibility: visible;transition: opacity 200ms ease-in}`)}}function getMetaConfig(){let n=getDocument().querySelector('meta[name="htmx-config"]');return n?parseJSON(n.content):null}function mergeMetaConfig(){let n=getMetaConfig();n&&(htmx.config=mergeObjects(htmx.config,n))}return ready(function(){mergeMetaConfig(),insertIndicatorStyles();let n=getDocument().body;processNode(n);let e=getDocument().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");n.addEventListener("htmx:abort",function(i){let r=i.detail.elt||i.target,o=getInternalData(r);o&&o.xhr&&o.xhr.abort()});let t=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(i){i.state&&i.state.htmx?(restoreHistory(),forEach(e,function(r){triggerEvent(r,"htmx:restored",{document:getDocument(),triggerEvent})})):t&&t(i)},getWindow().setTimeout(function(){triggerEvent(n,"htmx:load",{}),n=null},0)}),htmx})(),$_=tm;function Io(n,e){n.split(/\s+/).forEach(t=>{e(t)})}var si=class{constructor(){this._events={}}on(e,t){Io(e,i=>{let r=this._events[i]||[];r.push(t),this._events[i]=r})}off(e,t){var i=arguments.length;if(i===0){this._events={};return}Io(e,r=>{if(i===1){delete this._events[r];return}let o=this._events[r];o!==void 0&&(o.splice(o.indexOf(t),1),this._events[r]=o)})}trigger(e,...t){var i=this;Io(e,r=>{let o=i._events[r];o!==void 0&&o.forEach(s=>{s.apply(i,t)})})}};function Ro(n){return n.plugins={},class extends n{constructor(){super(...arguments),this.plugins={names:[],settings:{},requested:{},loaded:{}}}static define(e,t){n.plugins[e]={name:e,fn:t}}initializePlugins(e){var t,i;let r=this,o=[];if(Array.isArray(e))e.forEach(s=>{typeof s=="string"?o.push(s):(r.plugins.settings[s.name]=s.options,o.push(s.name))});else if(e)for(t in e)e.hasOwnProperty(t)&&(r.plugins.settings[t]=e[t],o.push(t));for(;i=o.shift();)r.require(i)}loadPlugin(e){var t=this,i=t.plugins,r=n.plugins[e];if(!n.plugins.hasOwnProperty(e))throw new Error('Unable to find "'+e+'" plugin');i.requested[e]=!0,i.loaded[e]=r.fn.apply(t,[t.plugins.settings[e]||{}]),i.names.push(e)}require(e){var t=this,i=t.plugins;if(!t.plugins.loaded.hasOwnProperty(e)){if(i.requested[e])throw new Error('Plugin has circular dependency ("'+e+'")');t.loadPlugin(e)}return i.loaded[e]}}}var ai=n=>(n=n.filter(Boolean),n.length<2?n[0]||"":im(n)==1?"["+n.join("")+"]":"(?:"+n.join("|")+")"),Po=n=>{if(!nm(n))return n.join("");let e="",t=0,i=()=>{t>1&&(e+="{"+t+"}")};return n.forEach((r,o)=>{if(r===n[o-1]){t++;return}i(),e+=r,t=1}),i(),e},Fo=n=>{let e=Array.from(n);return ai(e)},nm=n=>new Set(n).size!==n.length,Lt=n=>(n+"").replace(/([\$\(\)\*\+\.\?\[\]\^\{\|\}\\])/gu,"\\$1"),im=n=>n.reduce((e,t)=>Math.max(e,rm(t)),0),rm=n=>Array.from(n).length;var $o=n=>{if(n.length===1)return[[n]];let e=[],t=n.substring(1);return $o(t).forEach(function(r){let o=r.slice(0);o[0]=n.charAt(0)+o[0],e.push(o),o=r.slice(0),o.unshift(n.charAt(0)),e.push(o)}),e};var om=[[0,65535]],sm="[\u0300-\u036F\xB7\u02BE\u02BC]",sr,ja,am=3,Bo={},Ba={"/":"\u2044\u2215",0:"\u07C0",a:"\u2C65\u0250\u0251",aa:"\uA733",ae:"\xE6\u01FD\u01E3",ao:"\uA735",au:"\uA737",av:"\uA739\uA73B",ay:"\uA73D",b:"\u0180\u0253\u0183",c:"\uA73F\u0188\u023C\u2184",d:"\u0111\u0257\u0256\u1D05\u018C\uABB7\u0501\u0266",e:"\u025B\u01DD\u1D07\u0247",f:"\uA77C\u0192",g:"\u01E5\u0260\uA7A1\u1D79\uA77F\u0262",h:"\u0127\u2C68\u2C76\u0265",i:"\u0268\u0131",j:"\u0249\u0237",k:"\u0199\u2C6A\uA741\uA743\uA745\uA7A3",l:"\u0142\u019A\u026B\u2C61\uA749\uA747\uA781\u026D",m:"\u0271\u026F\u03FB",n:"\uA7A5\u019E\u0272\uA791\u1D0E\u043B\u0509",o:"\xF8\u01FF\u0254\u0275\uA74B\uA74D\u1D11",oe:"\u0153",oi:"\u01A3",oo:"\uA74F",ou:"\u0223",p:"\u01A5\u1D7D\uA751\uA753\uA755\u03C1",q:"\uA757\uA759\u024B",r:"\u024D\u027D\uA75B\uA7A7\uA783",s:"\xDF\u023F\uA7A9\uA785\u0282",t:"\u0167\u01AD\u0288\u2C66\uA787",th:"\xFE",tz:"\uA729",u:"\u0289",v:"\u028B\uA75F\u028C",vy:"\uA761",w:"\u2C73",y:"\u01B4\u024F\u1EFF",z:"\u01B6\u0225\u0240\u2C6C\uA763",hv:"\u0195"};for(let n in Ba){let e=Ba[n]||"";for(let t=0;t{sr===void 0&&(sr=hm(n||om))},Va=(n,e="NFKD")=>n.normalize(e),li=n=>Array.from(n).reduce((e,t)=>e+um(t),""),um=n=>(n=Va(n).toLowerCase().replace(lm,e=>Bo[e]||""),Va(n,"NFC"));function*dm(n){for(let[e,t]of n)for(let i=e;i<=t;i++){let r=String.fromCharCode(i),o=li(r);o!=r.toLowerCase()&&(o.length>am||o.length!=0&&(yield{folded:o,composed:r,code_point:i}))}}var fm=n=>{let e={},t=(i,r)=>{let o=e[i]||new Set,s=new RegExp("^"+Fo(o)+"$","iu");r.match(s)||(o.add(Lt(r)),e[i]=o)};for(let i of dm(n))t(i.folded,i.folded),t(i.folded,i.composed);return e},hm=n=>{let e=fm(n),t={},i=[];for(let o in e){let s=e[o];s&&(t[o]=Fo(s)),o.length>1&&i.push(Lt(o))}i.sort((o,s)=>s.length-o.length);let r=ai(i);return ja=new RegExp("^"+r,"u"),t},pm=(n,e=1)=>{let t=0;return n=n.map(i=>(sr[i]&&(t+=i.length),sr[i]||i)),t>=e?Po(n):""},mm=(n,e=1)=>(e=Math.max(e,n.length-1),ai($o(n).map(t=>pm(t,e)))),za=(n,e=!0)=>{let t=n.length>1?1:0;return ai(n.map(i=>{let r=[],o=e?i.length():i.length()-1;for(let s=0;s{for(let t of e){if(t.start!=n.start||t.end!=n.end||t.substrs.join("")!==n.substrs.join(""))continue;let i=n.parts,r=s=>{for(let a of i){if(a.start===s.start&&a.substr===s.substr)return!1;if(!(s.length==1||a.length==1)&&(s.starta.start||a.starts.start))return!0}return!1};if(!(t.parts.filter(r).length>0))return!0}return!1},ar=class n{constructor(){te(this,"parts");te(this,"substrs");te(this,"start");te(this,"end");this.parts=[],this.substrs=[],this.start=0,this.end=0}add(e){e&&(this.parts.push(e),this.substrs.push(e.substr),this.start=Math.min(e.start,this.start),this.end=Math.max(e.end,this.end))}last(){return this.parts[this.parts.length-1]}length(){return this.parts.length}clone(e,t){let i=new n,r=JSON.parse(JSON.stringify(this.parts)),o=r.pop();for(let l of r)i.add(l);let s=t.substr.substring(0,e-o.start),a=s.length;return i.add({start:o.start,end:o.start+a,length:a,substr:s}),i}},qa=n=>{cm(),n=li(n);let e="",t=[new ar];for(let i=0;i0){l=l.sort((u,d)=>u.length()-d.length());for(let u of l)gm(u,t)||t.push(u);continue}if(i>0&&c.size==1&&!c.has("3")){e+=za(t,!1);let u=new ar,d=t[0];d&&u.add(d.last()),t=[u]}}return e+=za(t,!0),e};var Wa=(n,e)=>{if(n)return n[e]},Ua=(n,e)=>{if(n){for(var t,i=e.split(".");(t=i.shift())&&(n=n[t]););return n}},lr=(n,e,t)=>{var i,r;return!n||(n=n+"",e.regex==null)||(r=n.search(e.regex),r===-1)?0:(i=e.string.length/n.length,r===0&&(i+=.5),i*t)},cr=(n,e)=>{var t=n[e];if(typeof t=="function")return t;t&&!Array.isArray(t)&&(n[e]=[t])},ci=(n,e)=>{if(Array.isArray(n))n.forEach(e);else for(var t in n)n.hasOwnProperty(t)&&e(n[t],t)},Ya=(n,e)=>typeof n=="number"&&typeof e=="number"?n>e?1:ne?1:e>n?-1:0);var ur=class{constructor(e,t){te(this,"items");te(this,"settings");this.items=e,this.settings=t||{diacritics:!0}}tokenize(e,t,i){if(!e||!e.length)return[];let r=[],o=e.split(/\s+/);var s;return i&&(s=new RegExp("^("+Object.keys(i).map(Lt).join("|")+"):(.*)$")),o.forEach(a=>{let l,c=null,u=null;s&&(l=a.match(s))&&(c=l[1],a=l[2]),a.length>0&&(this.settings.diacritics?u=qa(a)||null:u=Lt(a),u&&t&&(u="\\b"+u)),r.push({string:a,regex:u?new RegExp(u,"iu"):null,field:c})}),r}getScoreFunction(e,t){var i=this.prepareSearch(e,t);return this._getScoreFunction(i)}_getScoreFunction(e){let t=e.tokens,i=t.length;if(!i)return function(){return 0};let r=e.options.fields,o=e.weights,s=r.length,a=e.getAttrFn;if(!s)return function(){return 1};let l=(function(){return s===1?function(c,u){let d=r[0].field;return lr(a(u,d),c,o[d]||1)}:function(c,u){var d=0;if(c.field){let p=a(u,c.field);!c.regex&&p?d+=1/s:d+=lr(p,c,1)}else ci(o,(p,E)=>{d+=lr(a(u,E),c,p)});return d/s}})();return i===1?function(c){return l(t[0],c)}:e.options.conjunction==="and"?function(c){var u,d=0;for(let p of t){if(u=l(p,c),u<=0)return 0;d+=u}return d/i}:function(c){var u=0;return ci(t,d=>{u+=l(d,c)}),u/i}}getSortFunction(e,t){var i=this.prepareSearch(e,t);return this._getSortFunction(i)}_getSortFunction(e){var t,i=[];let r=this,o=e.options,s=!e.query&&o.sort_empty?o.sort_empty:o.sort;if(typeof s=="function")return s.bind(this);let a=function(c,u){return c==="$score"?u.score:e.getAttrFn(r.items[u.id],c)};if(s)for(let c of s)(e.query||c.field!=="$score")&&i.push(c);if(e.query){t=!0;for(let c of i)if(c.field==="$score"){t=!1;break}t&&i.unshift({field:"$score",direction:"desc"})}else i=i.filter(c=>c.field!=="$score");return i.length?function(c,u){var d,p;for(let E of i)if(p=E.field,d=(E.direction==="desc"?-1:1)*Ya(a(p,c),a(p,u)),d)return d;return 0}:null}prepareSearch(e,t){let i={};var r=Object.assign({},t);if(cr(r,"sort"),cr(r,"sort_empty"),r.fields){cr(r,"fields");let o=[];r.fields.forEach(s=>{typeof s=="string"&&(s={field:s,weight:1}),o.push(s),i[s.field]="weight"in s?s.weight:1}),r.fields=o}return{options:r,query:e.toLowerCase().trim(),tokens:this.tokenize(e,r.respect_word_boundaries,i),total:0,items:[],weights:i,getAttrFn:r.nesting?Ua:Wa}}search(e,t){var i=this,r,o;o=this.prepareSearch(e,t),t=o.options,e=o.query;let s=t.score||i._getScoreFunction(o);e.length?ci(i.items,(l,c)=>{r=s(l),(t.filter===!1||r>0)&&o.items.push({score:r,id:c})}):ci(i.items,(l,c)=>{o.items.push({score:1,id:c})});let a=i._getSortFunction(o);return a&&o.items.sort(a),o.total=o.items.length,typeof t.limit=="number"&&(o.items=o.items.slice(0,t.limit)),o}};var Fe=n=>typeof n=="undefined"||n===null?null:ui(n),ui=n=>typeof n=="boolean"?n?"1":"0":n+"",dr=n=>(n+"").replace(/&/g,"&").replace(//g,">").replace(/"/g,"""),Ga=(n,e)=>e>0?window.setTimeout(n,e):(n.call(null),null),Ka=(n,e)=>{var t;return function(i,r){var o=this;t&&(o.loading=Math.max(o.loading-1,0),clearTimeout(t)),t=setTimeout(function(){t=null,o.loadedSearches[i]=!0,n.call(o,i,r)},e)}},Vo=(n,e,t)=>{var i,r=n.trigger,o={};n.trigger=function(){var s=arguments[0];if(e.indexOf(s)!==-1)o[s]=arguments;else return r.apply(n,arguments)},t.apply(n,[]),n.trigger=r;for(i of e)i in o&&r.apply(n,o[i])},Xa=n=>({start:n.selectionStart||0,length:(n.selectionEnd||0)-(n.selectionStart||0)}),fe=(n,e=!1)=>{n&&(n.preventDefault(),e&&n.stopPropagation())},De=(n,e,t,i)=>{n.addEventListener(e,t,i)},Mt=(n,e)=>{if(!e||!e[n])return!1;var t=(e.altKey?1:0)+(e.ctrlKey?1:0)+(e.shiftKey?1:0)+(e.metaKey?1:0);return t===1},fr=(n,e)=>{let t=n.getAttribute("id");return t||(n.setAttribute("id",e),e)},zo=n=>n.replace(/[\\"']/g,"\\$&"),Nt=(n,e)=>{e&&n.append(e)},ve=(n,e)=>{if(Array.isArray(n))n.forEach(e);else for(var t in n)n.hasOwnProperty(t)&&e(n[t],t)};var nt=n=>{if(n.jquery)return n[0];if(n instanceof HTMLElement)return n;if(jo(n)){var e=document.createElement("template");return e.innerHTML=n.trim(),e.content.firstChild}return document.querySelector(n)},jo=n=>typeof n=="string"&&n.indexOf("<")>-1,Qa=n=>n.replace(/['"\\]/g,"\\$&"),hr=(n,e)=>{var t=document.createEvent("HTMLEvents");t.initEvent(e,!0,!1),n.dispatchEvent(t)},di=(n,e)=>{Object.assign(n.style,e)},$e=(n,...e)=>{var t=Ja(e);n=Za(n),n.map(i=>{t.map(r=>{i.classList.add(r)})})},gt=(n,...e)=>{var t=Ja(e);n=Za(n),n.map(i=>{t.map(r=>{i.classList.remove(r)})})},Ja=n=>{var e=[];return ve(n,t=>{typeof t=="string"&&(t=t.trim().split(/[\t\n\f\r\s]/)),Array.isArray(t)&&(e=e.concat(t))}),e.filter(Boolean)},Za=n=>(Array.isArray(n)||(n=[n]),n),pr=(n,e,t)=>{if(!(t&&!t.contains(n)))for(;n&&n.matches;){if(n.matches(e))return n;n=n.parentNode}},qo=(n,e=0)=>e>0?n[n.length-1]:n[0],el=n=>Object.keys(n).length===0,Wo=(n,e)=>{if(!n)return-1;e=e||n.nodeName;for(var t=0;n=n.previousElementSibling;)n.matches(e)&&t++;return t},le=(n,e)=>{ve(e,(t,i)=>{t==null?n.removeAttribute(i):n.setAttribute(i,""+t)})},fi=(n,e)=>{n.parentNode&&n.parentNode.replaceChild(e,n)};var tl=(n,e)=>{if(e===null)return;if(typeof e=="string"){if(!e.length)return;e=new RegExp(e,"i")}let t=o=>{var s=o.data.match(e);if(s&&o.data.length>0){var a=document.createElement("span");a.className="highlight";var l=o.splitText(s.index);l.splitText(s[0].length);var c=l.cloneNode(!0);return a.appendChild(c),fi(l,a),1}return 0},i=o=>{o.nodeType===1&&o.childNodes&&!/(script|style)/i.test(o.tagName)&&(o.className!=="highlight"||o.tagName!=="SPAN")&&Array.from(o.childNodes).forEach(s=>{r(s)})},r=o=>o.nodeType===3?t(o):(i(o),0);r(n)},nl=n=>{var e=n.querySelectorAll("span.highlight");Array.prototype.forEach.call(e,function(t){var i=t.parentNode;i.replaceChild(t.firstChild,t),i.normalize()})};var vm=typeof navigator=="undefined"?!1:/Mac/.test(navigator.userAgent),hi=vm?"metaKey":"ctrlKey";var Uo={options:[],optgroups:[],plugins:[],delimiter:",",splitOn:null,persist:!0,diacritics:!0,create:null,createOnBlur:!1,createFilter:null,clearAfterSelect:!1,highlight:!0,openOnFocus:!0,shouldOpen:null,maxOptions:50,maxItems:null,hideSelected:null,duplicates:!1,addPrecedence:!1,selectOnTab:!1,preload:null,allowEmptyOption:!1,refreshThrottle:300,loadThrottle:300,loadingClass:"loading",dataAttr:null,optgroupField:"optgroup",valueField:"value",labelField:"text",disabledField:"disabled",optgroupLabelField:"label",optgroupValueField:"value",lockOptgroupOrder:!1,sortField:"$order",searchField:["text"],searchConjunction:"and",mode:null,wrapperClass:"ts-wrapper",controlClass:"ts-control",dropdownClass:"ts-dropdown",dropdownContentClass:"ts-dropdown-content",itemClass:"item",optionClass:"option",dropdownParent:null,controlInput:'',copyClassesToDropdown:!1,placeholder:null,hidePlaceholder:null,shouldLoad:function(n){return n.length>0},render:{}};function mr(n,e){var t=Object.assign({},Uo,e),i=t.dataAttr,r=t.labelField,o=t.valueField,s=t.disabledField,a=t.optgroupField,l=t.optgroupLabelField,c=t.optgroupValueField,u=n.tagName.toLowerCase(),d=n.getAttribute("placeholder")||n.getAttribute("data-placeholder");if(!d&&!t.allowEmptyOption){let v=n.querySelector('option[value=""]');v&&(d=v.textContent)}var p={placeholder:d,options:[],optgroups:[],items:[],maxItems:null},E=()=>{var v,_=p.options,C={},D=1;let w=0;var T=H=>{var I=Object.assign({},H.dataset),L=i&&I[i];return typeof L=="string"&&L.length&&(I=Object.assign(I,JSON.parse(L))),I},K=(H,I)=>{var L=Fe(H.value);if(L!=null&&!(!L&&!t.allowEmptyOption)){if(C.hasOwnProperty(L)){if(I){var j=C[L][a];j?Array.isArray(j)?j.push(I):C[L][a]=[j,I]:C[L][a]=I}}else{var Y=T(H);Y[r]=Y[r]||H.textContent,Y[o]=Y[o]||L,Y[s]=Y[s]||H.disabled,Y[a]=Y[a]||I,Y.$option=H,Y.$order=Y.$order||++w,C[L]=Y,_.push(Y)}H.selected&&p.items.push(L)}},z=H=>{var I,L;L=T(H),L[l]=L[l]||H.getAttribute("label")||"",L[c]=L[c]||D++,L[s]=L[s]||H.disabled,L.$order=L.$order||++w,p.optgroups.push(L),I=L[c],ve(H.children,j=>{K(j,I)})};p.maxItems=n.hasAttribute("multiple")?null:1,ve(n.children,H=>{v=H.tagName.toLowerCase(),v==="optgroup"?z(H):v==="option"&&K(H)})},m=()=>{let v=n.getAttribute(i);if(v)p.options=JSON.parse(v),ve(p.options,C=>{p.items.push(C[o])});else{var _=n.value.trim()||"";if(!t.allowEmptyOption&&!_.length)return;let C=_.split(t.delimiter);ve(C,D=>{let w={};w[r]=D,w[o]=D,p.options.push(w)}),p.items=C}};return u==="select"?E():m(),Object.assign({},Uo,p,e)}var ol=0,we=class extends Ro(si){constructor(e,t){super(),this.order=0,this.isOpen=!1,this.isDisabled=!1,this.isReadOnly=!1,this.isInvalid=!1,this.isValid=!0,this.isLocked=!1,this.isFocused=!1,this.isInputHidden=!1,this.isSetup=!1,this.isDropdownContentStale=!0,this.ignoreFocus=!1,this.ignoreHover=!1,this.hasOptions=!1,this.lastValue="",this.caretPos=0,this.loading=0,this.loadedSearches={},this.activeOption=null,this.activeItems=[],this.optgroups={},this.options={},this.userOptions={},this.items=[],this.refreshTimeout=null,ol++;var i,r=nt(e);if(r.tomselect)throw new Error("Tom Select already initialized on this element");r.tomselect=this;var o=window.getComputedStyle&&window.getComputedStyle(r,null);i=o.getPropertyValue("direction");let s=mr(r,t);this.settings=s,this.input=r,this.tabIndex=r.tabIndex||0,this.is_select_tag=r.tagName.toLowerCase()==="select",this.rtl=/rtl/i.test(i),this.inputId=fr(r,"tomselect-"+ol),this.isRequired=r.required,this.sifter=new ur(this.options,{diacritics:s.diacritics}),s.mode=s.mode||(s.maxItems===1?"single":"multi"),typeof s.hideSelected!="boolean"&&(s.hideSelected=s.mode==="multi"),typeof s.hidePlaceholder!="boolean"&&(s.hidePlaceholder=s.mode!=="multi");var a=s.createFilter;typeof a!="function"&&(typeof a=="string"&&(a=new RegExp(a)),a instanceof RegExp?s.createFilter=_=>a.test(_):s.createFilter=_=>this.settings.duplicates||!this.options[_]),this.initializePlugins(s.plugins),this.setupCallbacks(),this.setupTemplates();let l=nt("
"),c=nt("
"),u=this._render("dropdown"),d=nt('
'),p=this.input.getAttribute("class")||"",E=s.mode;var m;if($e(l,s.wrapperClass,p,E),$e(c,s.controlClass),Nt(l,c),$e(u,s.dropdownClass,E),s.copyClassesToDropdown&&$e(u,p),$e(d,s.dropdownContentClass),Nt(u,d),nt(s.dropdownParent||l).appendChild(u),jo(s.controlInput)){m=nt(s.controlInput);var v=["autocorrect","autocapitalize","autocomplete","spellcheck","aria-label"];ve(v,_=>{r.getAttribute(_)&&le(m,{[_]:r.getAttribute(_)})}),m.tabIndex=-1,c.appendChild(m),this.focus_node=m}else s.controlInput?(m=nt(s.controlInput),this.focus_node=m):(m=nt(""),this.focus_node=c);this.wrapper=l,this.dropdown=u,this.dropdown_content=d,this.control=c,this.control_input=m,this.setup()}setup(){let e=this,t=e.settings,i=e.control_input,r=e.dropdown,o=e.dropdown_content,s=e.wrapper,a=e.control,l=e.input,c=e.focus_node,u={passive:!0},d=e.inputId+"-ts-dropdown";le(o,{id:d}),le(c,{role:"combobox","aria-haspopup":"listbox","aria-expanded":"false","aria-controls":d});let p=fr(c,e.inputId+"-ts-control"),E="label[for='"+Qa(e.inputId)+"']",m=document.querySelector(E),v=e.focus.bind(e);if(m){De(m,"click",v),le(m,{for:p});let w=fr(m,e.inputId+"-ts-label");le(c,{"aria-labelledby":w}),le(o,{"aria-labelledby":w})}if(s.style.width=l.style.width,s.style.minWidth=l.style.minWidth,s.style.maxWidth=l.style.maxWidth,e.plugins.names.length){let w="plugin-"+e.plugins.names.join(" plugin-");$e([s,r],w)}(t.maxItems===null||t.maxItems>1)&&e.is_select_tag&&le(l,{multiple:"multiple"}),t.placeholder&&le(i,{placeholder:t.placeholder}),!t.splitOn&&t.delimiter&&(t.splitOn=new RegExp("\\s*"+Lt(t.delimiter)+"+\\s*")),t.load&&t.loadThrottle&&(t.load=Ka(t.load,t.loadThrottle)),De(r,"mousemove",()=>{e.ignoreHover=!1}),De(r,"mouseenter",w=>{var T=pr(w.target,"[data-selectable]",r);T&&e.onOptionHover(w,T)},{capture:!0}),De(r,"click",w=>{let T=pr(w.target,"[data-selectable]");T&&(e.onOptionSelect(w,T),fe(w,!0))}),De(a,"click",w=>{var T=pr(w.target,"[data-ts-item]",a);if(T&&e.onItemSelect(w,T)){fe(w,!0);return}i.value==""&&(e.onClick(),fe(w,!0))}),De(c,"keydown",w=>e.onKeyDown(w)),De(i,"keypress",w=>e.onKeyPress(w)),De(i,"input",w=>e.onInput(w)),De(c,"blur",w=>e.onBlur(w)),De(c,"focus",w=>e.onFocus(w)),De(i,"paste",w=>e.onPaste(w));let _=w=>{let T=w.composedPath()[0];if(!s.contains(T)&&!r.contains(T)){e.isFocused&&e.blur(),e.inputState();return}T==i&&e.isOpen?w.stopPropagation():fe(w,!0)},C=()=>{e.isOpen&&e.positionDropdown()},D=()=>{e.isValid&&(e.isValid=!1,e.isInvalid=!0,e.refreshState())};De(l,"invalid",D),De(document,"mousedown",_),De(window,"scroll",C,u),De(window,"resize",C,u),this._destroy=()=>{l.removeEventListener("invalid",D),document.removeEventListener("mousedown",_),window.removeEventListener("scroll",C),window.removeEventListener("resize",C),m&&m.removeEventListener("click",v)},this.revertSettings={innerHTML:l.innerHTML,tabIndex:l.tabIndex},l.tabIndex=-1,l.insertAdjacentElement("afterend",e.wrapper),e.sync(!1),t.items=[],delete t.optgroups,delete t.options,e.refreshItems(),e.close(!1),e.inputState(),e.isSetup=!0,l.disabled?e.disable():l.readOnly?e.setReadOnly(!0):e.enable(),e.on("change",this.onChange),$e(l,"tomselected","ts-hidden-accessible"),e.trigger("initialize"),t.preload===!0&&e.preload()}setupOptions(e=[],t=[]){this.addOptions(e),ve(t,i=>{this.registerOptionGroup(i)})}setupTemplates(){var e=this,t=e.settings.labelField,i=e.settings.optgroupLabelField,r={optgroup:o=>{let s=document.createElement("div");return s.className="optgroup",s.appendChild(o.options),s},optgroup_header:(o,s)=>'
'+s(o[i])+"
",option:(o,s)=>"
"+s(o[t])+"
",item:(o,s)=>"
"+s(o[t])+"
",option_create:(o,s)=>'
Add '+s(o.input)+"
",no_results:()=>'
No results found
',loading:()=>'
',not_loading:()=>{},dropdown:()=>"
"};e.settings.render=Object.assign({},r,e.settings.render)}setupCallbacks(){var e,t,i={initialize:"onInitialize",change:"onChange",item_add:"onItemAdd",item_remove:"onItemRemove",item_select:"onItemSelect",clear:"onClear",option_add:"onOptionAdd",option_remove:"onOptionRemove",option_clear:"onOptionClear",optgroup_add:"onOptionGroupAdd",optgroup_remove:"onOptionGroupRemove",optgroup_clear:"onOptionGroupClear",dropdown_open:"onDropdownOpen",dropdown_close:"onDropdownClose",type:"onType",load:"onLoad",focus:"onFocus",blur:"onBlur"};for(e in i)t=this.settings[i[e]],t&&this.on(e,t)}sync(e=!0){let t=this,i=e?mr(t.input,{delimiter:t.settings.delimiter,allowEmptyOption:t.settings.allowEmptyOption}):t.settings;t.setupOptions(i.options,i.optgroups),t.setValue(i.items||[],!0)}onClick(){var e=this;if(e.activeItems.length>0){e.clearActiveItems(),e.focus();return}e.isFocused&&e.isOpen?e.blur():e.focus()}onMouseDown(){}onChange(){hr(this.input,"input"),hr(this.input,"change")}onPaste(e){var t=this;if(t.isInputHidden||t.isLocked){fe(e);return}t.settings.splitOn&&setTimeout(()=>{var i=t.inputValue();if(i.match(t.settings.splitOn)){var r=i.trim().split(t.settings.splitOn);ve(r,o=>{Fe(o)&&(this.options[o]?t.addItem(o):t.createItem(o))})}},0)}onKeyPress(e){var t=this;if(t.isLocked){fe(e);return}var i=String.fromCharCode(e.keyCode||e.which);if(t.settings.create&&t.settings.mode==="multi"&&i===t.settings.delimiter){t.createItem(),fe(e);return}}onKeyDown(e){var t=this;if(t.ignoreHover=!0,t.isLocked){e.keyCode!==9&&fe(e);return}switch(e.keyCode){case 65:if(Mt(hi,e)&&t.control_input.value==""){fe(e),t.selectAll();return}break;case 27:t.isOpen&&(fe(e,!0),t.close()),t.clearActiveItems();return;case 40:if(!t.isOpen&&t.hasOptions)t.open();else if(t.activeOption){let i=t.getAdjacent(t.activeOption,1);i&&t.setActiveOption(i)}fe(e);return;case 38:if(t.activeOption){let i=t.getAdjacent(t.activeOption,-1);i&&t.setActiveOption(i)}fe(e);return;case 13:t.canSelect(t.activeOption)?(t.onOptionSelect(e,t.activeOption),fe(e)):t.settings.create&&t.createItem()?fe(e):document.activeElement==t.control_input&&t.isOpen&&fe(e);return;case 37:t.advanceSelection(-1,e);return;case 39:t.advanceSelection(1,e);return;case 9:t.settings.selectOnTab&&(t.canSelect(t.activeOption)?(t.onOptionSelect(e,t.activeOption),fe(e)):t.settings.create&&t.createItem()&&fe(e));return;case 8:case 46:t.deleteSelection(e);return}t.isInputHidden&&!Mt(hi,e)&&fe(e)}onInput(e){if(this.isLocked)return;let t=this.inputValue();if(this.lastValue!==t){if(this.lastValue=t,t==""){this._onInput();return}this.refreshTimeout&&window.clearTimeout(this.refreshTimeout),this.refreshTimeout=Ga(()=>{this.refreshTimeout=null,this._onInput()},this.settings.refreshThrottle)}}_onInput(){let e=this.lastValue;this.settings.shouldLoad.call(this,e)&&this.load(e),this.refreshOptions(),this.trigger("type",e)}onOptionHover(e,t){this.ignoreHover||this.setActiveOption(t,!1)}onFocus(e){var t=this,i=t.isFocused;if(t.isDisabled||t.isReadOnly){t.blur(),fe(e);return}t.ignoreFocus||(t.isFocused=!0,t.settings.preload==="focus"&&t.preload(),i||t.trigger("focus"),t.activeItems.length||(t.inputState(),t.refreshOptions(!!t.settings.openOnFocus)),t.refreshState())}onBlur(e){if(document.hasFocus()!==!1){var t=this;if(t.isFocused){t.isFocused=!1,t.ignoreFocus=!1;var i=()=>{t.close(),t.setActiveItem(),t.setCaret(t.items.length),t.trigger("blur")};t.settings.create&&t.settings.createOnBlur?t.createItem(null,i):i()}}}onOptionSelect(e,t){var i,r=this;t.parentElement&&t.parentElement.matches("[data-disabled]")||(t.classList.contains("create")?r.createItem(null,()=>{r.settings.closeAfterSelect?r.close():r.settings.clearAfterSelect&&r.setTextboxValue()}):(i=t.dataset.value,typeof i!="undefined"&&(r.isDropdownContentStale=r.settings.hideSelected,r.addItem(i),r.settings.closeAfterSelect?r.close():r.settings.clearAfterSelect&&r.setTextboxValue(),!r.settings.hideSelected&&e.type&&/click/.test(e.type)&&r.setActiveOption(t))))}canSelect(e){return!!(this.isOpen&&e&&this.dropdown_content.contains(e))}onItemSelect(e,t){var i=this;return!i.isLocked&&i.settings.mode==="multi"?(fe(e),i.setActiveItem(t,e),!0):!1}canLoad(e){return!(!this.settings.load||this.loadedSearches.hasOwnProperty(e))}load(e){let t=this;if(!t.canLoad(e))return;$e(t.wrapper,t.settings.loadingClass),t.loading++;let i=t.loadCallback.bind(t);t.settings.load.call(t,e,i)}loadCallback(e,t){let i=this;i.loading=Math.max(i.loading-1,0),i.isDropdownContentStale=!0,i.clearActiveOption(),i.setupOptions(e,t),i.refreshOptions(i.isFocused&&!i.isInputHidden),i.loading||gt(i.wrapper,i.settings.loadingClass),i.trigger("load",e,t)}preload(){var e=this.wrapper.classList;e.contains("preloaded")||(e.add("preloaded"),this.load(""))}setTextboxValue(e=""){var t=this.control_input,i=t.value!==e;i&&(t.value=e,hr(t,"update"),this.lastValue=e)}getValue(){return this.is_select_tag&&this.input.hasAttribute("multiple")?this.items:this.items.join(this.settings.delimiter)}setValue(e,t){var i=t?[]:["change"];Vo(this,i,()=>{this.clear(t),this.addItems(e,t)})}setMaxItems(e){e===0&&(e=null),this.settings.maxItems=e,this.refreshState()}setActiveItem(e,t){var i=this,r,o,s,a,l,c;if(i.settings.mode!=="single"){if(!e){i.clearActiveItems(),i.isFocused&&i.inputState();return}if(r=t&&t.type.toLowerCase(),r==="click"&&Mt("shiftKey",t)&&i.activeItems.length){for(c=i.getLastActive(),s=Array.prototype.indexOf.call(i.control.children,c),a=Array.prototype.indexOf.call(i.control.children,e),s>a&&(l=s,s=a,a=l),o=s;o<=a;o++)e=i.control.children[o],i.activeItems.indexOf(e)===-1&&i.setActiveItemClass(e);fe(t)}else r==="click"&&Mt(hi,t)||r==="keydown"&&Mt("shiftKey",t)?e.classList.contains("active")?i.removeActiveItem(e):i.setActiveItemClass(e):(i.clearActiveItems(),i.setActiveItemClass(e));i.inputState(),i.isFocused||i.focus()}}setActiveItemClass(e){let t=this,i=t.control.querySelector(".last-active");i&>(i,"last-active"),$e(e,"active last-active"),t.trigger("item_select",e),t.activeItems.indexOf(e)==-1&&t.activeItems.push(e)}removeActiveItem(e){var t=this.activeItems.indexOf(e);this.activeItems.splice(t,1),gt(e,"active")}clearActiveItems(){gt(this.activeItems,"active"),this.activeItems=[]}setActiveOption(e,t=!0){e!==this.activeOption&&(this.clearActiveOption(),e&&(this.activeOption=e,le(this.focus_node,{"aria-activedescendant":e.getAttribute("id")}),le(e,{"aria-selected":"true"}),$e(e,"active"),t&&this.scrollToOption(e)))}scrollToOption(e,t){if(!e)return;let i=this.dropdown_content,r=i.clientHeight,o=i.scrollTop||0,s=e.offsetHeight,a=e.getBoundingClientRect().top-i.getBoundingClientRect().top+o;a+s>r+o?this.scroll(a-r+s,t):a{e.setActiveItemClass(i)}))}inputState(){var e=this;e.control.contains(e.control_input)&&(le(e.control_input,{placeholder:e.settings.placeholder}),e.activeItems.length>0||!e.isFocused&&e.settings.hidePlaceholder&&e.items.length>0?(e.setTextboxValue(),e.isInputHidden=!0):(e.settings.hidePlaceholder&&e.items.length>0&&le(e.control_input,{placeholder:""}),e.isInputHidden=!1),e.wrapper.classList.toggle("input-hidden",e.isInputHidden))}inputValue(){return this.control_input.value.trim()}focus(){var e=this;e.isDisabled||e.isReadOnly||(e.ignoreFocus=!0,e.control_input.offsetWidth?e.control_input.focus():e.focus_node.focus(),setTimeout(()=>{e.ignoreFocus=!1,e.onFocus()},0))}blur(){this.focus_node.blur(),this.onBlur()}getScoreFunction(e){return this.sifter.getScoreFunction(e,this.getSearchOptions())}getSearchOptions(){var e=this.settings,t=e.sortField;return typeof e.sortField=="string"&&(t=[{field:e.sortField}]),{fields:e.searchField,conjunction:e.searchConjunction,sort:t,nesting:e.nesting}}search(e){var t,i,r=this,o=this.getSearchOptions();if(r.settings.score&&(i=r.settings.score.call(r,e),typeof i!="function"))throw new Error('Tom Select "score" setting must be a function that returns a function');return r.isDropdownContentStale||e!==r.lastQuery?(r.lastQuery=e,/(.)\1{15,}/.test(e)&&(e=""),t=r.sifter.search(e,Object.assign(o,{score:i})),r.currentResults=t):t=Object.assign({},r.currentResults),r.settings.hideSelected&&(t.items=t.items.filter(s=>{let a=Fe(s.id);return!(a!==null&&r.items.indexOf(a)!==-1)})),t}refreshOptions(e=!0){var t,i,r,o,s,a,l,c,u,d;let p={},E=[];var m=this,v=m.inputValue();let _=v===m.lastQuery||v==""&&m.lastQuery==null;var C=m.search(v),D=null,w=m.settings.shouldOpen||!1,T=m.dropdown_content;_&&(D=m.activeOption,D&&(u=D.closest("[data-group]"))),o=C.items.length,typeof m.settings.maxOptions=="number"&&(o=Math.min(o,m.settings.maxOptions)),o>0&&(w=!0);let K=(I,L)=>{let j=p[I];if(j!==void 0){let Q=E[j];if(Q!==void 0)return[j,Q.fragment]}let Y=document.createDocumentFragment();return j=E.length,E.push({fragment:Y,order:L,optgroup:I}),[j,Y]};for(t=0;t0&&(Q=Q.cloneNode(!0),le(Q,{id:j.$id+"-clone-"+i,"aria-selected":null}),Q.classList.add("ts-cloned"),gt(Q,"active"),m.activeOption&&m.activeOption.dataset.value==L&&u&&u.dataset.group===s.toString()&&(D=Q)),re.appendChild(Q),s!=""&&(p[s]=Ce)}}m.settings.lockOptgroupOrder&&E.sort((I,L)=>I.order-L.order),l=document.createDocumentFragment(),ve(E,I=>{let L=I.fragment,j=I.optgroup;if(!L||!L.children.length)return;let Y=m.optgroups[j];if(Y!==void 0){let Q=document.createDocumentFragment(),Z=m.render("optgroup_header",Y);Nt(Q,Z),Nt(Q,L);let he=m.render("optgroup",{group:Y,options:Q});Nt(l,he)}else Nt(l,L)}),T.innerHTML="",Nt(T,l),m.isDropdownContentStale=!1,m.settings.highlight&&(nl(T),C.query.length&&C.tokens.length&&ve(C.tokens,I=>{tl(T,I.regex)}));var H=I=>{let L=m.render(I,{input:v});return L&&(w=!0,T.insertBefore(L,T.firstChild)),L};if(m.loading?H("loading"):m.settings.shouldLoad.call(m,v)?C.items.length===0&&H("no_results"):H("not_loading"),c=m.canCreate(v),c&&(d=H("option_create")),m.hasOptions=C.items.length>0||c,w){if(C.items.length>0){if(!D&&m.settings.mode==="single"&&m.items[0]!=null&&(D=m.getOption(m.items[0])),!T.contains(D)){let I=0;d&&!m.settings.addPrecedence&&(I=1),D=m.selectable()[I]}}else d&&(D=d);e&&!m.isOpen&&(m.open(),m.scrollToOption(D,"auto")),m.setActiveOption(D)}else m.clearActiveOption(),e&&m.isOpen&&m.close(!1)}selectable(){return this.dropdown_content.querySelectorAll("[data-selectable]")}addOption(e,t=!1){let i=this;if(Array.isArray(e))return i.addOptions(e,t),!1;let r=Fe(e[i.settings.valueField]);return r===null||i.options.hasOwnProperty(r)?(i.updateOption(e[i.settings.valueField],e),!1):(e.$order=e.$order||++i.order,e.$id=i.inputId+"-opt-"+e.$order,i.options[r]=e,i.isDropdownContentStale=!0,t&&(i.userOptions[r]=t,i.trigger("option_add",r,e)),r)}addOptions(e,t=!1){ve(e,i=>{this.addOption(i,t)})}registerOption(e){return this.addOption(e)}registerOptionGroup(e){var t=Fe(e[this.settings.optgroupValueField]);return t===null?!1:(e.$order=e.$order||++this.order,this.optgroups[t]=e,t)}addOptionGroup(e,t){var i;t[this.settings.optgroupValueField]=e,(i=this.registerOptionGroup(t))&&this.trigger("optgroup_add",i,t)}removeOptionGroup(e){this.optgroups.hasOwnProperty(e)&&(delete this.optgroups[e],this.clearCache(),this.trigger("optgroup_remove",e))}clearOptionGroups(){this.optgroups={},this.clearCache(),this.trigger("optgroup_clear")}updateOption(e,t){let i=this;var r,o;let s=Fe(e),a=Fe(t[i.settings.valueField]);if(s===null)return;let l=i.options[s];if(l==null)return;if(typeof a!="string")throw new Error("Value must be set in option data");let c=i.getOption(s),u=i.getItem(s);if(t.$order=t.$order||l.$order,delete i.options[s],i.uncacheValue(a),i.options[a]=t,c){if(i.dropdown_content.contains(c)){let d=i._render("option",t);fi(c,d),i.activeOption===c&&i.setActiveOption(d)}c.remove()}u&&(o=i.items.indexOf(s),o!==-1&&i.items.splice(o,1,a),r=i._render("item",t),u.classList.contains("active")&&$e(r,"active"),fi(u,r)),i.isDropdownContentStale=!0}removeOption(e,t){let i=this;e=ui(e),i.uncacheValue(e),delete i.userOptions[e],delete i.options[e],i.isDropdownContentStale=!0,i.trigger("option_remove",e),i.removeItem(e,t)}clearOptions(e){let t=(e||this.clearFilter).bind(this);this.loadedSearches={},this.userOptions={},this.clearCache();let i={};ve(this.options,(r,o)=>{t(r,o)&&(i[o]=r)}),this.options=this.sifter.items=i,this.isDropdownContentStale=!0,this.trigger("option_clear")}clearFilter(e,t){return this.items.indexOf(t)>=0}getOption(e,t=!1){let i=Fe(e);if(i===null)return null;let r=this.options[i];if(r!=null){if(r.$div)return r.$div;if(t)return this._render("option",r)}return null}getAdjacent(e,t,i="option"){var r=this,o;if(!e)return null;i=="item"?o=r.controlChildren():o=r.dropdown_content.querySelectorAll("[data-selectable]");for(let s=0;s0?o[s+1]:o[s-1];return null}getItem(e){if(typeof e=="object")return e;var t=Fe(e);return t!==null?this.control.querySelector(`[data-value="${zo(t)}"]`):null}addItems(e,t){var i=this,r=Array.isArray(e)?e:[e];r=r.filter(s=>i.items.indexOf(s)===-1);let o=r[r.length-1];r.forEach(s=>{i.isPending=s!==o,i.addItem(s,t)})}addItem(e,t){var i=t?[]:["change","dropdown_close"];Vo(this,i,()=>{var r,o;let s=this,a=s.settings.mode,l=Fe(e);if(!(l&&s.items.indexOf(l)!==-1&&(a==="single"&&s.close(),a==="single"||!s.settings.duplicates))&&!(l===null||!s.options.hasOwnProperty(l))&&(a==="single"&&s.clear(t),!(a==="multi"&&s.isFull()))){if(r=s._render("item",s.options[l]),s.control.contains(r)&&(r=r.cloneNode(!0)),o=s.isFull(),s.items.splice(s.caretPos,0,l),s.insertAtCaret(r),s.isSetup){if(!s.isPending&&s.settings.hideSelected){let c=s.getOption(l),u=s.getAdjacent(c,1);u&&s.setActiveOption(u)}s.settings.clearAfterSelect&&s.setTextboxValue(),!s.isPending&&!s.settings.closeAfterSelect&&s.refreshOptions(s.isFocused&&a!=="single"),s.settings.closeAfterSelect!=!1&&s.isFull()?s.close():s.isPending||s.positionDropdown(),s.trigger("item_add",l,r),s.isPending||s.updateOriginalInput({silent:t})}(!s.isPending||!o&&s.isFull())&&(s.inputState(),s.refreshState())}})}removeItem(e=null,t){let i=this;if(e=i.getItem(e),!e)return;var r,o;let s=e.dataset.value;r=Wo(e),e.remove(),e.classList.contains("active")&&(o=i.activeItems.indexOf(e),i.activeItems.splice(o,1),gt(e,"active")),i.items.splice(r,1),i.isDropdownContentStale=!0,!i.settings.persist&&i.userOptions.hasOwnProperty(s)&&i.removeOption(s,t),r{}){arguments.length===3&&(t=arguments[2]),typeof t!="function"&&(t=()=>{});var i=this,r=i.caretPos,o;if(e=e||i.inputValue(),!i.canCreate(e))return Fe(e)&&this.options[e]&&i.addItem(e),t(),!1;i.lock();var s=!1,a=l=>{if(i.unlock(),!l||typeof l!="object")return t();var c=Fe(l[i.settings.valueField]);if(typeof c!="string")return t();i.setTextboxValue(),i.addOption(l,!0),i.setCaret(r),i.addItem(c),t(l),s=!0};return typeof i.settings.create=="function"?o=i.settings.create.call(this,e,a):o={[i.settings.labelField]:e,[i.settings.valueField]:e},s||a(o),!0}refreshItems(){var e=this;e.isDropdownContentStale=!0,e.isSetup&&e.addItems(e.items),e.updateOriginalInput(),e.refreshState()}refreshState(){let e=this;e.refreshValidityState();let t=e.isFull(),i=e.isLocked;e.wrapper.classList.toggle("rtl",e.rtl);let r=e.wrapper.classList;r.toggle("focus",e.isFocused),r.toggle("disabled",e.isDisabled),r.toggle("readonly",e.isReadOnly),r.toggle("required",e.isRequired),r.toggle("invalid",!e.isValid),r.toggle("locked",i),r.toggle("full",t),r.toggle("input-active",e.isFocused&&!e.isInputHidden),r.toggle("dropdown-active",e.isOpen),r.toggle("has-options",el(e.options)),r.toggle("has-items",e.items.length>0)}refreshValidityState(){var e=this;e.input.validity&&(e.isValid=e.input.validity.valid,e.isInvalid=!e.isValid)}isFull(){return this.settings.maxItems!==null&&this.items.length>=this.settings.maxItems}updateOriginalInput(e={}){let t=this;var i,r;let o=t.input.querySelector('option[value=""]');if(t.is_select_tag){let l=function(c,u,d){return c||(c=nt('")),c!=o&&t.input.append(c),s.push(c),(c!=o||a>0)&&(c.selected=!0),c},s=[],a=t.input.querySelectorAll("option:checked").length;t.input.querySelectorAll("option:checked").forEach(c=>{c.selected=!1}),t.items.length==0&&t.settings.mode=="single"?l(o,"",""):t.items.forEach(c=>{if(i=t.options[c],r=i[t.settings.labelField]||"",s.includes(i.$option)){let u=t.input.querySelector(`option[value="${zo(c)}"]:not(:checked)`);l(u,c,r)}else i.$option=l(i.$option,c,r)})}else t.input.value=t.getValue();t.isSetup&&(e.silent||t.trigger("change",t.getValue()))}open(){var e=this;e.isLocked||e.isOpen||e.settings.mode==="multi"&&e.isFull()||(e.isOpen=!0,le(e.focus_node,{"aria-expanded":"true"}),e.refreshState(),di(e.dropdown,{visibility:"hidden",display:"block"}),e.positionDropdown(),di(e.dropdown,{visibility:"visible",display:"block"}),e.focus(),e.trigger("dropdown_open",e.dropdown))}close(e=!0){var t=this,i=t.isOpen;e&&(t.setTextboxValue(),t.settings.mode==="single"&&t.items.length&&t.inputState()),t.isOpen=!1,le(t.focus_node,{"aria-expanded":"false"}),di(t.dropdown,{display:"none"}),t.settings.hideSelected&&t.clearActiveOption(),t.refreshState(),i&&t.trigger("dropdown_close",t.dropdown)}positionDropdown(){if(this.settings.dropdownParent==="body"){var e=this.control,t=e.getBoundingClientRect(),i=e.offsetHeight+t.top+window.scrollY,r=t.left+window.scrollX;di(this.dropdown,{width:t.width+"px",top:i+"px",left:r+"px"})}}clear(e){var t=this;if(t.items.length){var i=t.controlChildren();ve(i,r=>{t.removeItem(r,!0)}),t.inputState(),e||t.updateOriginalInput(),t.trigger("clear")}}insertAtCaret(e){let t=this,i=t.caretPos,r=t.control;r.insertBefore(e,r.children[i]||null),t.setCaret(i+1)}deleteSelection(e){var t,i,r,o,s=this;t=e&&e.keyCode===8?-1:1,i=Xa(s.control_input);let a=[];if(s.activeItems.length)o=qo(s.activeItems,t),r=Wo(o),t>0&&r++,ve(s.activeItems,l=>a.push(l));else if((s.isFocused||s.settings.mode==="single")&&s.items.length){let l=s.controlChildren(),c;t<0&&i.start===0&&i.length===0?c=l[s.caretPos-1]:t>0&&i.start===s.inputValue().length&&(c=l[s.caretPos]),c!==void 0&&a.push(c)}if(!s.shouldDelete(a,e))return!1;for(fe(e,!0),typeof r!="undefined"&&s.setCaret(r);a.length;)s.removeItem(a.pop());return s.inputState(),s.positionDropdown(),s.refreshOptions(!1),!0}shouldDelete(e,t){let i=e.map(r=>r.dataset.value);return!(!i.length||typeof this.settings.onDelete=="function"&&this.settings.onDelete.call(this,i,t)===!1)}advanceSelection(e,t){var i,r,o=this;o.rtl&&(e*=-1),!o.inputValue().length&&(Mt(hi,t)||Mt("shiftKey",t)?(i=o.getLastActive(e),i?i.classList.contains("active")?r=o.getAdjacent(i,e,"item"):r=i:e>0?r=o.control_input.nextElementSibling:r=o.control_input.previousElementSibling,r&&(r.classList.contains("active")&&o.removeActiveItem(i),o.setActiveItemClass(r))):o.moveCaret(e))}moveCaret(e){}getLastActive(e){let t=this.control.querySelector(".last-active");if(t)return t;var i=this.control.querySelectorAll(".active");if(i)return qo(i,e)}setCaret(e){this.caretPos=this.items.length}controlChildren(){return Array.from(this.control.querySelectorAll("[data-ts-item]"))}lock(){this.setLocked(!0)}unlock(){this.setLocked(!1)}setLocked(e=this.isReadOnly||this.isDisabled){this.isLocked=e,this.refreshState()}disable(){this.setDisabled(!0),this.close()}enable(){this.setDisabled(!1)}setDisabled(e){this.focus_node.tabIndex=e?-1:this.tabIndex,this.isDisabled=e,this.input.disabled=e,this.control_input.disabled=e,this.setLocked()}setReadOnly(e){this.isReadOnly=e,this.input.readOnly=e,this.control_input.readOnly=e,this.setLocked()}destroy(){var e=this,t=e.revertSettings;e.trigger("destroy"),e.off(),e.wrapper.remove(),e.dropdown.remove(),e.input.innerHTML=t.innerHTML,e.input.tabIndex=t.tabIndex,gt(e.input,"tomselected","ts-hidden-accessible"),e._destroy(),delete e.input.tomselect}render(e,t){var i,r;let o=this;if(typeof this.settings.render[e]!="function"||(r=o.settings.render[e].call(this,t,dr),!r))return null;if(r=nt(r),e==="option"||e==="option_create"?t[o.settings.disabledField]?le(r,{"aria-disabled":"true"}):le(r,{"data-selectable":""}):e==="optgroup"&&(i=t.group[o.settings.optgroupValueField],le(r,{"data-group":i}),t.group[o.settings.disabledField]&&le(r,{"data-disabled":""})),e==="option"||e==="item"){let s=ui(t[o.settings.valueField]);le(r,{"data-value":s}),e==="item"?($e(r,o.settings.itemClass),le(r,{"data-ts-item":""})):($e(r,o.settings.optionClass),le(r,{role:"option",id:t.$id}),t.$div=r,o.options[s]=t)}return r}_render(e,t){let i=this.render(e,t);if(i==null)throw"HTMLElement expected";return i}clearCache(){ve(this.options,e=>{e.$div&&(e.$div.remove(),delete e.$div)})}uncacheValue(e){let t=this.getOption(e);t&&t.remove()}canCreate(e){return this.settings.create&&e.length>0&&this.settings.createFilter.call(this,e)}hook(e,t,i){var r=this,o=r[t];r[t]=function(){var s,a;return e==="after"&&(s=o.apply(r,arguments)),a=i.apply(r,arguments),e==="instead"?a:(e==="before"&&(s=o.apply(r,arguments)),s)}}};var Am=(n,e,t,i)=>{n.addEventListener(e,t,i)};function sl(){Am(this.input,"change",()=>{this.sync()})}var Dm=n=>typeof n=="undefined"||n===null?null:Om(n),Om=n=>typeof n=="boolean"?n?"1":"0":n+"",al=(n,e=!1)=>{n&&(n.preventDefault(),e&&n.stopPropagation())},Lm=n=>{if(n.jquery)return n[0];if(n instanceof HTMLElement)return n;if(Mm(n)){var e=document.createElement("template");return e.innerHTML=n.trim(),e.content.firstChild}return document.querySelector(n)},Mm=n=>typeof n=="string"&&n.indexOf("<")>-1;function ll(n){var e=this,t=e.onOptionSelect;e.settings.hideSelected=!1;let i=Object.assign({className:"tomselect-checkbox",checkedClassNames:void 0,uncheckedClassNames:void 0},n);var r=function(a,l){l?(a.checked=!0,i.uncheckedClassNames&&a.classList.remove(...i.uncheckedClassNames),i.checkedClassNames&&a.classList.add(...i.checkedClassNames)):(a.checked=!1,i.checkedClassNames&&a.classList.remove(...i.checkedClassNames),i.uncheckedClassNames&&a.classList.add(...i.uncheckedClassNames))},o=function(a){setTimeout(()=>{var l=a.querySelector("input."+i.className);l instanceof HTMLInputElement&&r(l,a.classList.contains("selected"))},1)};e.hook("after","setupTemplates",()=>{var s=e.settings.render.option;e.settings.render.option=(a,l)=>{var c=Lm(s.call(e,a,l)),u=document.createElement("input");i.className&&u.classList.add(i.className),u.addEventListener("click",function(p){al(p)}),u.type="checkbox";let d=Dm(a[e.settings.valueField]);return r(u,!!(d&&e.items.indexOf(d)>-1)),c.prepend(u),c}}),e.on("item_remove",s=>{var a=e.getOption(s);a&&(a.classList.remove("selected"),o(a))}),e.on("item_add",s=>{var a=e.getOption(s);a&&o(a)}),e.hook("instead","onOptionSelect",(s,a)=>{if(a.classList.contains("selected")){a.classList.remove("selected"),e.removeItem(a.dataset.value),e.refreshOptions(),al(s,!0);return}t.call(e,s,a),o(a)})}var Nm=n=>{if(n.jquery)return n[0];if(n instanceof HTMLElement)return n;if(km(n)){var e=document.createElement("template");return e.innerHTML=n.trim(),e.content.firstChild}return document.querySelector(n)},km=n=>typeof n=="string"&&n.indexOf("<")>-1;function cl(n){let e=this,t=Object.assign({className:"clear-button",title:"Clear All",role:"button",tabindex:0,html:i=>`
×
`},n);e.on("initialize",()=>{var i=Nm(t.html(t));i.addEventListener("click",r=>{e.isLocked||(e.clear(),e.settings.mode==="single"&&e.settings.allowEmptyOption&&e.addItem(""),e.refreshOptions(!1),r.preventDefault(),r.stopPropagation())}),e.control.appendChild(i)})}var Hm=(n,e=!1)=>{n&&(n.preventDefault(),e&&n.stopPropagation())},On=(n,e,t,i)=>{n.addEventListener(e,t,i)},Im=(n,e)=>{if(Array.isArray(n))n.forEach(e);else for(var t in n)n.hasOwnProperty(t)&&e(n[t],t)},Rm=n=>{if(n.jquery)return n[0];if(n instanceof HTMLElement)return n;if(Pm(n)){var e=document.createElement("template");return e.innerHTML=n.trim(),e.content.firstChild}return document.querySelector(n)},Pm=n=>typeof n=="string"&&n.indexOf("<")>-1,Fm=(n,e)=>{Im(e,(t,i)=>{t==null?n.removeAttribute(i):n.setAttribute(i,""+t)})},$m=(n,e)=>{var t;(t=n.parentNode)==null||t.insertBefore(e,n.nextSibling)},Bm=(n,e)=>{var t;(t=n.parentNode)==null||t.insertBefore(e,n)},Vm=(n,e)=>{do{var t;if(e=(t=e)==null?void 0:t.previousElementSibling,n==e)return!0}while(e&&e.previousElementSibling);return!1};function ul(){var n=this;if(n.settings.mode!=="multi")return;var e=n.lock,t=n.unlock;let i=!0,r;n.hook("after","setupTemplates",()=>{var o=n.settings.render.item;n.settings.render.item=(s,a)=>{let l=Rm(o.call(n,s,a));Fm(l,{draggable:"true"});let c=v=>{i||Hm(v),v.stopPropagation()},u=v=>{r=l,setTimeout(()=>{l.classList.add("ts-dragging")},0)},d=v=>{v.preventDefault(),l.classList.add("ts-drag-over"),E(l,r)},p=()=>{l.classList.remove("ts-drag-over")},E=(v,_)=>{_!==void 0&&(Vm(_,l)?$m(v,_):Bm(v,_))},m=()=>{var v;document.querySelectorAll(".ts-drag-over").forEach(C=>C.classList.remove("ts-drag-over")),(v=r)==null||v.classList.remove("ts-dragging"),r=void 0;var _=[];n.control.querySelectorAll("[data-value]").forEach(C=>{if(C.dataset.value){let D=C.dataset.value;D&&_.push(D)}}),n.setValue(_)};return On(l,"mousedown",c),On(l,"dragstart",u),On(l,"dragenter",d),On(l,"dragover",d),On(l,"dragleave",p),On(l,"dragend",m),l}}),n.hook("instead","lock",()=>(i=!1,e.call(n))),n.hook("instead","unlock",()=>(i=!0,t.call(n)))}var zm=(n,e=!1)=>{n&&(n.preventDefault(),e&&n.stopPropagation())},jm=n=>{if(n.jquery)return n[0];if(n instanceof HTMLElement)return n;if(qm(n)){var e=document.createElement("template");return e.innerHTML=n.trim(),e.content.firstChild}return document.querySelector(n)},qm=n=>typeof n=="string"&&n.indexOf("<")>-1;function dl(n){let e=this,t=Object.assign({title:"Untitled",headerClass:"dropdown-header",titleRowClass:"dropdown-header-title",labelClass:"dropdown-header-label",closeClass:"dropdown-header-close",html:i=>'
'+i.title+'×
'},n);e.on("initialize",()=>{var i=jm(t.html(t)),r=i.querySelector("."+t.closeClass);r&&r.addEventListener("click",o=>{zm(o,!0),e.close()}),e.dropdown.insertBefore(i,e.dropdown.firstChild)})}var Wm=(n,e)=>{if(Array.isArray(n))n.forEach(e);else for(var t in n)n.hasOwnProperty(t)&&e(n[t],t)},Um=(n,...e)=>{var t=Ym(e);n=Gm(n),n.map(i=>{t.map(r=>{i.classList.remove(r)})})},Ym=n=>{var e=[];return Wm(n,t=>{typeof t=="string"&&(t=t.trim().split(/[\t\n\f\r\s]/)),Array.isArray(t)&&(e=e.concat(t))}),e.filter(Boolean)},Gm=n=>(Array.isArray(n)||(n=[n]),n),Km=(n,e)=>{if(!n)return-1;e=e||n.nodeName;for(var t=0;n=n.previousElementSibling;)n.matches(e)&&t++;return t};function fl(){var n=this;n.hook("instead","setCaret",e=>{n.settings.mode==="single"||!n.control.contains(n.control_input)?e=n.items.length:(e=Math.max(0,Math.min(n.items.length,e)),e!=n.caretPos&&!n.isPending&&n.controlChildren().forEach((t,i)=>{i{if(!n.isFocused)return;let t=n.getLastActive(e);if(t){let i=Km(t);n.setCaret(e>0?i+1:i),n.setActiveItem(),Um(t,"last-active")}else n.setCaret(n.caretPos+e)})}var Xm=(n,e=!1)=>{n&&(n.preventDefault(),e&&n.stopPropagation())},Qm=(n,e,t,i)=>{n.addEventListener(e,t,i)},Jm=(n,e)=>{if(Array.isArray(n))n.forEach(e);else for(var t in n)n.hasOwnProperty(t)&&e(n[t],t)},hl=n=>{if(n.jquery)return n[0];if(n instanceof HTMLElement)return n;if(Zm(n)){var e=document.createElement("template");return e.innerHTML=n.trim(),e.content.firstChild}return document.querySelector(n)},Zm=n=>typeof n=="string"&&n.indexOf("<")>-1,eg=(n,...e)=>{var t=tg(e);n=ng(n),n.map(i=>{t.map(r=>{i.classList.add(r)})})},tg=n=>{var e=[];return Jm(n,t=>{typeof t=="string"&&(t=t.trim().split(/[\t\n\f\r\s]/)),Array.isArray(t)&&(e=e.concat(t))}),e.filter(Boolean)},ng=n=>(Array.isArray(n)||(n=[n]),n);function pl(){let n=this;n.settings.shouldOpen=!0,n.hook("before","setup",()=>{var e;n.focus_node=n.control,eg(n.control_input,"dropdown-input");let t=hl('