commit 214516ac676e10df4387b80d60f91ecbf8fb808e Author: teraflops Date: Thu May 29 22:40:58 2025 +0200 initial commit diff --git a/.zsh/completions/_pastebin_client b/.zsh/completions/_pastebin_client new file mode 100644 index 0000000..e940d34 --- /dev/null +++ b/.zsh/completions/_pastebin_client @@ -0,0 +1,190 @@ +#compdef pastebin_client.sh + +_pastebin_client() { + local context state line + typeset -A opt_args + API_URL="https://paste.priet.us" + + # List of main subcommands + local -a subcommands + subcommands=( + 'login:Log in with username and password' + 'create:Create a paste from STDIN' + 'upload:Upload a file' + 'list:List all pastes with optional filters' + 'view:Show the content of a paste' + 'view_raw:Show raw content of a paste' + 'delete:Delete a paste' + 'register:Register a new user' + 'details:Show details about the logged-in user' + 'search:Search for pastes by a search term' + 'favorite:Add a paste to favorites' + 'unfavorite:Remove a paste from favorites' + 'download:Download the content of a paste' + 'download_all:Download all pastes' + 'download_favorites:Download all favorite pastes' + 'list_favorites:List all favorite pastes' + 'list_pastes:List all pastes of current user' + 'shared_with_others:List pastes shared with other users' + 'shared_with_me:List pastes others shared with me' + 'share:Share a paste with another user' + 'unshare:Unshare a paste with a user' + 'edit:Edit paste from command line and upload changes to the server' + 'remove_gps: remove gps metadata from pastte id' + ) + + # Define the autocompletion logic + _arguments -C \ + '1:subcommand:->cmd' \ + '*::argument:->args' + + case $state in + cmd) + # Display available subcommands + _describe -t subcommands 'subcommand' subcommands + ;; + args) + case $words[1] in + login) + # login + _arguments \ + '1:Username:_default' \ + '2:Password:_default' + ;; + create) + _arguments \ + '1:Expire after 1 day:_expire_options' \ + '2:Private:_private_options' # ✅ Referencia a listas de valores + ;; + remove_gps) + # remove_gps + _dynamic_paste_ids + ;; + # upload + upload) + _arguments \ + '1:File:_files' \ + '2:Expire after 1 day:_expire_options' \ + '3:Private:_private_options' # ✅ Referencia a listas de valores + ;; + list) + # list options: --type, --from, --to + _arguments \ + '--type=[Filter by paste type]:type:(Text Image Video)' \ + '--from=[Start date (YYYY-MM-DD)]' \ + '--to=[End date (YYYY-MM-DD)]' + ;; + view|view_raw|delete|favorite|unfavorite|download|edit) + # view , view_raw , delete , favorite , unfavorite , download , edit + _dynamic_paste_ids + ;; + download_all|download_favorites|list_favorites) + # No additional arguments + ;; + register) + # register + _arguments \ + '1:Username:_default' \ + '2:Password:_default' + ;; + details) + # details (no arguments) + ;; + search) + # search + _arguments '1:Search query:_default' + ;; + share|unshare) + # share [can_edit] + if [[ $words[1] == "share" ]]; then + _arguments \ + '1:Paste ID:_dynamic_paste_ids' \ + '2:Username:_dynamic_usernames' \ + '3:Can Edit:(true false)' + else + # unshare + _arguments \ + '1:Paste ID:_dynamic_paste_ids' \ + '2:Username:_dynamic_usernames' + fi + ;; + esac + ;; + esac +} + +_expire_options() { + _values "Expiration option" \ + "yes[The paste will expire after 1 day]" \ + "no[The paste will not expire]" +} + +_private_options() { + _values "Privacy option" \ + "yes[The paste will be private]" \ + "no[The paste will be public]" +} + +# Function to dynamically fetch paste IDs from the server +_dynamic_paste_ids() { + local token_file="$HOME/.pastebin_token" + local token + local response + local paste_ids=() + + # Check if the token file exists + if [[ -f "$token_file" ]]; then + token=$(cat "$token_file") + else + _message "No token found. Please log in first." + return + fi + + # Fetch paste IDs using the API + response=$(curl -s -H "Authorization: Bearer $token" "$API_URL/pastes") + + # Parse the response to extract paste IDs + if [[ $? -eq 0 ]]; then + paste_ids=($(echo "$response" | jq -r '.[].id')) + fi + + if (( ${#paste_ids[@]} )); then + _values "paste_id" "${paste_ids[@]}" + else + _message "No pastes found." + fi +} + +# Function to dynamically fetch usernames from the server +_dynamic_usernames() { + local token_file="$HOME/.pastebin_token" + local token + local response + local usernames=() + + # Check if the token file exists + if [[ -f "$token_file" ]]; then + token=$(cat "$token_file") + else + _message "No token found. Please log in first." + return + fi + + # Fetch usernames using the API + response=$(curl -s -H "Authorization: Bearer $token" "$API_URL/api/users") + + # Parse the response to extract usernames + if [[ $? -eq 0 ]]; then + usernames=($(echo "$response" | jq -r '.users[]')) + fi + + if (( ${#usernames[@]} )); then + _values "username" "${usernames[@]}" + else + _message "No users found." + fi +} + +# Associate the autocompletion function with the script name +compdef _pastebin_client pastebin_client.sh + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..81de4eb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.9-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + libpq-dev \ + build-essential \ + python3-dev \ + libmagic-dev \ + mediainfo \ + tesseract-ocr \ + libtesseract-dev \ + poppler-utils \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY . . + +RUN pip install --no-cache-dir -r requirements.txt + +EXPOSE 5000 + +CMD ["gunicorn", "--preload", "-w", "4", "-b", "0.0.0.0:5000", "--timeout", "120", "app:app", "--log-level=debug"] + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ed11477 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +BSD 2-Clause License + +Copyright (c) 2024, [Tu Nombre o Nombre de tu Organización] +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..4bfe2a1 --- /dev/null +++ b/README.md @@ -0,0 +1,261 @@ +# MyPastebin + +**MyPastebin** is an application for creating and sharing text or file pastes with syntax highlighting. You can also access content in raw format and manage it through an API or a Bash client. + +## Features + +- Create text pastes or upload files with syntax highlighting. +- Supports images, videos, and generic binary files. +- View pastes in HTML or raw format. +- User management: register new users and list existing users (admin only). +- Interact with the application using `curl` or a Bash client. +- List and delete pastes with authentication. +- Deployable with Docker and Kubernetes. + +## Technologies Used + +- **Python** (Flask) for the backend. +- **SQLite** or **postgresql** as a local database. +- **Pygments** for syntax highlighting. +- **Docker** and **Kubernetes** for deployments. + +## Installation and Usage + +### Using Docker + +1. Build the Docker image: + ```bash + docker build -t mypastebin . + +2. Start the container with Docker Compose:: + ```bash + docker-compose up -d + ``` + +3. The application will be available at `http://localhost:5000`. + +### Using Kubernetes + +1. Apply the Kubernetes manifests: + ```bash + kubectl apply -f k8s/ + ``` + +2. Access the service through the Ingress configured in your cluster. + +### Acceso a la Aplicación + +the app will be available at : [https://paste.priet.us](https://paste.priet.us). + +Bash Client +----------- + +The Bash client simplifies interaction with the service. Download it here: + +[Download Bash Client](https://gitlab.com/teraflops/mypastebin/-/blob/main/pastebin_client.sh) | [GitLab Repository](https://gitlab.com/teraflops/mypastebin) + +### Examples + +* **Authentication:** + + ./pastebin_client.sh login admin password123 + +* **Create a Paste (with expiration):** + + echo "Hello World" | ./pastebin_client.sh create plaintext yes + + Creates a paste in `plaintext` format that expires in 1 day. + +* **Upload a File (with expiration):** + + ./pastebin_client.sh upload script.py python yes + + Uploads `script.py` as a Python paste that expires in 1 day. + +* **View a Paste:** + + ./pastebin_client.sh view 1 + +* **View Raw Content:** + + ./pastebin_client.sh view_raw 1 + +* **List Pastes:** + + ./pastebin_client.sh list + +* **Delete a Paste:** + + ./pastebin_client.sh delete 1 + +* **Search Paste Contents:** + + ./pastebin_client.sh search "search term" + +* **User Details:** + + ./pastebin_client.sh details + +* **Mark a Paste as Favorite:** + + ./pastebin_client.sh favorite 1 + +* **Remove a Paste from Favorites:** + + ./pastebin_client.sh unfavorite 1 + +* **Download a Paste:** + + ./pastebin_client.sh download 1 + +* **List Favorites:** + + ./pastebin_client.sh list_favorites + +* **List Pastes Shared With Others:** + + ./pastebin_client.sh shared_with_others + +* **List Pastes Shared With Me:** + + ./pastebin_client.sh shared_with_me + +* **Share a Paste:** + + ./pastebin_client.sh share 1 username true + + Shares paste with ID 1 with `username`, allowing edit if `true` is passed. + +* **Unshare a Paste:** + + ./pastebin_client.sh unshare 1 test + +* **Edit a Paste (via Editor):** + + ./pastebin_client.sh edit 1 + + Opens paste with ID 1 in your default editor. Upon saving and exiting, the updated content is sent to the server. + + +Curl Examples +------------- + +Interact directly with the service using `curl`. + +* **Authentication:** + + curl -X POST {{ request.host_url }}api/token \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"password123"}' + +* **Create a Paste (with expiration):** + + echo "Hello World" | curl -X POST {{ request.host_url }}paste \ + -H "Authorization: Bearer " \ + -F "c=@-" \ + -F "lang=plaintext" \ + -F "expire=yes" + + Creates a paste that expires in 1 day. Use `expire=no` for permanent pastes. + +* **Upload a File (with expiration):** + + curl -X POST {{ request.host_url }}paste \ + -H "Authorization: Bearer " \ + -F "c=@script.py" \ + -F "lang=python" \ + -F "expire=yes" + + Uploads `script.py` as a Python paste that expires in 1 day. + +* **View a Paste:** + + curl {{ request.host_url }}paste/1/json \ + -H "Authorization: Bearer " + +* **View Raw Content:** + + curl {{ request.host_url }}paste/1/raw \ + -H "Authorization: Bearer " + +* **List Pastes:** + + curl -H "Authorization: Bearer " {{ request.host_url }}pastes + +* **Delete a Paste:** + + curl -X DELETE {{ request.host_url }}paste/1 \ + -H "Authorization: Bearer " + +* **Search Paste Contents:** + + curl -X GET {{ request.host_url }}pastes/search?q="search term" \ + -H "Authorization: Bearer " + +* **User Details:** + + curl -X GET {{ request.host_url }}user/details \ + -H "Authorization: Bearer " + +* **Mark a Paste as Favorite:** + + curl -X POST {{ request.host_url }}api/paste/1/favorite \ + -H "Authorization: Bearer " + +* **Remove a Paste from Favorites:** + + curl -X POST {{ request.host_url }}api/paste/1/unfavorite \ + -H "Authorization: Bearer " + +* **Download a Paste:** + + curl -X GET {{ request.host_url }}api/paste/1/download \ + -H "Authorization: Bearer " \ + -J -O + +* **List Favorites:** + + curl -X GET {{ request.host_url }}api/favorites \ + -H "Authorization: Bearer " + +* **List Pastes Shared With Others:** + + curl -X GET {{ request.host_url }}api/shared_with_others \ + -H "Authorization: Bearer " + +* **List Pastes Shared With Me:** + + curl -X GET {{ request.host_url }}api/shared_with_me \ + -H "Authorization: Bearer " + +* **Share a Paste:** + + curl -X POST {{ request.host_url }}api/paste/1/share \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "username": "test_user", + "can_edit": true + }' + + Shares paste with ID 1 with `test_user`, allowing edit if `can_edit` is `true`. + +* **Unshare a Paste:** + + curl -X POST {{ request.host_url }}api/paste/1/unshare \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "username": "test" + }' + +* **Edit a Paste:** + + curl -X PUT {{ request.host_url }}api/paste/1 \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "content": "Updated content here" + }' + + Overwrites the content of paste with ID 1 using JSON. Make sure the authenticated user has edit permission. diff --git a/app.py b/app.py new file mode 100644 index 0000000..164a915 --- /dev/null +++ b/app.py @@ -0,0 +1,116 @@ +from flask import Flask, render_template, request, redirect, url_for, flash, session +from src.models import db, User, Paste +from src.routes import init_routes +from src.auth import jwt_required +from config import SQLALCHEMY_DATABASE_URI, SQLALCHEMY_ENGINE_OPTIONS +from werkzeug.security import generate_password_hash +from sqlalchemy.exc import IntegrityError +import os +from flask_login import LoginManager, login_user, current_user, logout_user, login_required +from werkzeug.middleware.proxy_fix import ProxyFix +import logging +from config import UPLOAD_FOLDER +from flask_migrate import Migrate +from flask_apscheduler import APScheduler +from src.routes import delete_expired_pastes +from elasticsearch import Elasticsearch + +# Inicializa la aplicación Flask +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = SQLALCHEMY_DATABASE_URI +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['SQLALCHEMY_ENGINE_OPTIONS'] = SQLALCHEMY_ENGINE_OPTIONS +app.secret_key = 'admin_console_secret_key' +app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER + +# Inicializa la base de datos +db.init_app(app) +migrate = Migrate(app, db) + +# Inicializa Flask-Login +login_manager = LoginManager() +login_manager.init_app(app) +login_manager.login_view = 'login' +login_manager.login_message_category = 'info' + +# Cargador de usuario para Flask-Login +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) # Asegúrate de que la ruta de importación sea correcta + +# Conexión con Elasticsearch +es = Elasticsearch(["http://elasticsearch:9200"]) + +def create_paste_index(): + """ + Crea el índice 'pastes' en Elasticsearch si no existe. + """ + index_name = "pastes" + + mappings = { + "mappings": { + "properties": { + "id": { "type": "keyword" }, + "title": { "type": "text" }, + "content": { "type": "text" }, + "owner_id": { "type": "keyword" }, + "language": { "type": "keyword" }, + "content_type": { "type": "keyword" }, + "created_at": { "type": "date" }, + "private": { "type": "boolean" }, + "shared_with": { "type": "keyword" } # ✅ Se deja sin `ignore_above` para múltiples valores + } + } + } + + # Eliminar índice si ya existe (para evitar conflictos) + if es.indices.exists(index=index_name): + es.indices.delete(index=index_name) + print(f"[INFO] Índice '{index_name}' eliminado antes de la recreación.") + + # Crear el índice con la nueva estructura + es.indices.create(index=index_name, body=mappings) + print(f"[INFO] Índice '{index_name}' creado correctamente en Elasticsearch.") + +# Ejecuta la creación del índice en el contexto de la app +with app.app_context(): + db.create_all() + create_paste_index() # 📌 Aquí se crea el índice antes de inicializar las rutas + + default_username = os.getenv("VALID_USER", "admin") + default_password = os.getenv("VALID_PASS", "password123") + try: + if not User.query.filter_by(username=default_username).first(): + user = User(username=default_username) + user.set_password(default_password) + db.session.add(user) + db.session.commit() + print(f"Usuario por defecto creado: {default_username}") + else: + print(f"Usuario por defecto ya existe: {default_username}") + except IntegrityError: + db.session.rollback() + print("Usuario admin ya existía. Continuando...") + +# Inicializar rutas +init_routes(app) + +scheduler = APScheduler() + +def delete_expired_pastes_task(): + """Ejecuta la limpieza de pastes expirados dentro del contexto de Flask""" + with app.app_context(): + delete_expired_pastes() + +def setup_scheduler(app): + """Configura y arranca el scheduler""" + scheduler.init_app(app) + scheduler.start() + scheduler.add_job(id='delete_expired_pastes', func=delete_expired_pastes_task, trigger='interval', hours=1) + +setup_scheduler(app) + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5000) + diff --git a/config.py b/config.py new file mode 100644 index 0000000..7ac18bf --- /dev/null +++ b/config.py @@ -0,0 +1,65 @@ +import os + +# Base de datos +DB_ENGINE = os.getenv("DB_ENGINE", "sqlite") + +POSTGRES_USER = os.getenv("POSTGRES_USER", "mypasteuser") +POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD", "mypastepassword") +POSTGRES_DB = os.getenv("POSTGRES_DB", "mypastedb") +POSTGRES_HOST = os.getenv("POSTGRES_HOST", "db") +POSTGRES_PORT = os.getenv("POSTGRES_PORT", "5432") + +DB_URI_POSTGRES = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}" + +DB_PATH = os.getenv("DB_PATH", "/app/database/database.db") +DB_URI_SQLITE = f"sqlite:///{DB_PATH}" + +if DB_ENGINE == "postgres": + SQLALCHEMY_DATABASE_URI = DB_URI_POSTGRES + SQLALCHEMY_ENGINE_OPTIONS = { + "pool_pre_ping": True, + "pool_recycle": 1800, # Recycle connections every 30 minutes + "pool_size": 10, # Set the pool size + "max_overflow": 20 # Allow a maximum of 20 connections to exceed the pool size + } +else: + SQLALCHEMY_DATABASE_URI = DB_URI_SQLITE + SQLALCHEMY_ENGINE_OPTIONS = {} + +# Clave secreta para JWT +SECRET_KEY = os.getenv("SECRET_KEY", "lñkkjkjkñkljñkjñkljlkjñklljñkjñlkj") # Mejor usar una clave generada al desplegar +JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256") +JWT_EXP_DELTA_SECONDS = int(os.getenv("JWT_EXP_DELTA_SECONDS", 3600)) + +# Ruta para subir archivos +UPLOAD_FOLDER = os.getenv("UPLOAD_FOLDER", "/app/uploads") + +# Configuración de SMTP +SMTP_SERVER = os.getenv("SMTP_SERVER", "priet.us") +SMTP_PORT = int(os.getenv("SMTP_PORT", 465)) # Usa 465 para SSL +SMTP_USERNAME = os.getenv("SMTP_USERNAME", "") +SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "") +SMTP_USE_TLS = os.getenv("SMTP_USE_TLS", "false").lower() == "true" +SMTP_USE_SSL = os.getenv("SMTP_USE_SSL", "true").lower() == "true" + +# config.py +ROLE_STORAGE_LIMITS = { + 'admin': -1, # Ilimitado + 'advanced': 2 * 1024**3, # 2GB en bytes + 'user': 1 * 1024**3, # 1GB en bytes +} + +# Debugging (opcional) +if __name__ == "__main__": + print("Current configuration:") + print(f"DB_ENGINE: {DB_ENGINE}") + print(f"SQLALCHEMY_DATABASE_URI: {SQLALCHEMY_DATABASE_URI}") + print(f"JWT_ALGORITHM: {JWT_ALGORITHM}") + print(f"JWT_EXP_DELTA_SECONDS: {JWT_EXP_DELTA_SECONDS}") + print(f"UPLOAD_FOLDER: {UPLOAD_FOLDER}") + print(f"SMTP_SERVER: {SMTP_SERVER}") + print(f"SMTP_PORT: {SMTP_PORT}") + print(f"SMTP_USERNAME: {SMTP_USERNAME}") + print(f"SMTP_USE_TLS: {SMTP_USE_TLS}") + print(f"SMTP_USE_SSL: {SMTP_USE_SSL}") + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5f5a4de --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,67 @@ +services: + db: + image: postgres:15 + environment: + - POSTGRES_USER=mypasteuser + - POSTGRES_PASSWORD=mypastepassword + - POSTGRES_DB=mypastedb + volumes: + - db_data:/var/lib/postgresql/data + networks: + - my_network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mypasteuser -d mypastedb"] + interval: 5s + timeout: 3s + retries: 5 + + app: + build: . + ports: + - "5000:5000" + volumes: + - ./uploads:/app/uploads + environment: + - DB_ENGINE=postgres + - POSTGRES_USER=mypasteuser + - POSTGRES_PASSWORD=mypastepassword + - POSTGRES_DB=mypastedb + - POSTGRES_HOST=db + - POSTGRES_PORT=5432 + - VALID_USER=admin + - VALID_PASS=password + - FLASK_ENV=production + - ELASTICSEARCH_HOST=http://elasticsearch:9200 # Agregado + depends_on: + db: + condition: service_healthy + elasticsearch: + condition: service_started + networks: + - my_network + + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.9.0 + environment: + - discovery.type=single-node # Configuración básica para un nodo único + - ES_JAVA_OPTS=-Xms512m -Xmx512m # Configuración de memoria + - xpack.security.enabled=false + - action.auto_create_index=true + ports: + - "9200:9200" # Puerto HTTP + - "9300:9300" # Puerto de comunicación entre nodos + networks: + - my_network + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:9200 || exit 1"] + interval: 5s + timeout: 3s + retries: 5 + +volumes: + db_data: + +networks: + my_network: + driver: bridge + diff --git a/generate_css.py b/generate_css.py new file mode 100644 index 0000000..eec1503 --- /dev/null +++ b/generate_css.py @@ -0,0 +1,25 @@ +import os +from pygments.styles import get_all_styles +from pygments.formatters import HtmlFormatter + +# Define la carpeta de destino para los archivos CSS +OUTPUT_DIR = os.path.join('static', 'css') + +# Crea la carpeta si no existe +os.makedirs(OUTPUT_DIR, exist_ok=True) + +# Itera sobre todos los estilos disponibles en Pygments +for style in get_all_styles(): + formatter = HtmlFormatter(style=style, linenos=True, cssclass="highlight") + css = formatter.get_style_defs('.highlight') + + # Define el nombre del archivo CSS + css_filename = f"{style}.css" + css_path = os.path.join(OUTPUT_DIR, css_filename) + + # Escribe el CSS en el archivo + with open(css_path, 'w') as f: + f.write(css) + + print(f"Generado: {css_path}") + diff --git a/k8s/cdn/cdn-deployment.yaml b/k8s/cdn/cdn-deployment.yaml new file mode 100644 index 0000000..392a861 --- /dev/null +++ b/k8s/cdn/cdn-deployment.yaml @@ -0,0 +1,35 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: static-content + labels: + app: static-content +spec: + replicas: 1 + selector: + matchLabels: + app: static-content + template: + metadata: + labels: + app: static-content + spec: + containers: + - name: nginx + image: nginx:latest + ports: + - containerPort: 80 + volumeMounts: + - name: static-files + mountPath: /usr/share/nginx/html + - name: nginx-config + mountPath: /etc/nginx/conf.d/default.conf + subPath: default.conf + volumes: + - name: static-files + persistentVolumeClaim: + claimName: static-pvc + - name: nginx-config + configMap: + name: nginx-config + diff --git a/k8s/cdn/cdn-ingress.yaml b/k8s/cdn/cdn-ingress.yaml new file mode 100644 index 0000000..0801c7b --- /dev/null +++ b/k8s/cdn/cdn-ingress.yaml @@ -0,0 +1,28 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: cdn-ingress + namespace: default + annotations: + kubernetes.io/ingress.class: traefik + cert-manager.io/cluster-issuer: letsencrypt-prod-dns + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: "true" + traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt +spec: + rules: + - host: cdn.priet.us + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: static-content-service + port: + number: 80 + tls: + - hosts: + - cdn.priet.us + secretName: cdn-tls + diff --git a/k8s/cdn/cdn-nginx-configmap.yaml b/k8s/cdn/cdn-nginx-configmap.yaml new file mode 100644 index 0000000..ada2574 --- /dev/null +++ b/k8s/cdn/cdn-nginx-configmap.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nginx-config + labels: + app: static-content +data: + default.conf: | + server { + listen 80; + server_name cdn.priet.us; + + root /usr/share/nginx/html; + + # Bloque para manejar solicitudes de archivos estáticos con CORS + location ~* \.(css|js|woff|woff2|ttf|eot|svg)$ { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept' always; + + # Manejar solicitudes OPTIONS (preflight) + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept' always; + add_header 'Content-Length' 0; + add_header 'Content-Type' 'text/plain; charset=UTF-8'; + return 204; + } + + try_files $uri $uri/ =404; + } + + # Bloque para manejar otras solicitudes + location / { + try_files $uri $uri/ =404; + } + } + diff --git a/k8s/cdn/cdn-pvc.yaml b/k8s/cdn/cdn-pvc.yaml new file mode 100644 index 0000000..0997173 --- /dev/null +++ b/k8s/cdn/cdn-pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: static-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: longhorn + diff --git a/k8s/cdn/cdn-service.yaml b/k8s/cdn/cdn-service.yaml new file mode 100644 index 0000000..891ce0f --- /dev/null +++ b/k8s/cdn/cdn-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: static-content-service + namespace: default +spec: + selector: + app: static-content + ports: + - protocol: TCP + port: 80 + targetPort: 80 + type: ClusterIP + diff --git a/k8s/cdn/default.conf b/k8s/cdn/default.conf new file mode 100644 index 0000000..d6185ec --- /dev/null +++ b/k8s/cdn/default.conf @@ -0,0 +1,31 @@ +server { + listen 80; + server_name localhost; + + root /usr/share/nginx/html; + + # Bloque para manejar solicitudes de archivos estáticos con CORS + location ~* \.(css|js|woff|woff2|ttf|eot|svg)$ { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept' always; + + # Manejar solicitudes OPTIONS (preflight) + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept' always; + add_header 'Content-Length' 0; + add_header 'Content-Type' 'text/plain; charset=UTF-8'; + return 204; + } + + try_files $uri $uri/ =404; + } + + # Bloque para manejar otras solicitudes + location / { + try_files $uri $uri/ =404; + } +} + diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml new file mode 100644 index 0000000..f88f208 --- /dev/null +++ b/k8s/configmap.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: pastebin-config + namespace: default +data: + SMTP_USE_TLS: "false" + SMTP_USE_SSL: "true" + SMTP_SERVER: "priet.us" + SMTP_USERNAME: "me@priet.us" + SMTP_PORT: "465" + SMTP_PASSWORD: "wasamasa123" + JWT_EXP_DELTA_SECONDS: "86400" + VALID_USER: "admin" + VALID_PASS: "wasamasa123" diff --git a/k8s/deployent.yaml b/k8s/deployent.yaml new file mode 100644 index 0000000..8a1ee04 --- /dev/null +++ b/k8s/deployent.yaml @@ -0,0 +1,66 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pastebin-app +spec: + replicas: 1 + selector: + matchLabels: + app: pastebin + template: + metadata: + labels: + app: pastebin + spec: + # Solo mantengo un initContainer para "uploads", + # asumiendo que no necesitas fix-permissions para /app/database + initContainers: + - name: fix-permissions-uploads + image: busybox + command: ["sh", "-c", "chown -R 1000:1000 /app/uploads"] + volumeMounts: + - name: uploads + mountPath: /app/uploads + + containers: + - name: pastebin-container +image: prietus/pastebin-app:1.2 + ports: + - containerPort: 5000 + securityContext: + runAsUser: 0 + runAsGroup: 0 + env: + # Variables para que tu app apunte a PostgreSQL + - name: DB_ENGINE + value: "postgres" + - name: POSTGRES_HOST + value: "db-service" # <--- nombre del servicio de PostgreSQL en el cluster + - name: POSTGRES_PORT + value: "5432" + - name: POSTGRES_USER + value: "postgres" # <--- ajusta según tu config + - name: POSTGRES_PASSWORD + value: "password" # <--- ajusta según tu config + - name: POSTGRES_DB + value: "mypastedb" # <--- ajusta según tu config + + # Usuario/contraseña por defecto de la app + - name: VALID_USER + value: "admin" + - name: VALID_PASS + value: "password" + + # Configuración de entorno Flask + - name: FLASK_ENV + value: "production" + + volumeMounts: + - name: uploads + mountPath: /app/uploads + + volumes: + - name: uploads + persistentVolumeClaim: + claimName: uploads-pvc + diff --git a/k8s/pastebin-deployment.yaml b/k8s/pastebin-deployment.yaml new file mode 100644 index 0000000..89e5d10 --- /dev/null +++ b/k8s/pastebin-deployment.yaml @@ -0,0 +1,97 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pastebin-app + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: pastebin + template: + metadata: + labels: + app: pastebin + spec: + initContainers: + - name: fix-permissions + image: busybox + command: ["sh", "-c", "chown -R 1000:1000 /app/uploads"] + volumeMounts: + - name: uploads + mountPath: /app/uploads + containers: + - name: pastebin-container + image: prietus/pastebin-app:1.65 + ports: + - containerPort: 5000 + env: + - name: DB_ENGINE + value: "postgres" + - name: POSTGRES_HOST + value: "pastebin-postgres" + - name: VALID_USER + valueFrom: + configMapKeyRef: + name: pastebin-config + key: VALID_USER + - name: VALID_PASS + valueFrom: + configMapKeyRef: + name: pastebin-config + key: VALID_PASS + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: pastebin-secret + key: POSTGRES_PASSWORD + - name: DATABASE_URL + value: "postgresql://mypasteuser:$(POSTGRES_PASSWORD)@pastebin-postgres:5432/mypastedb?connect_timeout=10" + - name: SMTP_USE_TLS + valueFrom: + configMapKeyRef: + name: pastebin-config + key: SMTP_USE_TLS + - name: SMTP_USE_SSL + valueFrom: + configMapKeyRef: + name: pastebin-config + key: SMTP_USE_SSL + - name: SMTP_SERVER + value: "priet.us" + - name: SMTP_PORT + valueFrom: + configMapKeyRef: + name: pastebin-config + key: SMTP_PORT + - name: SMTP_USERNAME + valueFrom: + secretKeyRef: + name: pastebin-secret + key: SMTP_USERNAME + - name: SMTP_PASSWORD + valueFrom: + secretKeyRef: + name: pastebin-secret + key: SMTP_PASSWORD + volumeMounts: + - name: uploads + mountPath: /app/uploads + volumes: + - name: uploads + persistentVolumeClaim: + claimName: uploads-pvc +--- +apiVersion: v1 +kind: Service +metadata: + name: pastebin-service + namespace: default +spec: + ports: + - port: 80 + targetPort: 5000 + selector: + app: pastebin + type: ClusterIP + diff --git a/k8s/pastebin-ingress.yaml b/k8s/pastebin-ingress.yaml new file mode 100644 index 0000000..46b405a --- /dev/null +++ b/k8s/pastebin-ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: pastebin-ingress + namespace: default + annotations: + kubernetes.io/ingress.class: traefik + cert-manager.io/cluster-issuer: letsencrypt-prod-dns +spec: + rules: + - host: paste.priet.us + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: pastebin-service # Nombre del Service asociado a tu aplicación + port: + number: 80 + tls: + - hosts: + - paste.priet.us + secretName: pastebin-tls + diff --git a/k8s/pastebin-postgres-deployment.yaml b/k8s/pastebin-postgres-deployment.yaml new file mode 100644 index 0000000..7876d03 --- /dev/null +++ b/k8s/pastebin-postgres-deployment.yaml @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pastebin-postgres + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: pastebin-postgres + template: + metadata: + labels: + app: pastebin-postgres + spec: + containers: + - name: postgres + image: postgres:15 + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + value: "mypasteuser" + - name: POSTGRES_PASSWORD + value: "wasamasa123" + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + subPath: pgdata + volumes: + - name: postgres-data + persistentVolumeClaim: + claimName: pastebin-postgres-pvc +--- +apiVersion: v1 +kind: Service +metadata: + name: pastebin-postgres + namespace: default +spec: + ports: + - port: 5432 + targetPort: 5432 + selector: + app: pastebin-postgres + diff --git a/k8s/pastebin-postgres-pvc.yaml b/k8s/pastebin-postgres-pvc.yaml new file mode 100644 index 0000000..0823b2d --- /dev/null +++ b/k8s/pastebin-postgres-pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: pastebin-postgres-pvc + namespace: default +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + diff --git a/k8s/pastebin-secret.yaml b/k8s/pastebin-secret.yaml new file mode 100644 index 0000000..2cd8f80 --- /dev/null +++ b/k8s/pastebin-secret.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: pastebin-secret + namespace: default +type: Opaque +data: + POSTGRES_PASSWORD: d2FzYW1hc2ExMjM= + SMTP_USERNAME: bWVAcHJpZXQudXM= + SMTP_PASSWORD: d2FzYW1hc2ExMjM= + SECRET_KEY: d2FzYW1hc2ExMjM= + diff --git a/k8s/pastebin-service.yaml b/k8s/pastebin-service.yaml new file mode 100644 index 0000000..ce5198b --- /dev/null +++ b/k8s/pastebin-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: pastebin-service +spec: + selector: + app: pastebin + ports: + - protocol: TCP + port: 80 + targetPort: 5000 + type: ClusterIP # Usa ClusterIP o NodePort según tus necesidades + diff --git a/k8s/pastebin-uploads-pvc.yaml b/k8s/pastebin-uploads-pvc.yaml new file mode 100644 index 0000000..460b4a5 --- /dev/null +++ b/k8s/pastebin-uploads-pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: uploads-pvc + namespace: default +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + diff --git a/k8s/pv.yaml b/k8s/pv.yaml new file mode 100644 index 0000000..185fc92 --- /dev/null +++ b/k8s/pv.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: uploads-pv +spec: + capacity: + storage: 50Gi + accessModes: + - ReadWriteOnce + hostPath: # Cambia a un proveedor como AWS, GCP, etc. + path: "/mnt/data/uploads" + diff --git a/k8s/pvc.yaml b/k8s/pvc.yaml new file mode 100644 index 0000000..2a7b46f --- /dev/null +++ b/k8s/pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: uploads-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 50Gi + diff --git a/k8s/sqlite/pastebin-configmap.yaml b/k8s/sqlite/pastebin-configmap.yaml new file mode 100644 index 0000000..eb6b610 --- /dev/null +++ b/k8s/sqlite/pastebin-configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: pastebin-config + namespace: default +data: + JWT_EXP_DELTA_SECONDS: "360000" + SMTP_SERVER: "212.24.103.64" + SMTP_PORT: "465" + SMTP_USE_TLS: "false" + SMTP_USE_SSL: "true" diff --git a/k8s/sqlite/pastebin-deployment.yaml b/k8s/sqlite/pastebin-deployment.yaml new file mode 100644 index 0000000..2805d13 --- /dev/null +++ b/k8s/sqlite/pastebin-deployment.yaml @@ -0,0 +1,98 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pastebin-app +spec: + replicas: 1 + selector: + matchLabels: + app: pastebin + template: + metadata: + labels: + app: pastebin + spec: + # InitContainers para ajustar permisos de ambos volúmenes + initContainers: + - name: fix-permissions-database + image: busybox + command: ["sh", "-c", "chown -R 1000:1000 /app/database"] + volumeMounts: + - name: database + mountPath: /app/database + - name: fix-permissions-uploads + image: busybox + command: ["sh", "-c", "chown -R 1000:1000 /app/uploads"] + volumeMounts: + - name: uploads + mountPath: /app/uploads + # Contenedor principal + containers: + - name: pastebin-container + image: prietus/pastebin-app:1.4.1.1 + ports: + - containerPort: 5000 + securityContext: + runAsUser: 0 + runAsGroup: 0 + env: + # Variables de entorno para configuración SMTP + - name: SMTP_USE_TLS + valueFrom: + configMapKeyRef: + name: pastebin-config + key: SMTP_USE_TLS + - name: SMTP_USE_SSL + valueFrom: + configMapKeyRef: + name: pastebin-config + key: SMTP_USE_SSL + - name: SMTP_SERVER + valueFrom: + configMapKeyRef: + name: pastebin-config + key: SMTP_SERVER + - name: SMTP_PORT + valueFrom: + configMapKeyRef: + name: pastebin-config + key: SMTP_PORT + - name: SMTP_USERNAME + valueFrom: + secretKeyRef: + name: pastebin-secret + key: SMTP_USERNAME + - name: SMTP_PASSWORD + valueFrom: + secretKeyRef: + name: pastebin-secret + key: SMTP_PASSWORD + # Variables adicionales + - name: VALID_USER + value: "admin" + - name: VALID_PASS + value: "password" + - name: SECRET_KEY + valueFrom: + secretKeyRef: + name: pastebin-secret + key: SECRET_KEY + - name: JWT_EXP_DELTA_SECONDS + valueFrom: + configMapKeyRef: + name: pastebin-config + key: JWT_EXP_DELTA_SECONDS + volumeMounts: + - name: database + mountPath: /app/database + - name: uploads + mountPath: /app/uploads + # Declaración de volúmenes + volumes: + - name: database + persistentVolumeClaim: + claimName: database-pvc + - name: uploads + persistentVolumeClaim: + claimName: uploads-pvc + diff --git a/k8s/sqlite/pastebin-deployment.yaml_backup b/k8s/sqlite/pastebin-deployment.yaml_backup new file mode 100644 index 0000000..3dc233b --- /dev/null +++ b/k8s/sqlite/pastebin-deployment.yaml_backup @@ -0,0 +1,88 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pastebin-app +spec: + replicas: 1 + selector: + matchLabels: + app: pastebin + template: + metadata: + labels: + app: pastebin + spec: + # InitContainers para ajustar permisos de ambos volúmenes + initContainers: + - name: fix-permissions-database + image: busybox + command: ["sh", "-c", "chown -R 1000:1000 /app/database"] + volumeMounts: + - name: database + mountPath: /app/database + - name: fix-permissions-uploads + image: busybox + command: ["sh", "-c", "chown -R 1000:1000 /app/uploads"] + volumeMounts: + - name: uploads + mountPath: /app/uploads + # Contenedor principal + containers: + - name: pastebin-container + image: prietus/pastebin-app:1.4 + ports: + - containerPort: 5000 + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + env: + # Variables de entorno para configuración SMTP + - name: SMTP_SERVER + valueFrom: + configMapKeyRef: + name: pastebin-config + key: SMTP_SERVER + - name: SMTP_PORT + valueFrom: + configMapKeyRef: + name: pastebin-config + key: SMTP_PORT + - name: SMTP_USERNAME + valueFrom: + secretKeyRef: + name: pastebin-secret + key: SMTP_USERNAME + - name: SMTP_PASSWORD + valueFrom: + secretKeyRef: + name: pastebin-secret + key: SMTP_PASSWORD + # Variables adicionales + - name: VALID_USER + value: "admin" + - name: VALID_PASS + value: "wasamasa123" + - name: SECRET_KEY + valueFrom: + secretKeyRef: + name: pastebin-secret + key: SECRET_KEY + - name: JWT_EXP_DELTA_SECONDS + valueFrom: + configMapKeyRef: + name: pastebin-config + key: JWT_EXP_DELTA_SECONDS + volumeMounts: + - name: database + mountPath: /app/database + - name: uploads + mountPath: /app/uploads + # Declaración de volúmenes + volumes: + - name: database + persistentVolumeClaim: + claimName: database-pvc + - name: uploads + persistentVolumeClaim: + claimName: uploads-pvc + diff --git a/k8s/sqlite/pastebin-ingress.yaml b/k8s/sqlite/pastebin-ingress.yaml new file mode 100644 index 0000000..46b405a --- /dev/null +++ b/k8s/sqlite/pastebin-ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: pastebin-ingress + namespace: default + annotations: + kubernetes.io/ingress.class: traefik + cert-manager.io/cluster-issuer: letsencrypt-prod-dns +spec: + rules: + - host: paste.priet.us + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: pastebin-service # Nombre del Service asociado a tu aplicación + port: + number: 80 + tls: + - hosts: + - paste.priet.us + secretName: pastebin-tls + diff --git a/k8s/sqlite/pastebin-secret.yaml b/k8s/sqlite/pastebin-secret.yaml new file mode 100644 index 0000000..01b0e2a --- /dev/null +++ b/k8s/sqlite/pastebin-secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: pastebin-secret + namespace: default +type: Opaque +data: + SECRET_KEY: d2FzYW1sdfsdfhc2ExMjM= # Este valor debe ser codificado en base64 + SMTP_USERNAME: bWVAcHJsdfsdfpZXQudXM= # Base64 de tu username + SMTP_PASSWORD: d2FzYW1hc2sdfsdfExMjM= diff --git a/k8s/sqlite/pastebin-service.yaml b/k8s/sqlite/pastebin-service.yaml new file mode 100644 index 0000000..ce5198b --- /dev/null +++ b/k8s/sqlite/pastebin-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: pastebin-service +spec: + selector: + app: pastebin + ports: + - protocol: TCP + port: 80 + targetPort: 5000 + type: ClusterIP # Usa ClusterIP o NodePort según tus necesidades + diff --git a/k8s/sqlite/pv.yaml b/k8s/sqlite/pv.yaml new file mode 100644 index 0000000..185fc92 --- /dev/null +++ b/k8s/sqlite/pv.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: uploads-pv +spec: + capacity: + storage: 50Gi + accessModes: + - ReadWriteOnce + hostPath: # Cambia a un proveedor como AWS, GCP, etc. + path: "/mnt/data/uploads" + diff --git a/k8s/sqlite/pvc.yaml b/k8s/sqlite/pvc.yaml new file mode 100644 index 0000000..e9e0024 --- /dev/null +++ b/k8s/sqlite/pvc.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: uploads-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 50Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: database-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + diff --git a/pastebin_client.sh b/pastebin_client.sh new file mode 100755 index 0000000..bee615b --- /dev/null +++ b/pastebin_client.sh @@ -0,0 +1,489 @@ +#!/bin/bash + +API_URL="http://localhost:5000" +TOKEN_FILE="$HOME/.pastebin_token" + +load_token() { + [[ -f "$TOKEN_FILE" ]] && TOKEN=$(cat "$TOKEN_FILE") || TOKEN="" +} + +save_token() { + echo "$TOKEN" > "$TOKEN_FILE" +} + +authenticate() { + local username="$1" password="$2" + response=$(curl -s -X POST "$API_URL/api/token" \ + -H "Content-Type: application/json" \ + -d '{"username": "'$username'", "password": "'$password'"}') + if echo "$response" | grep -q 'token'; then + TOKEN=$(echo "$response" | jq -r '.token') + save_token + echo "Authentication successful. Token saved." + else + echo "Authentication failed: $(echo "$response" | jq -r '.error')" + exit 1 + fi +} + +list_users() { + load_token + if [[ -z "$TOKEN" ]]; then + echo "Error: No token found. Please authenticate first." + return 1 + fi + + response=$(curl -s -H "Authorization: Bearer $TOKEN" "$API_URL/api/users") + + error_msg=$(echo "$response" | jq -r '.error // empty') + if [[ -n "$error_msg" ]]; then + echo "Error: $error_msg" + return 1 + fi + + usernames=$(echo "$response" | jq -r '.users[]') + echo "$usernames" +} + +remove_gps_metadata() { + local paste_id="$1" + load_token + + if [[ -z "$TOKEN" ]]; then + echo "Error: No token found. Please authenticate first." + return 1 + fi + + if [[ -z "$paste_id" ]]; then + echo "Usage: $0 remove_gps " + return 1 + fi + + response=$(curl -s -X POST "$API_URL/api/removegps" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"paste_id\": $paste_id}") + + success=$(echo "$response" | jq -r '.success // empty') + + if [[ "$success" == "true" ]]; then + echo "✅ GPS metadata successfully removed from paste ID $paste_id" + else + error_msg=$(echo "$response" | jq -r '.error // empty') + echo "❌ Error: $error_msg" + fi +} + +edit_paste() { + local paste_id="$1" + load_token + + # Chequear token + if [[ -z "$TOKEN" ]]; then + echo "Error: No token found. Please authenticate first." + return 1 + fi + + if [[ -z "$paste_id" ]]; then + echo "Usage: $0 edit " + return 1 + fi + + # 1. Descargar contenido JSON del paste + echo "Fetching current content of paste $paste_id..." + response=$(curl -s -H "Authorization: Bearer $TOKEN" \ + "$API_URL/paste/$paste_id/json") + + # Checar error + error_msg=$(echo "$response" | jq -r '.error // empty') + if [[ -n "$error_msg" ]]; then + echo "Error: $error_msg" + return 1 + fi + + # 2. Extraer el contenido, filename, etc. + current_content=$(echo "$response" | jq -r '.content // empty') + paste_filename=$(echo "$response" | jq -r '.filename // empty') + paste_language=$(echo "$response" | jq -r '.language // empty') + + # 3. Decidir la extensión local: + # a) tratar de inferirla de paste_filename + # b) si no hay, usar language + # c) fallback a "txt" + + # Extraer extensión del filename, si existe + extension="txt" + if [[ -n "$paste_filename" ]]; then + # e.g. "myscript.py" -> ".py" + ext_from_name="${paste_filename##*.}" # todo lo que viene después de la última. + if [[ "$ext_from_name" != "$paste_filename" ]]; then + extension="$ext_from_name" + fi + elif [[ -n "$paste_language" ]]; then + # Una pequeña tabla de mapeo básico + case "$paste_language" in + python) extension="py" ;; + javascript|js) extension="js" ;; + typescript|ts) extension="ts" ;; + java) extension="java" ;; + c) extension="c" ;; + cpp|c++) extension="cpp" ;; + bash|shell) extension="sh" ;; + html) extension="html" ;; + css) extension="css" ;; + json) extension="json" ;; + sql) extension="sql" ;; + *) extension="txt" ;; + esac + fi + + # 4. Crear archivo temporal con esa extensión + temp_file=$(mktemp "/tmp/paste_${paste_id}_XXXXXX.${extension}") + + # 5. Guardar el contenido en el archivo y abrir editor + echo "$current_content" > "$temp_file" + "${EDITOR:-nano}" "$temp_file" + + # 6. Leer el contenido editado + new_content=$(cat "$temp_file") + if [[ -z "$new_content" ]]; then + echo "No content provided. Aborting." + rm -f "$temp_file" + return 1 + fi + + # 7. Convertir a JSON con jq -Rs '.' + new_content_json=$(echo "$new_content" | jq -Rs '.') + + # 8. Enviar PUT /api/paste/ con el nuevo contenido + update_response=$(curl -s -X PUT "$API_URL/api/paste/$paste_id" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"content":'"$new_content_json"'}') + + # 9. Mostrar resultado + msg=$(echo "$update_response" | jq -r '.message // .error // empty') + if [[ -n "$msg" ]]; then + echo "$msg" + else + echo "$update_response" + fi + + # 10. Limpieza + rm -f "$temp_file" +} + + +unshare_paste() { + local paste_id="$1" username="$2" + load_token + if [[ -z "$TOKEN" ]]; then + echo "Error: No token found. Please authenticate first." + return 1 + fi + + if [[ -z "$paste_id" || -z "$username" ]]; then + echo "Error: Paste ID and username are required." + return 1 + fi + + response=$(curl -s -X POST "$API_URL/api/paste/$paste_id/unshare" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"username": "'$username'"}') + + if echo "$response" | grep -q '"message"'; then + echo "$response" | jq -r '.message' + else + echo "Error: $(echo "$response" | jq -r '.error')" + fi +} + +share_paste() { + local paste_id="$1" + local username="$2" + local can_edit_input="${3:-false}" # Por defecto, no se permite editar + + load_token + if [[ -z "$TOKEN" ]]; then + echo "Error: No token found. Please authenticate first." + return 1 + fi + + # Validación de argumentos + if [[ -z "$paste_id" || -z "$username" ]]; then + echo "Usage: share_paste [can_edit (true|false)]" + return 1 + fi + + # Validar que can_edit sea 'true' o 'false' + if [[ "$can_edit_input" != "true" && "$can_edit_input" != "false" ]]; then + echo "Error: can_edit must be 'true' or 'false'." + return 1 + fi + + # Convertir la entrada can_edit a booleano JSON + if [[ "$can_edit_input" == "true" ]]; then + can_edit_json=true + else + can_edit_json=false + fi + + # Construir el JSON de manera segura usando jq + json_data=$(jq -n --arg username "$username" --argjson can_edit "$can_edit_json" \ + '{username: $username, can_edit: $can_edit}') + + # Verificar que json_data es válido + if [[ -z "$json_data" ]]; then + echo "Error: Failed to construct JSON data." + return 1 + fi + + # Realizar la solicitud POST al endpoint y capturar la respuesta y el código HTTP + response=$(curl -s -w "\n%{http_code}" -X POST "$API_URL/api/paste/$paste_id/share" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "$json_data") + + # Separar el cuerpo de la respuesta y el código de estado + http_body=$(echo "$response" | sed '$d') + http_code=$(echo "$response" | tail -n1) + + # Manejar la respuesta según el código de estado + if [[ "$http_code" == "200" ]]; then + echo "$http_body" | jq + elif [[ "$http_code" == "400" || "$http_code" == "403" || "$http_code" == "404" || "$http_code" == "500" ]]; then + echo "$http_body" | jq -r '.error // .message' + else + echo "Unexpected HTTP code: $http_code" + echo "$http_body" + fi +} + +list_shared_with_others() { + load_token + if [[ -z "$TOKEN" ]]; then + echo "Error: No token found. Please authenticate first." + return 1 + fi + + response=$(curl -s -H "Authorization: Bearer $TOKEN" "$API_URL/api/shared_with_others") + echo "$response" | jq +} + +list_shared_with_me() { + load_token + if [[ -z "$TOKEN" ]]; then + echo "Error: No token found. Please authenticate first." + return 1 + fi + + response=$(curl -s -H "Authorization: Bearer $TOKEN" "$API_URL/api/shared_with_me") + echo "$response" | jq +} + + +list_favorites() { + load_token + if [[ -z "$TOKEN" ]]; then + echo "Error: No token found. Please authenticate first." + return 1 + fi + + response=$(curl -s -H "Authorization: Bearer $TOKEN" "$API_URL/api/favorites") + echo "$response" | jq +} + + +download() { + local paste_id="$1" + load_token + if [[ -z "$TOKEN" ]]; then + echo "Error: No token found. Please authenticate first." + return 1 + fi + + # Solicitar el archivo y guardar las cabeceras en un archivo temporal + response=$(curl -s -w "%{http_code}" -D headers.tmp -o "paste_${paste_id}.tmp" \ + -H "Authorization: Bearer $TOKEN" "$API_URL/api/paste/$paste_id/download") + + # Extraer el código HTTP del final de la respuesta + http_code=$(echo "$response" | tail -n1) + + if [[ "$http_code" -eq 200 ]]; then + # Extraer el nombre del archivo del encabezado "Content-Disposition" + filename=$(grep -i "Content-Disposition" headers.tmp | grep -o 'filename="[^"]*"' | sed 's/filename=//' | tr -d '"') + if [[ -z "$filename" ]]; then + # Si no se encuentra el encabezado, usar un nombre genérico + file_extension=$(file --mime-type -b "paste_${paste_id}.tmp" | awk -F'/' '{print $2}') + filename="paste_${paste_id}.${file_extension}" + fi + mv "paste_${paste_id}.tmp" "$filename" + echo "Paste $paste_id downloaded to $filename" + elif [[ "$http_code" -eq 403 ]]; then + echo "Error: You do not have permission to download paste $paste_id." + rm -f "paste_${paste_id}.tmp" + elif [[ "$http_code" -eq 404 ]]; then + echo "Error: Paste $paste_id not found." + rm -f "paste_${paste_id}.tmp" + else + echo "Failed to download paste $paste_id (HTTP code: $http_code)" + rm -f "paste_${paste_id}.tmp" + fi + + # Limpieza + rm -f headers.tmp +} + + +download_favorites() { + load_token + if [[ -z "$TOKEN" ]]; then + echo "Error: No token found. Please authenticate first." + return 1 + fi + response=$(curl -s -H "Authorization: Bearer $TOKEN" "$API_URL/api/favorites") + echo "$response" | jq -r '.[] | @base64' | while read paste; do + paste_id=$(echo "$paste" | base64 --decode | jq -r '.id') + download "$paste_id" + done +} + +create_paste() { +# local lang="$1" + local expire="${1:-yes}" + local private="${2:-no}" # ✅ Nuevo parámetro opcional, por defecto "no" + + echo "Enter the content for the paste (Ctrl+D to finish):" + content=$(cat) + + [[ -z "$content" ]] && { echo "Error: Paste content cannot be empty."; exit 1; } + + response=$(echo "$content" | curl -s -X POST "$API_URL/paste" \ + -H "Authorization: Bearer $TOKEN" \ + -F "c=@-" \ + -F "expire=$expire" \ + $( [[ "$private" == "yes" ]] && echo "-F private=true" ) ) # ✅ Se envía "private=true" solo si es necesario + + echo "$response" | jq -r '.url // .error' +} + +upload_file() { + local file="$1" + local expire="${2:-yes}" + local private="${3:-no}" + + [[ ! -f "$file" ]] && { echo "Error: File not found."; exit 1; } + + + response=$(curl -s -X POST "$API_URL/paste" \ + -H "Authorization: Bearer $TOKEN" \ + -F "c=@$file" \ + -F "expire=$expire" \ + $( [[ "$private" == "yes" ]] && echo "-F private=true" ) ) # ✅ Se envía "private=true" solo si es necesario + + echo "$response" | jq -r '.url // .error' +} + + +view_paste() { + local paste_id="$1" + response=$(curl -s "$API_URL/paste/$paste_id/json" \ + -H "Authorization: Bearer $TOKEN") + echo "$response" | jq -r '.content // .error' +} + +list_pastes() { + response=$(curl -s "$API_URL/pastes" -H "Authorization: Bearer $TOKEN") + echo "$response" | jq +} + +delete_paste() { + local paste_id="$1" + response=$(curl -s -X DELETE "$API_URL/paste/$paste_id" \ + -H "Authorization: Bearer $TOKEN") + echo "$response" +} + +view_raw() { + local paste_id="$1" + response=$(curl -s "$API_URL/paste/$paste_id/raw" \ + -H "Authorization: Bearer $TOKEN") + echo "$response" +} + +register_user() { + local username="$1" password="$2" + response=$(curl -s -X POST "$API_URL/register" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"username": "'$username'", "password": "'$password'"}') + echo "$response" +} + +details() { + response=$(curl -s -X GET "$API_URL/user/details" \ + -H "Authorization: Bearer $TOKEN") + echo "$response" | jq +} + +search_pastes() { + local query="$1" + response=$(curl -s -X GET "$API_URL/pastes/search?q=$query" \ + -H "Authorization: Bearer $TOKEN") + echo "$response" | jq +} + +add_to_favorites() { + local paste_id="$1" + load_token + if [[ -z "$TOKEN" ]]; then + echo "Error: No token found. Please authenticate first." + return 1 + fi + response=$(curl -s -X POST "$API_URL/api/paste/$paste_id/favorite" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json") + echo "$response" +} + +remove_from_favorites() { + local paste_id="$1" + load_token + if [[ -z "$TOKEN" ]]; then + echo "Error: No token found. Please authenticate first." + return 1 + fi + response=$(curl -s -X POST "$API_URL/api/paste/$paste_id/unfavorite" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json") + echo "$response" +} + + +load_token + +case "$1" in + login) authenticate "$2" "$3" ;; + edit) edit_paste "$2" ;; + shared_with_others) list_shared_with_others ;; + shared_with_me) list_shared_with_me ;; +download) download "$2" ;; + list_favorites) list_favorites ;; + remove_gps) remove_gps_metadata "$2" ;; + favorite) add_to_favorites "$2" ;; + unfavorite) remove_from_favorites "$2" ;; + create) create_paste "$2" "$3" ;; + upload) upload_file "$2" "$3" "$4" ;; + view) view_paste "$2" ;; + share) share_paste "$2" "$3" "$4" ;; + unshare) unshare_paste "$2" "$3" ;; + list) list_pastes ;; + delete) delete_paste "$2" ;; + view_raw) view_raw "$2" ;; + register) register_user "$2" "$3" ;; + details) details ;; + search) search_pastes "$2" ;; + *) echo "Usage: $0 {login|create|upload|view|list|delete|view_raw|register|details|search|favorite|unfavorite|shared_with_others|shared_with_me|share|unshare|remove_gps}" ;; +esac + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..44e6e07 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,19 @@ +Flask==2.2.3 +Flask-SQLAlchemy>=3.0.0 +PyJWT==2.7.0 +Werkzeug==2.2.3 +SQLAlchemy<2.0 +gunicorn +psycopg2>=2.9.10 +python-magic +pymediainfo +flask-login +markdown +elasticsearch==8.9.0 +Flask-Migrate==4.0.4 +rapidfuzz +flask-apscheduler +Pillow +guesslang>=2.0.0 +pytesseract +pygments==2.15.1 diff --git a/src/__pycache__/auth.cpython-312.pyc b/src/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000..e90e5fd Binary files /dev/null and b/src/__pycache__/auth.cpython-312.pyc differ diff --git a/src/__pycache__/models.cpython-312.pyc b/src/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..b5f9712 Binary files /dev/null and b/src/__pycache__/models.cpython-312.pyc differ diff --git a/src/__pycache__/routes.cpython-312.pyc b/src/__pycache__/routes.cpython-312.pyc new file mode 100644 index 0000000..4e4d985 Binary files /dev/null and b/src/__pycache__/routes.cpython-312.pyc differ diff --git a/src/__pycache__/routes.d.er b/src/__pycache__/routes.d.er new file mode 100644 index 0000000..a67ee6a --- /dev/null +++ b/src/__pycache__/routes.d.er @@ -0,0 +1,183 @@ +##[pylyzer] failed /home/teraflops/git/app/src/routes.py 1739030723 106467 +.smtplib: Never + +.___v_desugar_1: Never +.MIMEText: Never +.os: Never + +.mimetypes: Never + +.___v_desugar_2 = pyimport "__init__" +.__init__ = pyimport "__init__" +.request: Never +.jsonify: (*args: Obj, **kwargs := Obj) -> Never +.send_from_directory: (directory: Obj, path: Obj, **kwargs := Obj) -> wrappers.Response +.abort: (code: Int or wrappers.response.Response, *args: Obj, **kwargs := Obj) -> Never +.render_template: (template_name_or_list: global::List!(_: Type, _: Nat), **context := Obj) -> Str +.redirect: (location: Str, code: Int := Int, _ := {wrappers.Response}) -> wrappers.response.Response +.url_for: (endpoint: Str, _anchor: NoneType or Str := NoneType or Str, _method: NoneType or Str := NoneType or Str, _scheme: NoneType or Str := NoneType or Str, _external: NoneType or Bool := NoneType or Bool, **values := Obj) -> Str +.flash: (message: Str, category: Str := Str) -> NoneType +.session: Never +.___v_desugar_3: Never +.db: Never +.Paste: Never +.User: Never +.shared_pastes: Never +.___v_desugar_4: Never +.generate_token: Never +.___v_desugar_5: Never +.UPLOAD_FOLDER: Never +.SMTP_SERVER: Never +.SMTP_PORT: Never +.SMTP_USERNAME: Never +.SMTP_PASSWORD: Never +.SMTP_USE_TLS: Never +.SMTP_USE_SSL: Never +.ROLE_STORAGE_LIMITS: Never +.___v_desugar_6 = pyimport "__init__" + +.highlight: (code: Obj, lexer: Obj, formatter: Never, outfile: Bool := Bool) -> Never +.___v_desugar_7 = pyimport "__init__" + +.guess_lexer: (_text: global::Bytes, **options := NoneType) -> Never +.get_lexer_by_name: (_alias: Never, **options := Obj) -> Never +.guess_lexer_for_filename: (_fn: Obj, _text: Obj, **options := Obj) -> Never +.___v_desugar_8 = pyimport "__init__" + +.HtmlFormatter: Never +.___v_desugar_9 = pyimport "util" +.util = pyimport "util" +.ClassNotFound: {pygments.util.ClassNotFound} +.___v_desugar_10 = pyimport "__init__" + +.func: Never +.magic = pyimport "__init__" + + +.___v_desugar_11 = pyimport "utils" +.utils = pyimport "utils" +.secure_filename: (filename: Str) -> Str +.logging: Never + +.___v_desugar_12: Never +.BytesIO: Never +.___v_desugar_13 = pyimport "__init__" + +.send_file: (path_or_file: Obj, mimetype: NoneType or Str := NoneType or Str, as_attachment: Bool := Bool, download_name: NoneType or Str := NoneType or Str, conditional: Bool := Bool, etag: Bool or Str := Bool or Str, last_modified: Obj := Obj, max_age: NoneType or Int := NoneType or Int) -> wrappers.Response +.Response: Never +.uuid: Never + +.___v_desugar_14: Never +.MediaInfo: Never +.secrets: Never + +.___v_desugar_15 = pyimport "exc" +.exc = pyimport "exc" +.SQLAlchemyError: {sqlalchemy.exc.SQLAlchemyError} +.___v_desugar_16 = pyimport "__init__" + +.get_all_styles: () -> NoneType +.___v_desugar_17: Never +.datetime: Never +.___v_desugar_18: Never +.login_required: Never +.___v_desugar_19: Never +.login_user: Never +.___v_desugar_20: Never +.logout_user: Never +.___v_desugar_21 = pyimport "security" +.security = pyimport "security" +.check_password_hash: (pwhash: Str, password: Str) -> Bool +.___v_desugar_22: Never +.jwt_required: Never +.___v_desugar_23: Never +.current_user: Never +.___v_desugar_24 = pyimport "__init__" + +.markdown: (text: Str, **kwargs := Obj) -> Str +.___v_desugar_25: Never +.defaultdict: Never +.jwt = pyimport "__init__" + + +.___v_desugar_26 = pyimport "__init__" + +app = pyimport "app" +.current_app: app.Flask +.___v_desugar_27: Never +.Elasticsearch: Never +.___v_desugar_28: Never +.datetime: Never +.___v_desugar_29: Never +.Favorite: Never +.___v_desugar_30: Never +.UPLOAD_FOLDER: Never +.process = pyimport "process" +.fuzz = pyimport "fuzz" + +.___v_desugar_31 = pyimport "__init__" + +.or_: Never +.json: Never + +.___v_desugar_32: Never +.datetime: Never +.timedelta: Never +.___v_desugar_33 = pyimport "__init__" + +.get_all_lexers: (plugins: Bool := Bool) -> NoneType +.___v_desugar_34 = pyimport "util" + +.ClassNotFound: {pygments.util.ClassNotFound} +.base64: Never + +.Image = pyimport "Image" + +.___v_desugar_35 = pyimport "ExifTags" +.ExifTags = pyimport "ExifTags" +.TAGS: Never +.GPSTAGS: Never +.es: Never +.get_pygments_language_mappings: Never +.register_error_handlers: (app: Obj) -> NoneType +.delete_expired_pastes: () -> NoneType +.calculate_storage_used: (user_id: Obj) -> Float +.get_shared_pastes: |Type_390527 <: Structural({.id = ?E}), E: Type|(user: Type_390527, paste_filters: Bool := Bool, page: Obj := Obj, per_page: Obj := Obj) -> Never +.delete_paste_from_index: |Type_474413: Type, T <: Structural({.id = ?474413 and ?474411}), Type_474411: Type|(paste: T) -> NoneType +.calculate_stats: |R <: Bool|(user_id: Obj := Obj, start_date: R := R, end_date: R := R) -> global::Dict!({{"counts_text"}: global::List!(Never, 0), {"total_media_pastes"}: Never, {"pastes"}: Never, {"counts_media"}: global::List!(Never, 0), {"counts_file"}: global::List!(Never, 0), {"counts_compressed"}: global::List!(Never, 0), {"total_file_pastes"}: Never, {"total_text_pastes"}: Never, {"total_pastes"}: Nat, {"total_compressed_pastes"}: Never, {"languages"}: global::List!(Never, _: Nat), {"total_size"}: global::Add(Never)}) +.index_paste: |Type_475054 <: Ref(Obj), Type_475063: Type, Type_390545 <: Structural({.title = ?475063; .owner_id = ?475067; .filename = ?475054 and ?R; .created_at = Never; .id = ?475062 and ?475070}), Type_475062: Type, Type_475070: Type, Type_475067: Type, R: Type|(paste: Type_390545) -> NoneType +.get_current_user: () -> Never + +.highlight_code: (content: global::Bytes, language: Never := Never, filename: Bool := Bool) -> Never +.MEDIA_MIME_TYPES: {Type_v_global_251113: global::Tuple([Str, Str, Str, Str]) | Type_v_global_251113 == ("image.", "video.", "audio.", "application.pdf")} +.TEXT_BASED_APPLICATION_MIME_TYPES: {Type_v_global_251115: global::Tuple([Str, Str, Str, Str, Str, Str]) | Type_v_global_251115 == ("(...)", "(...)", "application.xml", "(...)", "application.sql", "text.xml")} +.LANGUAGE_TO_EXTENSION: Never +.EXTENSION_TO_LANGUAGE: Never +.FILENAME_TO_LANGUAGE: Never +.mime: magic.Magic +.os: Never + +.logging: Never + +.___v_desugar_36 = pyimport "__init__" + +.guess_lexer: (_text: global::Bytes, **options := NoneType) -> Never +.___v_desugar_37 = pyimport "util" + +.ClassNotFound: Never +.COMPRESSED_MIME_TYPES: Never +.COMPRESSED_EXTENSIONS: Never +.os: Never + +.logging: Never + +.___v_desugar_38 = pyimport "__init__" + +.guess_lexer: Never +.___v_desugar_39 = pyimport "util" + +.ClassNotFound: Never +.COMPRESSED_MIME_TYPES: Never +.COMPRESSED_EXTENSIONS: Never +.detect_language: (content: Never, unique_filename: Bool := Bool, original_filename: Bool := Bool, detected_mime_type: Structural({.__and__ = (self: Never, Obj) -> Bool}) := {None} and Structural({.__and__ = (self: Never, Obj) -> Bool})) -> {"unknown"} +.init_routes: (app: Obj) -> NoneType diff --git a/src/auth.py b/src/auth.py new file mode 100644 index 0000000..20a644e --- /dev/null +++ b/src/auth.py @@ -0,0 +1,51 @@ +import jwt +import datetime +from flask import request, jsonify +from functools import wraps +from config import SECRET_KEY, JWT_ALGORITHM, JWT_EXP_DELTA_SECONDS +from src.models import User + +# Generar token JWT +def generate_token(username): + payload = { + "username": username, + "exp": datetime.datetime.utcnow() + datetime.timedelta(seconds=JWT_EXP_DELTA_SECONDS) + } + return jwt.encode(payload, SECRET_KEY, algorithm=JWT_ALGORITHM) + +# Decodificar token JWT +def decode_token(token): + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[JWT_ALGORITHM]) + return payload + except jwt.ExpiredSignatureError: + return None + except jwt.InvalidTokenError: + return None + +# Decorador para rutas protegidas con JWT +def jwt_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + auth_header = request.headers.get('Authorization') + if not auth_header: + return jsonify({"error": "Authorization header missing"}), 401 + + # Validar que el formato sea correcto + parts = auth_header.split(" ") + if len(parts) != 2 or parts[0] != "Bearer": + return jsonify({"error": "Invalid Authorization header format"}), 401 + + token = parts[1] + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[JWT_ALGORITHM]) + user = User.query.filter_by(username=payload['username']).first() + if not user: + return jsonify({"error": "User not found"}), 401 + request.user = user + except (jwt.ExpiredSignatureError, jwt.InvalidTokenError): + return jsonify({"error": "Invalid or expired token"}), 401 + + return f(*args, **kwargs) + return decorated_function + diff --git a/src/constants.py b/src/constants.py new file mode 100644 index 0000000..3cb998d --- /dev/null +++ b/src/constants.py @@ -0,0 +1,26 @@ +LANGUAGE_TO_EXTENSION = { + "python": "py", + "javascript": "js", + "java": "java", + "csharp": "cs", + "cpp": "cpp", + "ruby": "rb", + "go": "go", + "html": "html", + "css": "css", + "php": "php", + "swift": "swift", + "kotlin": "kt", + "rust": "rs", + "typescript": "ts", + "bash": "sh", + "plaintext": "txt", + "json": "json", + "yaml": "yml", + "toml": "toml", + "markdown": "md", + "sql": "sql", + "xml": "xml", + "lua": "lua" +} + diff --git a/src/models.py b/src/models.py new file mode 100644 index 0000000..f88823a --- /dev/null +++ b/src/models.py @@ -0,0 +1,208 @@ +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime +from werkzeug.security import generate_password_hash, check_password_hash +import os +from config import UPLOAD_FOLDER +from flask_login import UserMixin + +db = SQLAlchemy() + +class Favorite(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + paste_id = db.Column(db.Integer, db.ForeignKey('paste.id'), nullable=False) + created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + + user = db.relationship('User', backref=db.backref('favorites', lazy=True)) + paste = db.relationship('Paste', backref=db.backref('favorited_by', lazy=True)) + + paste_favorites = db.Table( + 'paste_favorites', + db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True), + db.Column('paste_id', db.Integer, db.ForeignKey('paste.id'), primary_key=True) + ) +class User(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + password_hash = db.Column(db.String(128), nullable=False) + role = db.Column(db.String(10), nullable=False, default='user') + storage_used = db.Column(db.BigInteger, nullable=False, default=0) # En bytes + storage_limit = db.Column(db.BigInteger, nullable=False, default=1 * 1024**3) + theme_preference = db.Column(db.String(10), default="light") # Guardar "light" o "dark" + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + + def has_unlimited_storage(self): + return self.storage_limit == -1 + + def get_storage_limit(self): + """Devuelve el límite de almacenamiento del usuario.""" + return self.storage_limit + +shared_pastes = db.Table( + 'shared_pastes', + db.Column('paste_id', db.Integer, db.ForeignKey('paste.id'), primary_key=True), + db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True), + db.Column('can_edit', db.Boolean, default=False) # Nuevo campo para indicar permiso de edición +) + +# Tabla intermedia para usuarios con permisos de edición +paste_editors = db.Table( + 'paste_editors', + db.Column('paste_id', db.Integer, db.ForeignKey('paste.id'), primary_key=True), + db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True) +) + +class Paste(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(255), nullable=True) + editable = db.Column(db.Boolean, default=True) + content_type = db.Column(db.String(50), nullable=False) + filename = db.Column(db.String(255), nullable=True) + owner_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + expires_at = db.Column(db.DateTime, nullable=True) # Nuevo campo de expiración + language = db.Column(db.String(50), nullable=True) + last_edited_at = db.Column(db.DateTime, nullable=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + size = db.Column(db.Integer, nullable=True) + private = db.Column(db.Boolean, default=False) + + # Relaciones + editors = db.relationship('User', secondary=paste_editors, backref=db.backref('editable_pastes', lazy='dynamic')) + owner = db.relationship('User', backref=db.backref('owned_pastes', lazy=True), foreign_keys=[owner_id]) + favorites = db.relationship('User', secondary='paste_favorites', backref=db.backref('favorite_pastes', lazy='dynamic')) + shared_with = db.relationship('User', secondary=shared_pastes, backref=db.backref('shared_pastes', lazy='dynamic')) + + owner = db.relationship( + 'User', + backref=db.backref('owned_pastes', lazy=True), + foreign_keys=[owner_id] + ) + favorites = db.relationship( + 'User', + secondary='paste_favorites', + backref=db.backref('favorite_pastes', lazy='dynamic') + ) + def is_expired(self): + """ Verifica si el paste ha expirado. """ + return self.expires_at and datetime.utcnow() > self.expires_at + def to_dict(self): + return { + "id": self.id, + "content_type": self.content_type, + "filename": self.filename, + "owner_id": self.owner_id, + "created_at": self.created_at.isoformat(), + "language": self.language, + "user_id": self.user_id, + "size": self.file_size + } + + + def get_size(self): + file_path = os.path.join(UPLOAD_FOLDER, self.filename) + try: + return os.path.getsize(file_path) # Devuelve el tamaño del archivo + except FileNotFoundError: + return 0 + + def get_type(self): + # Determinar el tipo de archivo basado en content_type + if self.content_type.startswith("video"): + return "Video" + elif self.content_type.startswith("audio"): + return "Audio" + elif self.content_type.startswith("image"): + return "Image" + elif self.content_type in [ + "application/zip", "application/x-tar", "application/gzip", + "application/x-bzip2", "application/x-7z-compressed", "application/x-rar-compressed" + ]: + return "Compressed" + elif self.content_type == "application/pdf": + return "PDF" + elif self.content_type.startswith("text"): + if self.language: + return f"Text ({self.language})" + return "Text" + else: + return "Other" + + def get_extension(self): + from src.constants import LANGUAGE_TO_EXTENSION + + # 1. Si filename tiene extensión, úsala + if self.filename and '.' in self.filename: + ext = self.filename.rsplit('.', 1)[-1].lower() + if len(ext) <= 5: # filtra hashes falsos + return ext + + # 2. Si hay lenguaje conocido + if self.language: + ext = LANGUAGE_TO_EXTENSION.get(self.language.lower()) + if ext: + return ext + + # 3. Si se puede deducir por content_type + if self.content_type: + if self.content_type == "application/pdf": + return "pdf" + elif self.content_type in ["application/zip", "application/x-zip-compressed"]: + return "zip" + elif self.content_type.startswith("image/"): + return self.content_type.split("/")[-1].split('+')[0] + elif self.content_type.startswith("audio/"): + return self.content_type.split("/")[-1].split('+')[0] + elif self.content_type.startswith("video/"): + return self.content_type.split("/")[-1].split('+')[0] + elif self.content_type.startswith("text/"): + return "txt" + + return "bin" + + + + def has_edit_permission(self, user): + """ + Verifica si un usuario tiene permiso de edición en este paste. + """ + # Si el usuario no está autenticado, no puede tener permisos + if not user.is_authenticated: + print(f"[DEBUG] Unauthenticated user tried to edit paste {self.id}. Permission denied.") + return False + + # El dueño del paste siempre tiene permisos + if user.id == self.owner_id: + print(f"[DEBUG] User {user.id} is the owner of paste {self.id}. Permission granted.") + return True + + # Verificar si el usuario tiene permisos en `shared_pastes` + shared_entry = db.session.query(shared_pastes).filter_by( + paste_id=self.id, + user_id=user.id, + can_edit=True + ).first() + + if shared_entry: + print(f"[DEBUG] User {user.id} has edit permissions for paste {self.id}.") + else: + print(f"[DEBUG] User {user.id} does NOT have edit permissions for paste {self.id}.") + + return shared_entry is not None + + def can_view(self, user): + """ + Determina si un usuario puede ver el paste. + """ + if not self.private: + return True # Si no es privado, cualquiera puede verlo + + if user and user.is_authenticated: + if user.id == self.owner_id or user in self.shared_with: + return True # Si el usuario es el dueño o está en la lista de compartidos, puede verlo + + return False # No es dueño ni compartido, acceso denegado diff --git a/src/models.py_backup b/src/models.py_backup new file mode 100644 index 0000000..ded23a6 --- /dev/null +++ b/src/models.py_backup @@ -0,0 +1,156 @@ +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime +from werkzeug.security import generate_password_hash, check_password_hash +import os +from config import UPLOAD_FOLDER +from flask_login import UserMixin + +db = SQLAlchemy() + +class Favorite(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + paste_id = db.Column(db.Integer, db.ForeignKey('paste.id'), nullable=False) + created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + + user = db.relationship('User', backref=db.backref('favorites', lazy=True)) + paste = db.relationship('Paste', backref=db.backref('favorited_by', lazy=True)) + + paste_favorites = db.Table( + 'paste_favorites', + db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True), + db.Column('paste_id', db.Integer, db.ForeignKey('paste.id'), primary_key=True) + ) +class User(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + password_hash = db.Column(db.String(128), nullable=False) + role = db.Column(db.String(10), nullable=False, default='user') + storage_used = db.Column(db.BigInteger, nullable=False, default=0) # En bytes + storage_limit = db.Column(db.BigInteger, nullable=False, default=1 * 1024**3) + theme_preference = db.Column(db.String(10), default="light") # Guardar "light" o "dark" + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + + def has_unlimited_storage(self): + return self.storage_limit == -1 + + def get_storage_limit(self): + """Devuelve el límite de almacenamiento del usuario.""" + return self.storage_limit + +shared_pastes = db.Table( + 'shared_pastes', + db.Column('paste_id', db.Integer, db.ForeignKey('paste.id'), primary_key=True), + db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True), + db.Column('can_edit', db.Boolean, default=False) # Nuevo campo para indicar permiso de edición +) + +# Tabla intermedia para usuarios con permisos de edición +paste_editors = db.Table( + 'paste_editors', + db.Column('paste_id', db.Integer, db.ForeignKey('paste.id'), primary_key=True), + db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True) +) + +class Paste(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(255), nullable=True) + editable = db.Column(db.Boolean, default=True) + content_type = db.Column(db.String(50), nullable=False) + filename = db.Column(db.String(255), nullable=True) + owner_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + expires_at = db.Column(db.DateTime, nullable=True) # Nuevo campo de expiración + language = db.Column(db.String(50), nullable=True) + last_edited_at = db.Column(db.DateTime, nullable=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + size = db.Column(db.Integer, nullable=True) + + # Relaciones + editors = db.relationship('User', secondary=paste_editors, backref=db.backref('editable_pastes', lazy='dynamic')) + owner = db.relationship('User', backref=db.backref('owned_pastes', lazy=True), foreign_keys=[owner_id]) + favorites = db.relationship('User', secondary='paste_favorites', backref=db.backref('favorite_pastes', lazy='dynamic')) + shared_with = db.relationship('User', secondary=shared_pastes, backref=db.backref('shared_pastes', lazy='dynamic')) + + owner = db.relationship( + 'User', + backref=db.backref('owned_pastes', lazy=True), + foreign_keys=[owner_id] + ) + favorites = db.relationship( + 'User', + secondary='paste_favorites', + backref=db.backref('favorite_pastes', lazy='dynamic') + ) + def is_expired(self): + """ Verifica si el paste ha expirado. """ + return self.expires_at and datetime.utcnow() > self.expires_at + def to_dict(self): + return { + "id": self.id, + "content_type": self.content_type, + "filename": self.filename, + "owner_id": self.owner_id, + "created_at": self.created_at.isoformat(), + "language": self.language, + "user_id": self.user_id, + "size": self.file_size + } + + + def get_size(self): + file_path = os.path.join(UPLOAD_FOLDER, self.filename) + try: + return os.path.getsize(file_path) # Devuelve el tamaño del archivo + except FileNotFoundError: + return 0 + + def get_type(self): + # Determinar el tipo de archivo basado en content_type + if self.content_type.startswith("video"): + return "Video" + elif self.content_type.startswith("audio"): + return "Audio" + elif self.content_type.startswith("image"): + return "Image" + elif self.content_type in ["application/zip", "application/x-tar", "application/gzip", "application/x-bzip2", "application/x-7z-compressed", "application/x-rar-compressed"]: + return "Compressed" + elif self.content_type.startswith("text"): + if self.language: + return f"Text ({self.language})" + return "Text" + else: + return "Other" + + def has_edit_permission(self, user): + """ + Verifica si un usuario tiene permiso de edición en este paste. + """ + # Si el usuario no está autenticado, no puede tener permisos + if not user.is_authenticated: + print(f"[DEBUG] Unauthenticated user tried to edit paste {self.id}. Permission denied.") + return False + + # El dueño del paste siempre tiene permisos + if user.id == self.owner_id: + print(f"[DEBUG] User {user.id} is the owner of paste {self.id}. Permission granted.") + return True + + # Verificar si el usuario tiene permisos en `shared_pastes` + shared_entry = db.session.query(shared_pastes).filter_by( + paste_id=self.id, + user_id=user.id, + can_edit=True + ).first() + + if shared_entry: + print(f"[DEBUG] User {user.id} has edit permissions for paste {self.id}.") + else: + print(f"[DEBUG] User {user.id} does NOT have edit permissions for paste {self.id}.") + + return shared_entry is not None + diff --git a/src/routes.py b/src/routes.py new file mode 100644 index 0000000..342fe39 --- /dev/null +++ b/src/routes.py @@ -0,0 +1,3050 @@ +import os +import re +import smtplib +import logging +import json +import base64 +import mimetypes +import secrets +import uuid +import requests +from datetime import datetime, timedelta +from collections import defaultdict +import io +from io import BytesIO + +from flask import ( + request, jsonify, send_from_directory, abort, render_template, + redirect, url_for, flash, session, current_app, send_file, Response +) +from flask_login import ( + login_required, login_user, logout_user, current_user +) +from sqlalchemy import func, or_ +from sqlalchemy.exc import SQLAlchemyError + +from config import ( + UPLOAD_FOLDER, SMTP_SERVER, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, + SMTP_USE_TLS, SMTP_USE_SSL, ROLE_STORAGE_LIMITS +) +from elasticsearch import Elasticsearch +from pygments import highlight +from pygments.formatters import HtmlFormatter +from pygments.lexers import ( + guess_lexer, guess_lexer_for_filename, get_lexer_by_name, get_all_lexers +) +from pygments.styles import get_all_styles +from pygments.util import ClassNotFound +from markdown import markdown +from PIL import Image, ExifTags +from PIL.ExifTags import TAGS +from pymediainfo import MediaInfo +from rapidfuzz import process, fuzz +from src.auth import generate_token, jwt_required +from src.models import db, Paste, User, Favorite, shared_pastes +from werkzeug.security import check_password_hash +from werkzeug.utils import secure_filename +import magic +import jwt +from guesslang import Guess +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +import smtplib +import pytesseract +import subprocess +import tempfile +from flask import Flask, request, Response, stream_with_context, jsonify +from pygments.lexers import get_all_lexers + +#es = Elasticsearch(hosts=["http://elasticsearch:9200"]) +es = Elasticsearch(hosts=["http://elasticsearch:9200"], timeout=30, max_retries=10, retry_on_timeout=True) + + +# Configuración de DeepSeek-Coder +DEEPSEEK_API_URL = "https://api.deepseek.com/beta/v1/chat/completions" + + +DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") # Reemplaza con tu clave de API + +HEADERS = { + "Authorization": f"Bearer {DEEPSEEK_API_KEY}", + "Content-Type": "application/json" +} + + +def get_readable_languages(): + """ + Retorna una lista de lenguajes con sus nombres amigables (ej. 'Python', 'JavaScript') + usando el primer alias disponible como `value` y el nombre formal como `label`. + """ + languages = [] + for lexer in get_all_lexers(): + name, aliases, _, _ = lexer + if aliases: # Ignorar lexers sin alias + languages.append((aliases[0], name)) + # Ordenar alfabéticamente por nombre amigable + return sorted(languages, key=lambda x: x[1].lower()) + + +def stream_response(payload): + """ + Stream the response from DeepSeek-Chat to the frontend in real-time. + Extracts only the generated text from each chunk. + """ + try: + with requests.post(DEEPSEEK_API_URL, headers=HEADERS, json=payload, stream=True, timeout=120) as response: + response.raise_for_status() + + for line in response.iter_lines(): + if line: + decoded_line = line.decode("utf-8").strip() + + # Ignorar mensajes de keep-alive o fin de transmisión + if decoded_line == "data: [DONE]": + break + + if decoded_line.startswith("data:"): + try: + json_data = json.loads(decoded_line[5:].strip()) # Eliminar "data: " del inicio + if "choices" in json_data and len(json_data["choices"]) > 0: + chunk_text = json_data["choices"][0].get("delta", {}).get("content", "") + if chunk_text: + yield chunk_text # Enviar solo el código generado + except json.JSONDecodeError: + continue # Ignorar líneas malformadas + except requests.exceptions.RequestException as e: + yield f"Error connecting to DeepSeek: {str(e)}" + + +def summarize_text(content): + """ Envía el contenido del paste a la API de Hugging Face para obtener un resumen. """ + payload = { + "inputs": content[:1024], # Limitamos a 1024 caracteres + "parameters": { + "max_length": 200, # Definir max_length explícitamente + "min_length": 50, # Evitar resúmenes demasiado cortos + "do_sample": False # No generar texto aleatorio + } + } + + try: + response = requests.post(HUGGINGFACE_API_URL, headers=HUGGINGFACE_HEADERS, json=payload) + response.raise_for_status() + + result = response.json() + + logging.debug(f"Respuesta completa de Hugging Face: {result}") + + if "error" in result: + return f"⚠️ Error en la API: {result['error']}" + + if not isinstance(result, list) or "generated_text" not in result[0]: + return "⚠️ Respuesta inesperada de la API. Intenta más tarde." + + return result[0]["generated_text"] + + except requests.exceptions.HTTPError as e: + return f"❌ Error HTTP: {e.response.status_code} - {e.response.text}" + except requests.exceptions.RequestException as e: + return f"⚠️ Error inesperado: {str(e)}" + + +def extract_pdf_text(file_content): + with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_pdf: + tmp_pdf.write(file_content) + tmp_pdf.flush() + pdf_path = tmp_pdf.name + + cmd = ["pdftotext", pdf_path, "-"] # "-" => salida a stdout + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return result.stdout + except subprocess.CalledProcessError as e: + logging.error(f"❌ Error con pdftotext: {e}") + return "" + finally: + # Opcional: borrar el archivo temporal + import os + os.remove(pdf_path) + +def index_paste(paste): + try: + file_path = os.path.join("/app/uploads", paste.filename) + content = "" + + if paste.content_type == "application/pdf": + logging.info(f"📄 Intentando extraer texto del PDF: {paste.filename}") + with open(file_path, "rb") as f: + file_content = f.read() + content = extract_pdf_text(file_content) + if not content.strip(): + logging.warning(f"⚠️ No se extrajo texto del PDF {paste.filename}.") + + elif paste.content_type.startswith("image/"): + logging.info(f"🖼 Procesando imagen con OCR: {paste.filename}") + with open(file_path, "rb") as f: + img_data = f.read() + image = Image.open(io.BytesIO(img_data)) + try: + content = pytesseract.image_to_string(image) + logging.info(f"🖼 Texto extraído de la imagen (primeros 300 chars): {content[:300]}") + except Exception as e: + logging.error(f"❌ Error al procesar OCR: {e}") + content = "" + + else: + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + content = f.read() + + shared_with_ids = [str(user.id) for user in paste.shared_with] + + doc = { + "id": str(paste.id), + "title": paste.title or paste.filename, + "content": content, + "owner_id": str(paste.owner_id), + "created_at": paste.created_at.isoformat(), + "private": paste.private, + "shared_with": shared_with_ids + } + + es.index(index="pastes", id=paste.id, document=doc) + logging.info(f"✅ Paste {paste.id} indexado correctamente en Elasticsearch.") + + except Exception as e: + current_app.logger.error(f"Error indexando paste {paste.id}: {e}") + +def convert_to_degrees(value): + """ Convierte coordenadas GPS en formato EXIF a grados decimales """ + d, m, s = value + return float(d) + (float(m) / 60.0) + (float(s) / 3600.0) + +def get_image_metadata(file_path): + """ Extrae y formatea los metadatos EXIF de una imagen, incluyendo GPS. """ + try: + img = Image.open(file_path) + exif_data = img._getexif() # Obtener datos EXIF + if not exif_data: + return [{"Info": "No EXIF metadata found in image file"}] + + metadata = [] + gps_data = {} + + for tag, value in exif_data.items(): + decoded_tag = ExifTags.TAGS.get(tag, tag) # Decodificar etiqueta EXIF + + if decoded_tag == "GPSInfo": + # Extraer GPSInfo correctamente + gps_info_raw = exif_data.get(tag, {}) + if isinstance(gps_info_raw, dict): + for gps_tag, gps_value in gps_info_raw.items(): + gps_decoded = ExifTags.GPSTAGS.get(gps_tag, gps_tag) + gps_data[gps_decoded] = gps_value + + # Convertir coordenadas GPS a formato decimal + if "GPSLatitude" in gps_data and "GPSLongitude" in gps_data: + lat = convert_to_degrees(gps_data["GPSLatitude"]) + lon = convert_to_degrees(gps_data["GPSLongitude"]) + lat_ref = gps_data.get("GPSLatitudeRef", "N") + lon_ref = gps_data.get("GPSLongitudeRef", "E") + + if lat_ref != "N": + lat = -lat + if lon_ref != "E": + lon = -lon + + gps_data["Decimal Coordinates"] = f"{lat}, {lon}" + + else: + gps_data["Error"] = "GPSInfo format incorrect, could not decode" + + else: + metadata.append({decoded_tag: str(value)}) + + # Si hay datos GPS, los añadimos a los metadatos + if gps_data: + metadata.append({"GPS Data": gps_data}) + + return metadata if metadata else [{"Info": "No metadata found in image file"}] + + except Exception as e: + print(f"[ERROR] No se pudo leer EXIF de {file_path}: {e}") + return [{"Error": f"Failed to read EXIF: {str(e)}"}] + +def has_gps_data(file_path): + """Verifica si una imagen tiene datos GPS en los metadatos EXIF.""" + try: + img = Image.open(file_path) + exif_data = img.getexif() + + if not exif_data: + return False # No hay EXIF + + gps_info_tag = 34853 # Código del tag GPSInfo + if gps_info_tag in exif_data: + gps_info = exif_data[gps_info_tag] + return bool(gps_info) # Devuelve True si hay info GPS + + return False # No se encontraron datos GPS + except Exception as e: + print(f"[ERROR] No se pudo leer EXIF de {file_path}: {e}") + return False + +def get_pygments_language_mappings(): + """Genera un mapeo entre Pygments y Monaco Editor, incluyendo extensiones de archivo.""" + all_lexers = list(get_all_lexers()) + + language_mappings = {} + extension_mappings = {} + + for name, aliases, filenames, mimetypes in all_lexers: + if aliases: + main_alias = aliases[0] # Tomamos el primer alias como principal + language_mappings[main_alias] = main_alias # Asumimos que Monaco reconoce el mismo nombre + + # Extraer extensiones de archivo + for filename in filenames: + if filename.startswith("*."): + ext = filename[2:] # Removemos "*." + extension_mappings[main_alias] = ext + + return language_mappings, extension_mappings + +def register_error_handlers(app): + @app.errorhandler(404) + def not_found_error(error): + return render_template("errors/404.html"), 404 + + @app.errorhandler(500) + def internal_error(error): + return render_template("errors/500.html"), 500 + + @app.errorhandler(403) + def forbidden_error(error): + return render_template("errors/403.html"), 403 + +def delete_expired_pastes(): + """Elimina los pastes expirados de la base de datos y Elasticsearch.""" + now = datetime.utcnow() + + # Obtener pastes expirados + expired_pastes = Paste.query.filter(Paste.expires_at < now).all() + + if not expired_pastes: + print("[INFO] No expired pastes found.") + return + + for paste in expired_pastes: + try: + # Eliminar de Elasticsearch + es.delete(index="pastes", id=paste.id, ignore=[404]) + print(f"[INFO] Deleted paste {paste.id} from Elasticsearch.") + except Exception as e: + print(f"[ERROR] Failed to delete paste {paste.id} from Elasticsearch: {e}") + + # Eliminar de la base de datos + delete_paste_from_index(paste) + db.session.delete(paste) + + db.session.commit() + print(f"[INFO] Deleted {len(expired_pastes)} expired pastes from the database.") + + +def calculate_storage_used(user_id): + """ + Calcula el almacenamiento utilizado por el usuario. + + :param user_id: ID del usuario. + :return: Almacenamiento utilizado en MB. + """ + total = db.session.query(db.func.sum(Paste.size)).filter_by(user_id=user_id).scalar() + total = total if total else 0 + mb = total / (1024 ** 2) # Convertir bytes a MB + return mb + + + +def get_shared_pastes(user, paste_filters=None, page=1, per_page=10): + """ + Obtiene los pastes compartidos con el usuario especificado, aplicando filtros si es necesario. + + :param user: Objeto de usuario actual. + :param paste_filters: Lista de filtros adicionales para aplicar a la consulta. + :param page: Número de página para la paginación. + :param per_page: Número de elementos por página. + :return: Objeto de paginación con los pastes compartidos. + """ + query = Paste.query.join(shared_pastes).filter(shared_pastes.c.user_id == user.id) + + if paste_filters: + query = query.filter(*paste_filters) + + query = query.order_by(Paste.created_at.desc()) + + pagination = query.paginate(page=page, per_page=per_page, error_out=False) + + return pagination + + +def delete_paste_from_index(paste): + try: + es.delete(index='pastes', id=paste.id) + print(f"Deleted paste {paste.id} from index.") + except Exception as e: + print(f"Error deleting paste {paste.id} from index: {e}") + +def calculate_stats(user_id=None, start_date=None, end_date=None): + query = Paste.query + + if user_id is not None: + query = query.filter_by(user_id=user_id) + + if start_date: + if isinstance(start_date, str): + start_date = datetime.strptime(start_date, '%Y-%m-%d') + query = query.filter(Paste.created_at >= start_date) + + if end_date: + if isinstance(end_date, str): + end_date = datetime.strptime(end_date, '%Y-%m-%d').replace(hour=23, minute=59, second=59) + query = query.filter(Paste.created_at <= end_date) + + pastes = query.all() + + stats = { + "total_pastes": len(pastes), + "total_size": sum(paste.size for paste in pastes), + "total_text_pastes": query.filter(Paste.content_type.like('text/%')).count(), + "total_file_pastes": query.filter(Paste.content_type.notlike('text/%')).count(), + "total_media_pastes": query.filter( + or_( + Paste.content_type.like('image/%'), + Paste.content_type.like('video/%'), + Paste.content_type.like('audio/%') + ) + ).count(), + "total_compressed_pastes": query.filter( + Paste.content_type.in_([ + "application/zip", "application/x-tar", "application/gzip", + "application/x-bzip2", "application/x-7z-compressed", "application/x-rar-compressed" + ]) + ).count(), + "languages": list(set(paste.language for paste in pastes if paste.language)), + "counts_text": [], + "counts_file": [], + "counts_media": [], + "counts_compressed": [], + "pastes": pastes # Lista de pastes + } + + # Agrupar estadísticas por lenguaje y tipo + language_groups = query.with_entities(Paste.language, Paste.content_type).all() + language_counts = {} + for lang, ctype in language_groups: + if lang not in language_counts: + language_counts[lang] = {"text": 0, "file": 0, "media": 0} + if ctype.startswith("text/"): + language_counts[lang]["text"] += 1 + elif ctype.startswith(("image/", "video/", "audio/")): + language_counts[lang]["media"] += 1 + else: + language_counts[lang]["file"] += 1 + + stats["counts_text"] = [language_counts[lang]["text"] for lang in stats["languages"]] + stats["counts_file"] = [language_counts[lang]["file"] for lang in stats["languages"]] + stats["counts_media"] = [language_counts[lang]["media"] for lang in stats["languages"]] + + return stats + +def get_current_user(): + user_id = session.get('user_id') + if not user_id: + return None + # Supongamos que tienes un modelo User para buscar el usuario + return User.query.get(user_id) + +logging.basicConfig( + level=logging.DEBUG, # Cambiar a INFO si no quieres mensajes de depuración + format="%(asctime)s - %(levelname)s - %(message)s", + handlers=[ + logging.StreamHandler() # Envía los logs a la consola + ] +) + +# Define la función highlight_code +def highlight_code(content, language=None, filename=None): + if language: + try: + lexer = get_lexer_by_name(language) + except: + lexer = guess_lexer(content) + else: + if filename: + lexer = guess_lexer_for_filename(filename, content) + else: + lexer = guess_lexer(content) + + formatter = HtmlFormatter(linenos=True, cssclass="highlight") + html_code = highlight(content, lexer, formatter) + + return html_code + +MEDIA_MIME_TYPES = ( + 'image/', + 'video/', + 'audio/', + 'application/pdf', +) + +# Tipos MIME basados en texto que no comienzan con 'text/' +TEXT_BASED_APPLICATION_MIME_TYPES = ( + 'application/json', + 'application/javascript', + 'application/xml', + 'application/xhtml+xml', + 'application/sql', + 'text/xml', + # Agrega otros tipos basados en texto según tus necesidades +) + +LANGUAGE_TO_EXTENSION = { + "python": "py", + "javascript": "js", + "java": "java", + "csharp": "cs", + "cpp": "cpp", + "ruby": "rb", + "go": "go", + "html": "html", + "css": "css", + "php": "php", + "swift": "swift", + "kotlin": "kt", + "rust": "rs", + "typescript": "ts", + "bash": "sh", + "plaintext": "txt", + "sql": "sql", + "json": "json", + "yaml": "yaml", + "xml": "xml" +} + +# Mapeo personalizado de extensiones a lenguajes +EXTENSION_TO_LANGUAGE = { + ".sh": "bash", + ".bash": "bash", + ".py": "python", + ".json": "json", + ".js": "javascript", + ".ts": "typescript", + ".sql": "sql", + ".html": "html", + ".css": "css", + ".java": "java", + ".c": "c", + ".cpp": "cpp", + ".rb": "ruby", + ".go": "go", + ".png": "image", + ".jpg": "image", + ".jpeg": "image", + ".gif": "image", + ".mp4": "video", + ".avi": "video", + ".mp3": "audio", + ".webm": "video", + ".mov": "video", + ".mkv": "video", + ".pdf": "pdf", + ",log": "plaintext", + ".md": "markdown", + ".yaml": "yaml", + ".toml": "toml", + ".rs": "rust", + ".kt": "kotlin", + ".php": "php", + ".cs": "csharp", + ".xml": "xml", + ".7z": "compressed", + ".zip": "compressed", + ".tar": "compressed", + ".gz": "compressed", + ".bz2": "compressed", + ".rar": "compressed", + ".tgz": "compressed", + ".xz": "compressed", + ".zst": "compressed", + ".lz4": "compressed", + ".lzma": "compressed", + ".lzo": "compressed", + ".z": "compressed", + ".arj": "compressed", + ".cab": "compressed" + # Agrega más mapeos según tus necesidades +} + +FILENAME_TO_LANGUAGE = { + "PKGBUILD": "bash", + # Puedes añadir más archivos específicos aquí + # "INSTALL": "bash", + # "configure": "bash", +} + + +mime = magic.Magic(mime=True, uncompress=True) + + +# Definir MIME types y extensiones de archivos comprimidos +COMPRESSED_MIME_TYPES = { + "application/zip": "compressed", + "application/gzip": "compressed", + "application/x-tar": "compressed", + "application/x-bzip2": "compressed", + "application/x-7z-compressed": "compressed" +} + +COMPRESSED_EXTENSIONS = { + ".zip": "compressed", + ".gz": "compressed", + ".tgz": "compressed", + ".tar": "compressed", + ".bz2": "compressed", + ".7z": "compressed" +} + + +def detect_language(content, unique_filename=None, original_filename=None, detected_mime_type=None): + MIME_TO_LANGUAGE = { + "application/json": "json", + "application/x-shellscript": "bash", + "application/javascript": "javascript", + "text/plain": None, + "text/xml": "xml", + "text/html": "html", + "text/css": "css", + "text/x-python": "python", + "text/x-c": "c", + "text/x-c++": "cpp", + "text/x-java": "java", + "text/x-php": "php", + "text/x-shellscript": "bash", + "application/sql": "sql", + "application/x-yaml": "yaml", + "application/x-toml": "toml", + "text/x-markdown": "markdown", + "text/markdown": "markdown", + "text/x-lua": "lua", + "text/x-rust": "rust", + "application/x-perl": "perl", + "text/x-ruby": "ruby", + } + + # 1️⃣ Por extensión + if unique_filename: + ext = os.path.splitext(unique_filename)[1].lower() + if ext in EXTENSION_TO_LANGUAGE: + language_by_extension = EXTENSION_TO_LANGUAGE[ext] + logging.info(f"📂 Detectado lenguaje por extensión '{ext}': '{language_by_extension}'") + return language_by_extension + + # 2️⃣ Por nombre de archivo específico + if original_filename: + base_name = os.path.basename(original_filename) + if base_name.upper() in FILENAME_TO_LANGUAGE: + language = FILENAME_TO_LANGUAGE[base_name.upper()] + logging.info(f"📄 Nombre de archivo '{base_name}' mapeado a lenguaje '{language}'") + return language + + # 3️⃣ Por shebang (primera línea del contenido) + if content: + first_line = content.strip().splitlines()[0] if content.strip() else '' + if first_line.startswith('#!'): + shebang_map = { + 'python': 'python', + 'bash': 'bash', + 'sh': 'bash', + 'perl': 'perl', + 'ruby': 'ruby', + 'php': 'php', + 'node': 'javascript', + 'lua': 'lua', + } + for key, lang in shebang_map.items(): + if key in first_line: + logging.info(f"📌 Shebang detectado en primera línea: {first_line} → {lang}") + return lang + + # 4️⃣ Por MIME + if detected_mime_type: + detected_language = MIME_TO_LANGUAGE.get(detected_mime_type) + if detected_language: + logging.info(f"📝 MIME type '{detected_mime_type}' mapeado a lenguaje '{detected_language}'") + return detected_language + elif detected_mime_type == "text/plain": + logging.info(f"📎 MIME type 'text/plain', usando Guesslang como fallback.") + + # 5️⃣ Por Guesslang (si el contenido es suficientemente largo) + if content and len(content.strip()) > 30: + try: + guess = Guess() + detected_language = guess.language_name(content) + logging.info(f"🤖 Guesslang detectó: '{detected_language}'") + return detected_language + except Exception as e: + logging.warning(f"⚠️ Guesslang falló: {e}") + + # 6️⃣ Por Pygments (fallback) + if content: + try: + lexer = guess_lexer(content) + language = lexer.aliases[0] + logging.info(f"🎨 Pygments detectó el lenguaje como '{language}'") + return language + except ClassNotFound: + logging.warning("⚠️ Pygments no pudo detectar el lenguaje.") + + # 7️⃣ Por defecto + logging.warning("🚨 No se pudo determinar el lenguaje, asignando 'plaintext'.") + return "plaintext" + + +def init_routes(app): + @app.route('/file/', methods=['GET']) + def get_file(filename): + file_path = os.path.join(UPLOAD_FOLDER, filename) + + if not os.path.exists(file_path): + abort(404, description="file not found") + + mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + return send_from_directory(UPLOAD_FOLDER, filename, mimetype=mime_type) + + + @app.route('/update-theme', methods=['POST']) + def update_theme(): + data = request.get_json() + theme = data.get('theme') + if theme not in ['light', 'dark']: + return jsonify({'error': 'Invalid theme'}), 400 + session['theme'] = theme + return jsonify({'message': 'Theme updated successfully'}), 200 + + + @app.route('/', methods=['GET']) + def index(): + base_url = request.host_url.rstrip('/') + user_name = None + + # Supongamos que el usuario está guardado en la sesión con su ID + if 'user_id' in session: + user = User.query.get(session['user_id']) # Recuperar el usuario de la base de datos + user_name = user.username if user else None # Obtener el nombre del usuario + + return render_template('index.html', base_url=base_url, user_name=user_name) + + + @app.route('/login', methods=['GET', 'POST']) + def login(): + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + user = User.query.filter_by(username=username).first() + + if user and user.check_password(password): + login_user(user) # Autentica al usuario + flash("Login successful!", "success") + return redirect(url_for('user_dashboard')) + + flash("Invalid username or password", "danger") + return redirect(url_for('login')) + + return render_template('login.html') + + @app.route('/paste//json', methods=['GET']) + def get_paste_json(id): + paste = Paste.query.get(id) + + if not paste: + return jsonify({"error": "Paste not found"}), 404 + + # ✅ Restringir acceso si el paste es privado + if paste.private: + if not current_user.is_authenticated: + logging.warning(f"🚫 Usuario no autenticado intentó acceder al paste privado {id} en JSON.") + return jsonify({"error": "You do not have permission to view this paste"}), 403 + + if current_user.id != paste.owner_id and current_user not in paste.shared_with: + logging.warning(f"🚫 Usuario {current_user.id} no tiene acceso al paste privado {id} en JSON.") + return jsonify({"error": "You do not have permission to view this paste"}), 403 + + response_data = { + "id": paste.id, + "filename": paste.filename, + "language": paste.language, + "content_type": paste.content_type, + "size": paste.size, + "created_at": paste.created_at.strftime('%Y-%m-%d %H:%M:%S') if paste.created_at else None, + "content": None + } + + # ✅ Solo incluir contenido si es accesible + if hasattr(paste, 'content') and paste.content: + response_data["content"] = paste.content + elif paste.filename: + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if os.path.exists(file_path): + with open(file_path, 'r', encoding='utf-8', errors='replace') as f: + response_data["content"] = f.read() + + return jsonify(response_data), 200 + + + @app.route('/paste//raw', methods=['GET']) + def get_paste_raw(id): + paste = Paste.query.get(id) + + if not paste: + return jsonify({"error": "Paste not found"}), 404 + + # ✅ Restringir acceso si el paste es privado + if paste.private: + if not current_user.is_authenticated: + logging.warning(f"🚫 Usuario no autenticado intentó acceder al paste privado {id} en RAW.") + return jsonify({"error": "You do not have permission to view this paste"}), 403 + + if current_user.id != paste.owner_id and current_user not in paste.shared_with: + logging.warning(f"🚫 Usuario {current_user.id} no tiene acceso al paste privado {id} en RAW.") + return jsonify({"error": "You do not have permission to view this paste"}), 403 + + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + # ✅ Leer el contenido del archivo y devolverlo como texto sin formato + with open(file_path, 'r', encoding='utf-8', errors='replace') as f: + content = f.read() + + return content, 200, {'Content-Type': 'text/plain; charset=utf-8'} + + + + @app.route('/paste', methods=['POST']) + @jwt_required + def create_paste(): + """Crea un nuevo paste con detección automática de lenguaje, opciones de expiración y soporte para compartir.""" + file_obj = request.files.get('c') + user_language = request.form.get('lang', '').strip().lower() + expire = request.form.get('expire', 'yes').strip().lower() # Normaliza el input + share_with = request.form.getlist('share_with') # Lista de usuarios con los que se comparte + can_edit = request.form.get('can_edit', 'false').strip().lower() == 'true' # Convertir a booleano + private = request.form.get('private', 'false').strip().lower() == 'true' # ✅ Nuevo: privacidad del paste + + logging.info(f"User provided language: {user_language}") + + if not file_obj: + logging.warning("No file provided for the paste.") + return jsonify({"error": "No content provided"}), 400 + + filename = None + content_type = None + + # Obtener usuario autenticado + user = request.user + + # Leer el archivo antes de calcular el tamaño + file_content = file_obj.read() + file_size = len(file_content) + file_obj.seek(0) # Resetear el puntero del archivo + + # Validar límites de usuario + if user.role == 'user': + if file_size > 100 * 1024**2: # 100 MB por paste + logging.warning(f"User {user.username} tried to upload a file exceeding 100 MB.") + return jsonify({"error": "File exceeds the 100 MB limit for your role"}), 403 + if user.storage_used + file_size > user.storage_limit: # Límite de almacenamiento total + logging.warning(f"User {user.username} exceeded their total storage limit.") + return jsonify({"error": "You have exceeded your total storage limit"}), 403 + + try: + # Obtener la extensión del archivo + original_filename = file_obj.filename + file_extension = os.path.splitext(original_filename)[1].lower() + + # Generar un nombre único para el archivo + unique_filename = f"{uuid.uuid4().hex}{file_extension}" + file_path = os.path.join(UPLOAD_FOLDER, unique_filename) + + # Leer una muestra del archivo para analizar MIME + file_sample = file_content[:8192] + detected_mime_type = magic.Magic(mime=True).from_buffer(file_sample) + + logging.info(f"Detected MIME type: {detected_mime_type}") + logging.info(f"Original filename: {original_filename}, Extension: {file_extension}, Size: {file_size} bytes") + content_type = detected_mime_type + + # Guardar el archivo en el sistema de archivos + with open(file_path, 'wb') as f: + f.write(file_content) + logging.info(f"Saved paste content to: {file_path}") + + # **Usar `detect_language()` para la detección de lenguaje** + if not user_language: + logging.info("No valid language provided, using automatic detection.") + language = detect_language( + content=file_content.decode(errors="ignore"), + unique_filename=unique_filename, + original_filename=original_filename, + detected_mime_type=detected_mime_type + ) + else: + language = user_language + logging.info(f"User specified language: {language}") + + filename = unique_filename + + # Actualizar el almacenamiento usado por el usuario + user.storage_used += file_size + db.session.commit() + + except Exception as e: + logging.error(f"Exception occurred: {str(e)}") + return jsonify({"error": f"Error processing the content: {str(e)}"}), 500 + + # **Manejo de Expiración** + expires_at = datetime.utcnow() + timedelta(days=1) if expire == 'yes' else None + + # **Crear registro del paste en la base de datos** + paste = Paste( + content_type=content_type, + filename=filename, + owner_id=user.id, + user_id=user.id, + language=language, + size=file_size, + expires_at=expires_at, + private=private # ✅ Guardamos la privacidad del paste + ) + + try: + db.session.add(paste) + db.session.commit() + logging.info(f"Saved paste with ID: {paste.id}") + + # **Indexar contenido en Elasticsearch** + index_paste(paste) + + # **Compartir paste con otros usuarios** + shared_users = [] + if share_with: + for username in share_with: + shared_user = User.query.filter_by(username=username).first() + if shared_user and shared_user.id != user.id: + db.session.execute(shared_pastes.insert().values( + paste_id=paste.id, + user_id=shared_user.id, + can_edit=can_edit + )) + shared_users.append(username) + + db.session.commit() + logging.info(f"Paste {paste.id} shared with: {shared_users} (Editable: {can_edit})") + + except Exception as e: + db.session.rollback() + logging.error(f"Error saving paste: {str(e)}") + return jsonify({"error": f"Error saving paste: {str(e)}"}), 500 + + base_url = request.host_url.rstrip('/') + paste_url = f"{base_url}/paste/{paste.id}" + + logging.info(f"Paste created successfully. ID: {paste.id}, URL: {paste_url}") + return jsonify({ + "url": paste_url, + "language": language, + "expires_at": expires_at, + "private": private, # ✅ Enviar estado de privacidad en la respuesta + "shared_with": shared_users, + "can_edit": can_edit + }), 201 + + + @app.route('/paste/', methods=['GET']) + def get_paste(id): + paste = Paste.query.get(id) + + if not paste: + logging.warning(f"Paste ID {id} not found.") + return render_template("errors/404.html", message="This paste has been deleted or has expired."), 404 + + # ✅ Restringir acceso si el paste es privado + if paste.private: + if not current_user.is_authenticated: + logging.warning("🚫 Usuario no autenticado intentando acceder a paste privado.") + return render_template("errors/403.html"), 403 + + if current_user.id != paste.owner_id and current_user not in paste.shared_with: + logging.warning(f"🚫 Usuario {current_user.id} no tiene acceso al paste privado {id}.") + return render_template("errors/403.html"), 403 + + # ✅ Determinar si el usuario puede editar el paste + can_edit = paste.has_edit_permission(current_user) if current_user.is_authenticated else False + + # ✅ Obtener ruta del archivo + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) if paste.filename else None + has_gps = has_gps_data(file_path) if paste.content_type.startswith("image/") else False + + # ✅ Si el usuario es el dueño, obtener lista de usuarios con los que se ha compartido + shared_with = [] + if current_user.is_authenticated and paste.owner_id == current_user.id: + shared_with = db.session.query(User.username, shared_pastes.c.can_edit).join( + shared_pastes, User.id == shared_pastes.c.user_id + ).filter(shared_pastes.c.paste_id == paste.id).all() + + # ✅ Definir los tipos MIME + BINARIO_MIME_TYPES = ('font/', 'model/') + TEXT_BASED_APPLICATION_MIME_TYPES = ( + 'application/json', 'application/javascript', 'application/xml', + 'application/xhtml+xml', 'application/sql' + ) + MEDIA_MIME_TYPES = ('image/', 'video/', 'audio/', 'application/pdf') + + # ✅ Funciones para verificar tipos MIME + def is_binary(mime_type): + return any(mime_type.startswith(prefix) for prefix in BINARIO_MIME_TYPES) + + def is_media(mime_type): + return any(mime_type.startswith(prefix) for prefix in MEDIA_MIME_TYPES) + + # ✅ Determinar el tipo de contenido + if paste.content_type.startswith('text/') or paste.content_type in TEXT_BASED_APPLICATION_MIME_TYPES: + es_binario, es_media = False, False + elif is_media(paste.content_type): + es_binario, es_media = False, True + elif is_binary(paste.content_type): + es_binario, es_media = True, False + else: + es_binario, es_media = True, False # Por defecto, tratarlo como binario + + logging.debug(f"Content Type: {paste.content_type} | Is binary: {es_binario} | Is media: {es_media}") + + # ✅ Si es binario, renderizar `binary_view.html` + if es_binario: + if not paste.filename or not os.path.exists(file_path): + logging.error(f"File for paste ID {id} not found: {file_path}") + return jsonify({"error": "File not found"}), 404 + + try: + owner = User.query.get(paste.owner_id) + file_size = paste.size # Asegurar que `size` está en el modelo `Paste` + return render_template( + 'binary_view.html', paste=paste, filename=paste.filename, mime_type=paste.content_type, + owner=owner, size=file_size, can_edit=can_edit + ) + except Exception as e: + logging.error(f"Error rendering binary view for paste ID {id}: {e}") + return jsonify({"error": "Error while processing the file"}), 500 + + # ✅ Si es media, renderizar `media_view.html` + elif es_media: + if not paste.filename or not os.path.exists(file_path): + logging.error(f"File for paste ID {id} not found: {file_path}") + return jsonify({"error": "File not found"}), 404 + + try: + metadata = [] + if paste.content_type.startswith("image/"): + logging.info(f"Extracting metadata for image: {file_path}") + metadata = get_image_metadata(file_path) or [{"Info": "No metadata found in image file"}] + elif paste.content_type.startswith(("video/", "audio/")): + logging.info(f"Extracting metadata for media: {file_path}") + media_info = MediaInfo.parse(file_path) + metadata = [{key: value for key, value in track.to_data().items() if value} for track in media_info.tracks] or [{"Info": "No metadata found in media file"}] + + return render_template( + 'media_view.html', filename=paste.filename, mime_type=paste.content_type, + metadata=metadata, paste_id=paste.id, can_edit=can_edit, has_gps=has_gps + ), 200 + + except Exception as e: + logging.error(f"Error rendering media view for paste ID {id}: {e}") + return jsonify({"error": "Error while processing the media file"}), 500 + + # ✅ Si es texto, renderizar `text_paste.html` + else: + try: + with open(file_path, 'r', encoding='utf-8', errors='replace') as f: + content = f.read() + + is_markdown = ( + paste.filename and paste.filename.lower().endswith('.md') or + paste.language and paste.language.lower() in ['md', 'markdown'] or + paste.content_type == 'text/markdown' + ) + + if is_markdown: + md_html = markdown(content, extensions=['tables', 'fenced_code', 'codehilite']) + return render_template( + 'text_paste.html', paste=paste, md_html_code=md_html, is_markdown=True, + can_edit=can_edit, shared_with=shared_with + ), 200 + else: + html_code = highlight_code(content, language=paste.language, filename=paste.filename) + return render_template( + 'text_paste.html', paste=paste, html_code=html_code, is_markdown=False, + can_edit=can_edit, shared_with=shared_with + ), 200 + + except Exception as e: + logging.error(f"Error reading text file for paste ID {id}: {e}") + return jsonify({"error": "Error processing text file"}), 500 + + + @app.route('/pastes', methods=['GET']) + @jwt_required + def list_pastes(): + try: + # Obtener todos los pastes del usuario autenticado + user = request.user + pastes = Paste.query.filter_by(owner_id=user.id).all() + + if not pastes: + return jsonify({"message": "No pastes found"}), 200 + + base_url = request.host_url.rstrip('/') + response_list = [] + for paste in pastes: + response_list.append({ + "id": paste.id, + "url": f"{base_url}/paste/{paste.id}", + "title": paste.title or paste.filename, + "type": paste.get_type(), # Método definido en tu modelo Paste + "size": paste.size or 0, # Devuelve 0 si el tamaño no está definido + "created_at": paste.created_at.strftime('%Y-%m-%d %H:%M:%S') if paste.created_at else None, + "is_favorite": paste in user.favorite_pastes # Verifica si el paste es favorito + }) + + return jsonify(response_list), 200 + except Exception as e: + logging.error(f"Error retrieving pastes: {e}") + return jsonify({"error": "Error retrieving pastes"}), 500 + + @app.route('/register', methods=['POST']) + @jwt_required + def register_user(): + authenticated_user = request.user + + if authenticated_user.username != 'admin': + return jsonify({"error": "you have no access rights to register users"}), 403 + + data = request.get_json() + if not data: + return jsonify({"error": "No data provided"}), 400 + + username = data.get('username') + password = data.get('password') + + if not username or not password: + return jsonify({"error": "'username' and 'password' required"}), 400 + + existing_user = User.query.filter_by(username=username).first() + if existing_user: + return jsonify({"error": "El nombre de usuario ya existe"}), 400 + + try: + new_user = User(username=username) + new_user.set_password(password) + db.session.add(new_user) + db.session.commit() + return jsonify({"message": f"Usuario '{username}' registry successful"}), 201 + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Error registering user: {str(e)}"}), 500 + + @app.route('/admin/login', methods=['GET', 'POST']) + def admin_login(): + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + user = User.query.filter_by(username=username).first() + + if user and user.check_password(password) and username == 'admin': + session['admin'] = username + flash('Login successful', 'success') + return redirect(url_for('admin_dashboard')) + + flash('Invalid credentials', 'danger') + return render_template('admin_login.html') + + @app.route('/admin/logout') + def admin_logout(): + session.pop('admin', None) + flash('Logged out', 'info') + return redirect(url_for('admin_login')) + + @app.route('/admin') + def admin_dashboard(): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + users_count = User.query.count() + pastes_count = Paste.query.count() + return render_template('dashboard.html', users_count=users_count, pastes_count=pastes_count) + + @app.route('/admin/users') + def admin_users(): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + users = User.query.all() + return render_template('users.html', users=users) + + @app.route('/admin/users/add', methods=['GET', 'POST']) + def admin_add_user(): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + role = request.form.get('role') # Obtén el rol del formulario + + # Validar datos + if not username or not password or not role: + flash('All fields are required', 'danger') + return redirect(url_for('admin_add_user')) + + # Validar si el rol es válido + valid_roles = ['admin', 'advanced', 'user'] + if role not in valid_roles: + flash('Invalid role selected.', 'danger') + return redirect(url_for('admin_add_user')) + + # Crear usuario + try: + user = User(username=username, role=role, storage_limit=ROLE_STORAGE_LIMITS.get(role)) + user.set_password(password) + db.session.add(user) + db.session.commit() + flash('User added successfully', 'success') + return redirect(url_for('admin_users')) + except Exception as e: + db.session.rollback() + flash(f'Error: {str(e)}', 'danger') + return redirect(url_for('admin_add_user')) + + return render_template('add_user.html') + + @app.route('/admin/users/delete/', methods=['POST']) + def admin_delete_user(user_id): + if 'admin' not in session: + # Enviar JSON de error en lugar de redirigir + return jsonify({"error": "Unauthorized"}), 401 + + user = User.query.get(user_id) + if not user: + return jsonify({"error": "User not found"}), 404 + + db.session.delete(user) + db.session.commit() + return jsonify({"message": "User deleted successfully"}), 200 + + + @app.route('/admin/pastes') + def admin_pastes(): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + pastes = Paste.query.all() + return render_template('pastes.html', pastes=pastes) + + @app.route('/admin/pastes/delete/', methods=['POST']) + def admin_delete_paste(paste_id): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + paste = Paste.query.get(paste_id) + if not paste: + flash('Paste not found', 'danger') + else: + delete_paste_from_index(paste) + paste.shared_with.clear() + db.session.delete(paste) + db.session.commit() + flash('Paste deleted successfully', 'success') + return redirect(url_for('admin_pastes')) + + @app.route('/stats', methods=['GET']) + def anonymous_stats(): + start_date = request.args.get('start_date') + end_date = request.args.get('end_date') + + stats = calculate_stats(start_date=start_date, end_date=end_date) + + return render_template( + 'anonymous_stats.html', + total_pastes=stats["total_pastes"], + total_text_pastes=stats["total_text_pastes"], + total_file_pastes=stats["total_file_pastes"], + total_media_pastes=stats["total_media_pastes"], + languages=stats["languages"], + counts_text=stats["counts_text"], + counts_file=stats["counts_file"], + counts_media=stats["counts_media"], + ) + + + @app.route('/api/docs') + def redoc(): + return ''' + + + + Pastebin API Docs + + + + + + + + ''' + @app.route('/paste//download', methods=['GET']) + def download_paste(id): + paste = Paste.query.get_or_404(id) + + # Comprobación de permisos para pastes privados + if paste.private: + if not current_user.is_authenticated or ( + current_user.id != paste.owner_id and current_user not in paste.shared_with + ): + flash('No tienes permiso para descargar este archivo.', 'danger') + return redirect(url_for('get_paste', id=id)) + + if not paste.filename: + flash('Este paste no tiene un archivo asociado.', 'warning') + return redirect(url_for('get_paste', id=id)) + + file_path = os.path.join(app.config['UPLOAD_FOLDER'], paste.filename) + if not os.path.exists(file_path): + flash('El archivo no existe.', 'danger') + return redirect(url_for('get_paste', id=id)) + + # 🧠 Nombre limpio + extension = paste.get_extension() + download_name = f"paste_{paste.id}.{extension}" + + # 🧠 Corrección de mimetype si es sospechoso + fallback_mime = { + 'zip': 'application/zip', + 'pdf': 'application/pdf', + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'png': 'image/png', + 'gif': 'image/gif', + 'mp3': 'audio/mpeg', + 'mp4': 'video/mp4', + 'txt': 'text/plain', + 'json': 'application/json' + } + mimetype = paste.content_type or fallback_mime.get(extension, 'application/octet-stream') + # fallback si content_type es incorrecto (ej: text/plain para .zip) + + # 📦 Leer archivo y servir como stream + with open(file_path, 'rb') as f: + file_data = BytesIO(f.read()) + + return send_file( + file_data, + as_attachment=True, + download_name=download_name, + mimetype=mimetype + ) + + + @app.route('/request-account', methods=['GET', 'POST']) + def request_account(): + if request.method == 'POST': + email = request.form.get('email') + username = request.form.get('username') + + # Validate basic input + if not email or not username: + logging.warning("Validation failed: Missing email or username") + flash("Email and username are required.", "danger") + return redirect(url_for('request_account')) + + logging.info(f"Account request initiated. Username: {username}, Email: {email}") + + # Log the SMTP environment variables for debugging + logging.debug(f"SMTP_SERVER={SMTP_SERVER}") + logging.debug(f"SMTP_PORT={SMTP_PORT}") + logging.debug(f"SMTP_USERNAME={SMTP_USERNAME}") + logging.debug(f"SMTP_USE_SSL={SMTP_USE_SSL}, SMTP_USE_TLS={SMTP_USE_TLS}") + + # Send email + try: + msg = MIMEText(f"New account request:\n\nUsername: {username}\nEmail: {email}") + msg['Subject'] = 'Account Request' + msg['From'] = SMTP_USERNAME + msg['To'] = SMTP_USERNAME # Ajusta a tu dirección de correo receptora + + # Connect to the SMTP server + if SMTP_USE_SSL: + with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as server: + logging.debug("Using SMTP over SSL") + server.login(SMTP_USERNAME, SMTP_PASSWORD) + logging.info("SMTP login successful") + server.send_message(msg) + else: + with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server: + if SMTP_USE_TLS: + logging.debug("Starting TLS session") + server.starttls() + server.login(SMTP_USERNAME, SMTP_PASSWORD) + logging.info("SMTP login successful") + server.send_message(msg) + + logging.info("Account request email sent successfully") + flash("Your account request has been submitted successfully!", "success") + return redirect(url_for('index')) + + except smtplib.SMTPException as smtp_error: + logging.error(f"SMTP error: {smtp_error}") + flash(f"SMTP error: {smtp_error}", "danger") + except Exception as general_error: + logging.error(f"An unexpected error occurred: {general_error}") + flash(f"An error occurred: {general_error}", "danger") + + return redirect(url_for('request_account')) + + return render_template('request_account.html') + + @app.route('/user/details', methods=['GET']) + @jwt_required + def get_user_details(): + user = request.user # Usuario autenticado + if not user: + return jsonify({"error": "User not found"}), 404 + + # Calcular contadores + pastes_count = Paste.query.filter_by(owner_id=user.id).count() + favorites_count = user.favorite_pastes.count() + shared_with_me_count = db.session.query(Paste).join(shared_pastes).filter( + shared_pastes.c.user_id == user.id + ).count() + shared_with_others_count = db.session.query(Paste).join(shared_pastes).filter( + Paste.owner_id == user.id, + shared_pastes.c.user_id != user.id + ).count() + + # Construir la respuesta + response = { + "id": user.id, + "username": user.username, + "role": user.role, + "storage_used": user.storage_used, + "storage_limit": user.storage_limit, + "storage_remaining": max(user.storage_limit - user.storage_used, 0) if user.storage_limit != -1 else None, + "theme_preference": user.theme_preference, + # Suponiendo que tienes un campo user.created_at + "created_at": user.created_at.isoformat() if hasattr(user, 'created_at') and user.created_at else None, + "pastes_count": pastes_count, + "favorites_count": favorites_count, + "shared_with_me_count": shared_with_me_count, + "shared_with_others_count": shared_with_others_count + } + + return jsonify(response), 200 + + + @app.route('/admin/users//change_role', methods=['POST']) + def change_user_role(user_id): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + try: + user = User.query.get(user_id) + if not user: + return jsonify({"error": "User not found"}), 404 + + new_role = request.form.get('role') + valid_roles = ['admin', 'advanced', 'user'] + if new_role not in valid_roles: + return jsonify({"error": "Invalid role"}), 400 + + # Actualizar rol y límite de almacenamiento + user.role = new_role + user.storage_limit = ROLE_STORAGE_LIMITS[new_role] + db.session.commit() + + return jsonify({"message": f"User role updated to {new_role}"}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Failed to update role: {str(e)}"}), 500 + + @app.route('/logout') + @login_required + def logout(): + logout_user() + flash("You have been logged out.", "info") + return redirect(url_for('login')) + + @app.route('/user-dashboard', methods=['GET']) + @login_required + def user_dashboard(): + user = current_user # Usuario autenticado automáticamente + + # Obtener el número de página desde los parámetros de consulta, por defecto es 1 + page = request.args.get('page', 1, type=int) + per_page = 10 # Número de pastes por página + favorite_page = request.args.get('favorite_page', 1, type=int) + + private_page = request.args.get('private_page', 1, type=int) + + pastes = Paste.query.filter_by(owner_id=user.id).order_by(Paste.created_at.desc()).paginate(page=page, per_page=10) + private_pastes = Paste.query.filter_by(owner_id=user.id, private=True).order_by(Paste.created_at.desc()).paginate(page=private_page, per_page=10) + + # Consultar los pastes del usuario con paginación + pastes_query = Paste.query.filter_by(user_id=user.id).order_by(Paste.created_at.desc()) + pagination = pastes_query.paginate(page=page, per_page=per_page, error_out=False) + pastes = pagination.items + + # Consultar los pastes favoritos del usuario con paginación + favorite_query = Paste.query.filter(Paste.favorites.any(id=user.id)) + favorite_pagination = favorite_query.paginate(page=favorite_page, per_page=10, error_out=False) + favorite_pastes = favorite_pagination.items + + # Consultar los pastes compartidos con el usuario + shared_pastes_list = Paste.query.join(shared_pastes, (shared_pastes.c.paste_id == Paste.id))\ + .filter(shared_pastes.c.user_id == user.id)\ + .all() + + # Calcular estadísticas generales + stats = calculate_stats(user.id) # Asegúrate de que esta función devuelve un diccionario con las claves usadas a continuación + + # **Nuevo Código: Cálculo de Almacenamiento** + + # Obtener el rol del usuario + user_role = user.role.lower() if user.role else 'user' # Asegúrate de que el campo 'role' existe en el modelo User + + # Obtener el límite de almacenamiento basado en el rol + storage_limits = current_app.config.get('ROLE_STORAGE_LIMITS', { + 'admin': -1, # Ilimitado + 'advanced': 2 * 1024**3, # 2GB en bytes + 'user': 1 * 1024**3, # 1GB en bytes + }) + + # Obtener límite en bytes y convertir a MB + storage_limit = storage_limits.get(user_role, 1 * 1024**3) # 1GB en bytes + storage_used = db.session.query(db.func.sum(Paste.size)).filter_by(user_id=user.id).scalar() or 0 + + # Calcular almacenamiento disponible + storage_available = storage_limit - storage_used if storage_limit != -1 else -1 + storage_available = max(storage_available, 0) # Evita valores negativos + + # CONVERSIÓN A MB + storage_used_mb = round(storage_used / (1024**2), 2) + storage_limit_mb = -1 if storage_limit == -1 else round(storage_limit / (1024**2), 2) + storage_available_mb = -1 if storage_available == -1 else round(storage_available / (1024**2), 2) + + # DEBUG LOG PARA COMPROBAR QUE SE CONVIERTE BIEN + app.logger.debug(f"Storage (MB) - Used: {storage_used_mb}, Limit: {storage_limit_mb}, Available: {storage_available_mb}") + + # Información del perfil del usuario (sin email ni bio) + user_profile = { + 'username': user.username, + 'role': user.role.capitalize() if user.role else 'User', + } + + return render_template( + 'user_dashboard.html', + user=user, + total_pastes=stats['total_pastes'], + total_size=stats['total_size'], + pastes=pastes, + pagination=pagination, # Pasar el objeto de paginación al template + total_text_pastes=stats.get('total_text_pastes', 0), + total_file_pastes=stats.get('total_file_pastes', 0), + total_media_pastes=stats.get('total_media_pastes', 0), + languages=stats.get('languages', []), + counts_text=stats.get('counts_text', []), + counts_file=stats.get('counts_file', []), + counts_media=stats.get('counts_media', []), + favorite_pastes=favorite_pastes, + favorite_pagination=favorite_pagination, + shared_pastes=shared_pastes_list, + storage_used=storage_used_mb, + storage_limit=storage_limit_mb, + storage_available=storage_available_mb, + user_profile=user_profile, + private_pastes=private_pastes, + LANGUAGE_TO_EXTENSION=LANGUAGE_TO_EXTENSION + ) + + + @app.route('/paste/', methods=['DELETE']) + @jwt_required + def delete_paste(id): + paste = Paste.query.get(id) + if not paste: + return jsonify({"error": "Paste not found"}), 404 + + # Permitir eliminación solo si es el propietario o un administrador + if paste.owner_id != request.user.id and request.user.username != 'admin': + return jsonify({"error": "You do not have permission to delete this paste"}), 403 + + try: + # Eliminar favoritos asociados + paste.favorites.clear() + + # Eliminar el archivo si existe + if paste.filename: + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if os.path.exists(file_path): + os.remove(file_path) + + delete_paste_from_index(paste) + paste.shared_with.clear() + db.session.delete(paste) + db.session.commit() + return jsonify({"message": "Paste deleted successfully"}), 200 + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Error deleting paste: {str(e)}"}), 500 + + + @app.route('/change-password-form', methods=['GET']) + @login_required + def change_password_form(): + return render_template('change_password.html') + + + @app.route('/change-password', methods=['POST']) + @login_required + def change_password(): + current_password = request.form.get('current_password') + new_password = request.form.get('new_password') + confirm_password = request.form.get('confirm_password') + + # Validar que los campos no estén vacíos + if not current_password or not new_password or not confirm_password: + flash("All fields are required.", "danger") + return redirect(url_for('change_password_form')) + + # Verificar que las contraseñas coincidan + if new_password != confirm_password: + flash("New passwords do not match.", "danger") + return redirect(url_for('change_password_form')) + + # Verificar contraseña actual + if not current_user.check_password(current_password): # Método check_password en el modelo User + flash("Current password is incorrect.", "danger") + return redirect(url_for('change_password_form')) + + # Actualizar la contraseña + try: + current_user.set_password(new_password) # Método set_password en el modelo User + db.session.commit() + flash("Password updated successfully.", "success") + except Exception as e: + db.session.rollback() + flash(f"An error occurred: {str(e)}", "danger") + + return redirect(url_for('user_dashboard')) + + @app.context_processor + def inject_pygments_styles(): + styles = list(get_all_styles()) + current_theme = session.get('theme', 'light') # Obtener el tema actual de la sesión + default_pygments_style = 'monokai' if current_theme == 'dark' else 'default' + + # Obtener el nombre del usuario si está logueado + user_name = None + if 'user_id' in session: + user = User.query.get(session['user_id']) + user_name = user.username if user else None + + return dict( + pygments_styles=styles, + default_pygments_style=default_pygments_style, + user_name=user_name + ) + + @app.route('/paste//download_page', methods=['GET']) + def download_page(id): + paste = Paste.query.get(id) + if not paste: + flash("Paste no encontrado", "danger") + return redirect(url_for('index')) + + return render_template('download_page.html', paste=paste) + + @app.route('/paste//download_file', methods=['GET']) + def download_file(id): + logging.info(f"Confirmed download request for paste ID: {id}") + paste = Paste.query.get(id) + if not paste: + logging.warning(f"Paste ID {id} not found.") + return jsonify({"error": "Paste not found"}), 404 + + # ✅ Obtener la extensión del mapeo usando paste.language + extension = LANGUAGE_TO_EXTENSION.get(paste.language.lower(), 'txt') if paste.language else 'txt' + download_name = f"paste_{paste.id}.{extension}" + logging.info(f"Using download filename: {download_name}") + + if paste.filename: # 📂 Archivo físico guardado + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if not os.path.exists(file_path): + logging.error(f"File {paste.filename} for paste ID {id} not found.") + return jsonify({"error": "File not found"}), 404 + + return send_from_directory( + directory=UPLOAD_FOLDER, + path=paste.filename, + as_attachment=True, + download_name=download_name, + mimetype=paste.content_type + ) + + elif paste.content: # 📝 Paste de texto + file_stream = BytesIO() + file_stream.write(paste.content.encode('utf-8')) + file_stream.seek(0) + + return send_file( + file_stream, + as_attachment=True, + download_name=download_name, + mimetype='text/plain' + ) + + logging.error(f"Paste ID {id} has no associated content.") + return jsonify({"error": "Paste has no associated content"}), 400 + + + + @app.route('/paste//stream_download', methods=['GET']) + def stream_download(id): + paste = Paste.query.get(id) + if not paste or not paste.filename: + return jsonify({"error": "Paste or file not found"}), 404 + + def generate(): + with open(os.path.join(UPLOAD_FOLDER, paste.filename), 'rb') as f: + while True: + chunk = f.read(4096) + if not chunk: + break + yield chunk + + response = Response(generate(), mimetype=paste.content_type) + response.headers.set('Content-Disposition', 'attachment', filename=paste.filename) + return response + + @app.route('/user/paste//delete', methods=['POST']) + @login_required + def user_delete_paste(id): + paste = Paste.query.get_or_404(id) + + # Verificar si el usuario es el propietario + if paste.owner_id != current_user.id: + return jsonify({"error": "You do not have permission to delete this paste"}), 403 + + try: + # Eliminar favoritos asociados + paste.favorites.clear() + + # Eliminar el archivo asociado, si existe + if paste.filename: + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if os.path.exists(file_path): + os.remove(file_path) + + # Eliminar el paste de la base de datos + delete_paste_from_index(paste) + db.session.delete(paste) + db.session.commit() + return jsonify({"message": "Paste deleted successfully"}), 200 + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Error deleting paste: {str(e)}"}), 500 + + @app.route('/user//stats', methods=['GET']) + @login_required + def user_stats(username): + if current_user.username != username: + abort(404) + + start_date_str = request.args.get('start_date') + end_date_str = request.args.get('end_date') + + paste_filters = [] + try: + if start_date_str: + start_date = datetime.strptime(start_date_str, '%Y-%m-%d') + paste_filters.append(Paste.created_at >= start_date) + if end_date_str: + end_date = datetime.strptime(end_date_str, '%Y-%m-%d').replace(hour=23, minute=59, second=59) + paste_filters.append(Paste.created_at <= end_date) + except ValueError as ve: + app.logger.error(f"Error al parsear las fechas: {ve}") + + # Obtener pastes y favoritos + page = request.args.get('page', 1, type=int) + per_page = 10 + pastes_query = Paste.query.filter_by(user_id=current_user.id).order_by(Paste.created_at.desc()) + if paste_filters: + pastes_query = pastes_query.filter(*paste_filters) + pagination = pastes_query.paginate(page=page, per_page=per_page, error_out=False) + + favorite_query = Paste.query.filter(Paste.favorites.any(id=current_user.id)) + if paste_filters: + favorite_query = favorite_query.filter(*paste_filters) + favorite_pagination = favorite_query.paginate(page=1, per_page=10, error_out=False) + + # Calcular estadísticas + stats = calculate_stats(current_user.id) + + # ✅ ✅ Si es AJAX (`?format=json`), solo devuelve datos del gráfico + if request.args.get('format') == 'json': + return jsonify({ + "languageLabels": stats['languages'], + "textCounts": stats['counts_text'], + "fileCounts": stats['counts_file'], + "mediaCounts": stats['counts_media'], + # ✅ Añadir las métricas con nombres correctos + "totalPastes": stats['total_pastes'], + "totalTextPastes": stats['total_text_pastes'], + "totalFilePastes": stats['total_file_pastes'], + "totalMediaPastes": stats['total_media_pastes'] + }) + + # Obtener almacenamiento + try: + storage_used = calculate_storage_used(current_user.id) + storage_limit = current_user.get_storage_limit() or -1 + storage_available = max(storage_limit - storage_used, 0) if storage_limit != -1 else -1 + + storage_used_mb = round(storage_used / (1024**2), 2) + storage_limit_mb = -1 if storage_limit == -1 else round(storage_limit / (1024**2), 2) + storage_available_mb = -1 if storage_available == -1 else round(storage_available / (1024**2), 2) + except Exception as e: + app.logger.error(f"Error al calcular el almacenamiento: {e}") + storage_used_mb = 0 + storage_limit_mb = -1 + storage_available_mb = -1 + + shared_page = request.args.get('shared_page', 1, type=int) + shared_pagination = get_shared_pastes(current_user, paste_filters=paste_filters, page=shared_page, per_page=per_page) + + # ✅ ✅ Si NO es AJAX, devuelve la página HTML normal (`render_template`) + # ✅ Si es AJAX (`?format=json`), devuelve solo JSON (para gráficos y métricas) + if request.args.get('format') == 'json': + return jsonify({ + "languageLabels": stats['languages'], + "textCounts": stats['counts_text'], + "fileCounts": stats['counts_file'], + "mediaCounts": stats['counts_media'], + "totalPastes": stats['total_pastes'], + "totalTextPastes": stats['total_text_pastes'], + "totalFilePastes": stats['total_file_pastes'], + "totalMediaPastes": stats['total_media_pastes'] + }) + + return render_template( + 'user_dashboard.html', + user=current_user, + user_profile=current_user.role, + total_pastes=stats['total_pastes'], + total_size=stats['total_size'], + total_text_pastes=stats['total_text_pastes'], + total_file_pastes=stats['total_file_pastes'], + total_media_pastes=stats['total_media_pastes'], + languages=stats['languages'], + counts_text=stats['counts_text'], + counts_file=stats['counts_file'], + counts_media=stats['counts_media'], + pastes=pagination.items, + pagination=pagination, + favorite_pastes=favorite_pagination.items, + favorite_pagination=favorite_pagination, + shared_pastes=shared_pagination.items, + shared_pagination=shared_pagination, + start_date=start_date_str, + end_date=end_date_str, + storage_used=storage_used_mb, + storage_limit=storage_limit_mb, + storage_available=storage_available_mb, + ) + + + @app.route('/api/token', methods=['POST']) + def get_token(): + """ + Endpoint dedicado para generar un token JWT. + """ + data = request.json + if not data or 'username' not in data or 'password' not in data: + return jsonify({"error": "Username and password are required"}), 400 + + username = data['username'] + password = data['password'] + + user = User.query.filter_by(username=username).first() + if user and check_password_hash(user.password_hash, password): + token = generate_token(username) + return jsonify({"token": token}), 200 + + return jsonify({"error": "Invalid username or password"}), 401 + + @app.route('/paste//favorite', methods=['POST']) + @login_required + def add_to_favorites(id): + user = current_user # Usuario autenticado + paste = Paste.query.get_or_404(id) + + # Verificar si ya es favorito + if user in paste.favorites: + # Sigues devolviendo 200, pero ahora con "success": True + return jsonify({ + "success": True, + "message": "Paste already in favorites" + }), 200 + + try: + paste.favorites.append(user) + db.session.commit() + # ¡Aquí pones success: True! + return jsonify({ + "success": True, + "message": "Paste added to favorites" + }), 201 + except Exception as e: + db.session.rollback() + return jsonify({"error": str(e), "success": False}), 500 + + @app.route('/paste//unfavorite', methods=['POST']) + @login_required + def remove_from_favorites(id): + user = current_user # Usuario autenticado + paste = Paste.query.get_or_404(id) + + if user not in paste.favorites: + # Devuelve success: False + return jsonify({"error": "Paste not in favorites", "success": False}), 404 + + try: + paste.favorites.remove(user) + db.session.commit() + return jsonify({ + "success": True, + "message": "Paste removed from favorites" + }), 200 + except Exception as e: + db.session.rollback() + return jsonify({"error": str(e), "success": False}), 500 + + + @app.route('/favorites', methods=['GET']) + @login_required + def list_favorites(): + user = current_user + page = request.args.get('page', 1, type=int) + per_page = 10 + favorite_query = Favorite.query.filter_by(user_id=user.id) + pagination = favorite_query.paginate(page=page, per_page=per_page, error_out=False) + + favorites = [{ + "id": fav.paste.id, + "title": fav.paste.filename, + "type": fav.paste.get_type(), # Tipo de archivo + "created_at": fav.paste.created_at.strftime('%Y-%m-%d %H:%M:%S') # Fecha de creación + } for fav in pagination.items] + + return jsonify({ + "favorites": favorites, + "pagination": { + "page": pagination.page, + "per_page": pagination.per_page, + "total_pages": pagination.pages, + "total_items": pagination.total, + "has_next": pagination.has_next, + "has_prev": pagination.has_prev, + "next_page": pagination.next_num if pagination.has_next else None, + "prev_page": pagination.prev_num if pagination.has_prev else None + } + }) + + + @app.route('/media/', methods=['GET']) + def serve_media(id): + paste = Paste.query.get_or_404(id) + + # Ruta completa del archivo + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + + if not os.path.exists(file_path): + return "File not found", 404 + + # Sirve el archivo directamente como contenido de video + return send_file(file_path, mimetype=paste.content_type) + + + @app.route('/create_paste_web', methods=['GET', 'POST']) + @login_required + def create_paste_web(): + if request.method == 'POST': + paste_type = request.form.get('type') + content = request.form.get('content', '').strip() + file = request.files.get('file') + expire = request.form.get('expire', 'yes').strip().lower() + user_language = request.form.get('language', '').strip().lower() + private = request.form.get('private', 'false').strip().lower() == 'true' # ✅ Nuevo campo de privacidad + share_with = request.form.getlist('share_with') # ✅ Lista de usuarios para compartir + + filename = None + language = None + content_type = None + size = 0 + + if file: + original_filename = secure_filename(file.filename) + file_extension = os.path.splitext(original_filename)[1].lower().lstrip('.') + filename = f"{uuid.uuid4().hex}.{file_extension}" + file_path = os.path.join(UPLOAD_FOLDER, filename) + + file_content = file.read() + size = len(file_content) + file.seek(0) + + detected_mime_type = magic.Magic(mime=True).from_buffer(file_content[:8192]) + content_type = detected_mime_type + + with open(file_path, 'wb') as f: + f.write(file_content) + + language = detect_language(content=file_content.decode(errors="ignore"), unique_filename=filename) + + elif paste_type == 'Text': + if not user_language: + language = detect_language(content=content) + else: + language = user_language + + filename = f"{uuid.uuid4().hex}.txt" + file_path = os.path.join(UPLOAD_FOLDER, filename) + + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + + content_type = 'text/plain' + size = len(content) + + expires_at = datetime.utcnow() + timedelta(days=1) if expire == 'yes' else None + + # ✅ Crear el nuevo paste con privacidad y expiración + paste = Paste( + content_type=content_type, + filename=filename, + owner_id=current_user.id, + user_id=current_user.id, + language=language, + size=size, + expires_at=expires_at, + private=private # ✅ Guardamos la privacidad del paste + ) + + db.session.add(paste) + db.session.commit() + + # ✅ Compartir el paste con otros usuarios si está configurado + shared_users = [] + if share_with: + for username in share_with: + shared_user = User.query.filter_by(username=username).first() + if shared_user and shared_user.id != current_user.id: + db.session.execute(shared_pastes.insert().values( + paste_id=paste.id, + user_id=shared_user.id + )) + shared_users.append(username) + + db.session.commit() + + index_paste(paste) + + flash("Paste created successfully!", "success") + return redirect(url_for('get_paste', id=paste.id)) + + pygments_languages = get_readable_languages() + return render_template('create_paste_web.html', pygments_languages=pygments_languages) + + def new_mail_signal(display_name, new_count, *args): + # Extraer los valores de las estructuras + mailbox = display_name[0] if isinstance(display_name, dbus.Struct) else display_name + count = new_count[0] if isinstance(new_count, dbus.Struct) else new_count + + print(f"Nuevo correo en: {mailbox}. Correos nuevos: {count}") + + def main(): + # Configurar el loop de D-Bus + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + + # Conectarse al bus de sesión + bus = dbus.SessionBus() + + # Suscribirse a la señal de nuevo correo + bus.add_signal_receiver( + new_mail_signal, + dbus_interface="org.gnome.evolution.mail.dbus.Signal", + signal_name="Newmail", + path="/org/gnome/evolution/mail/newmail" + ) + + # Ejecutar el loop principal + loop = GLib.MainLoop() + loop.run() + + if __name__ == "__main__": + main() + + @app.route('/paste/') + @login_required + def view_paste(paste_id): + paste = Paste.query.get_or_404(paste_id) + # Verificar que el usuario es el propietario o tiene permisos para ver el paste + if paste.owner_id != current_user.id: + flash('No tienes permiso para ver este paste.', 'danger') + return redirect(url_for('user_dashboard')) + return render_template('view_paste.html', paste=paste) + + @app.route('/pastes/search', methods=['GET']) + @jwt_required + def search_pastes(): + """Búsqueda de pastes en Elasticsearch considerando pastes públicos y compartidos.""" + try: + query = request.args.get('q', '').strip() + if not query: + return jsonify({"error": "Search query is required"}), 400 + + user = request.user + if not user: + return jsonify({"error": "User not authenticated"}), 403 + + body = { + "query": { + "bool": { + "must": [{"match": {"content": query}}], + "filter": [ + {"bool": { + "should": [ + {"term": {"owner_id": user.id}}, + {"terms": {"id": [paste.id for paste in user.shared_pastes]}}, + {"term": {"private": False}} + ] + }} + ] + } + } + } + results = es.search(index="pastes", body=body) + hits = results["hits"]["hits"] + + base_url = request.host_url.rstrip('/') + response = [{ + "id": hit["_id"], + "url": f"{base_url}/paste/{hit['_id']}", + "content": hit["_source"].get("content", "") + } for hit in hits] + + return jsonify(response), 200 + + except Exception as e: + current_app.logger.error(f"Error searching pastes: {e}") + return jsonify({"error": "Error searching pastes"}), 500 + + + @app.route('/pastes/search_web', methods=['GET']) + @login_required + def search_pastes_web(): + """Búsqueda de pastes desde la web con filtros adicionales.""" + try: + query = request.args.get('q', '').strip() + content_type = request.args.get('content_type', '').strip() + language = request.args.get('language', '').strip() + + user = current_user + if not user.is_authenticated: + return redirect(url_for('login')) + + must_clauses = [] + if query: + must_clauses.append({"match": {"content": query}}) + if content_type: + must_clauses.append({"term": {"content_type": content_type}}) + if language: + must_clauses.append({"term": {"language": language}}) + + body = { + "query": { + "bool": { + "must": must_clauses, + "filter": [ + {"bool": { + "should": [ + {"term": {"owner_id": user.id}}, + {"terms": {"id": [paste.id for paste in user.shared_pastes]}}, + {"term": {"private": False}} + ] + }} + ] + } + } + } + results = es.search(index="pastes", body=body) + hits = results["hits"]["hits"] + + pastes = [ + { + "id": hit["_id"], + "url": f"{request.host_url.rstrip('/')}/paste/{hit['_id']}", + "content_type": hit["_source"].get("content_type", ""), + "language": hit["_source"].get("language", ""), + } + for hit in hits + ] + + return render_template('search_results.html', pastes=pastes, query=query, content_type=content_type, language=language) + + except Exception as e: + current_app.logger.error(f"Error in web search: {e}") + return render_template('error.html', message="Search failed"), 500 + + @app.route('/api/paste//favorite', methods=['POST']) + @jwt_required + def api_add_to_favorites(id): + try: + # Usuario autenticado + user = request.user + paste = Paste.query.get_or_404(id) + + # Verificar si ya es favorito + if paste in user.favorite_pastes: + return jsonify({"message": "Paste already in favorites"}), 200 + + # Añadir a favoritos + user.favorite_pastes.append(paste) + db.session.commit() + return jsonify({"message": "Paste added to favorites"}), 201 + + except Exception as e: + logging.error(f"Error adding paste to favorites: {e}") + db.session.rollback() + return jsonify({"error": str(e)}), 500 + @app.route('/api/paste//unfavorite', methods=['POST']) + @jwt_required + def api_remove_from_favorites(id): + try: + # Usuario autenticado + user = request.user + paste = Paste.query.get_or_404(id) + + # Verificar si no está en favoritos + if paste not in user.favorite_pastes: + return jsonify({"message": "Paste is not in favorites"}), 200 + + # Quitar de favoritos + user.favorite_pastes.remove(paste) + db.session.commit() + return jsonify({"message": "Paste removed from favorites"}), 200 + + except Exception as e: + logging.error(f"Error removing paste from favorites: {e}") + db.session.rollback() + return jsonify({"error": str(e)}), 500 + + @app.route('/api/paste//download', methods=['GET']) + @jwt_required + def api_download_paste_file(id): + user = request.user # Usuario autenticado mediante JWT + paste = Paste.query.get_or_404(id) + + # Verificar permisos + if paste.owner_id != user.id: + return jsonify({"error": "You do not have permission to download this file"}), 403 + + # Verificar que el paste tiene un archivo asociado + if not paste.filename: + return jsonify({"error": "This paste does not have an associated file"}), 400 + + # Construir la ruta completa del archivo + file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], paste.filename) + + # Verificar que el archivo exista + if not os.path.exists(file_path): + return jsonify({"error": "The file does not exist"}), 404 + + # Determinar el tipo MIME del archivo + mime_type = paste.content_type or mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + + # Enviar el archivo con el nombre original y tipo MIME + return send_file( + file_path, + as_attachment=True, + download_name=paste.filename, # Flask >= 2.0 + mimetype=mime_type + ) + + @app.route('/api/favorites', methods=['GET']) + @jwt_required + def api_list_favorites(): + try: + # Obtener el usuario autenticado + user = request.user + + # Obtener todos los favoritos del usuario + favorite_pastes = user.favorite_pastes # Relación definida en el modelo User + + if not favorite_pastes: + return jsonify({"message": "No favorites found"}), 200 + + base_url = request.host_url.rstrip('/') + response_list = [] + for paste in favorite_pastes: + response_list.append({ + "id": paste.id, + "url": f"{base_url}/paste/{paste.id}", + "title": paste.title or paste.filename, + "type": paste.get_type(), # Método definido en tu modelo Paste + "size": paste.size or 0, # Devuelve 0 si el tamaño no está definido + "created_at": paste.created_at.strftime('%Y-%m-%d %H:%M:%S') if paste.created_at else None + }) + + return jsonify(response_list), 200 + except Exception as e: + logging.error(f"Error retrieving favorites: {e}") + return jsonify({"error": "Error retrieving favorites"}), 500 + + @app.route('/paste//toggle_editable', methods=['POST']) + @login_required + def toggle_editable(id): + paste = Paste.query.get_or_404(id) + + # Verificar que el usuario es el propietario + if paste.owner_id != current_user.id: + return jsonify({"error": "No tienes permiso para modificar este paste"}), 403 + + try: + # Toggle el estado de editable + paste.editable = not paste.editable + db.session.commit() + + status = "enabled" if paste.editable else "disabled" + message = f"Editable {status} successfully." + return jsonify({"success": True, "editable": paste.editable, "message": message}), 200 + + except Exception as e: + db.session.rollback() + logging.error(f"Error toggling editable: {e}") + return jsonify({"success": False, "error": "Error al actualizar el estado editable"}), 500 + + @app.route('/paste//edit', methods=['GET', 'POST']) + @login_required + def edit_paste_web(id): + paste = Paste.query.get_or_404(id) + + # Verificar permisos de edición + if not paste.has_edit_permission(current_user): + flash("No tienes permiso para editar este paste.", "danger") + return redirect(url_for('get_paste', id=id)) + + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) if paste.filename else None + + # Manejar GET y POST + if request.method == 'POST': + new_content = request.form.get('content') + if not new_content: + flash("El contenido no puede estar vacío.", "danger") + return redirect(url_for('edit_paste_web', id=id)) + + try: + # 1. Eliminar el paste del índice anterior en Elasticsearch + delete_paste_from_index(paste) + + # 2. Guardar el contenido actualizado en el archivo + with open(file_path, 'w', encoding='utf-8') as f: + f.write(new_content) + + # Actualizar la información del paste en la base de datos + paste.last_edited_at = datetime.utcnow() + db.session.commit() + + # 3. Indexar nuevamente el paste actualizado en Elasticsearch + index_paste(paste) + + flash("Paste actualizado correctamente.", "success") + return redirect(url_for('get_paste', id=id)) + except Exception as e: + db.session.rollback() + flash(f"Ocurrió un error al guardar el paste: {str(e)}", "danger") + return redirect(url_for('edit_paste_web', id=id)) + + # Cargar contenido actual para la edición + current_content = "" + if file_path and os.path.exists(file_path): + try: + with open(file_path, 'r', encoding='utf-8') as f: + current_content = f.read() + except Exception as e: + flash(f"Error al leer el contenido del paste: {str(e)}", "danger") + + return render_template('edit_paste.html', paste=paste, current_content=current_content) + + + @app.route('/paste//share', methods=['POST']) + @login_required + def share_paste(id): + data = request.json + username = data.get('username') + can_edit = data.get('can_edit', False) + + # Buscar al usuario con el que se compartirá el paste + user = User.query.filter_by(username=username).first() + if not user: + return jsonify({"error": "User not found"}), 404 + + # Buscar el paste + paste = Paste.query.get_or_404(id) + if paste.owner_id != current_user.id: + return jsonify({"error": "You do not own this paste"}), 403 + + try: + # Verificar si ya existe el registro + existing_entry = db.session.query(shared_pastes).filter_by( + paste_id=paste.id, + user_id=user.id + ).first() + + if existing_entry: + # Si el registro existe, actualizar el valor de `can_edit` + db.session.execute( + shared_pastes.update() + .where( + (shared_pastes.c.paste_id == paste.id) & + (shared_pastes.c.user_id == user.id) + ) + .values(can_edit=can_edit) + ) + else: + # Si el registro no existe, insertar uno nuevo + stmt = shared_pastes.insert().values( + paste_id=paste.id, + user_id=user.id, + can_edit=can_edit + ) + db.session.execute(stmt) + + db.session.commit() + return jsonify({"message": f"Paste shared with {username}, can_edit: {can_edit}."}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Error sharing paste: {str(e)}"}), 500 + + + @app.route('/paste//check-permissions', methods=['GET']) + @login_required + def check_permissions(id): + paste = Paste.query.get_or_404(id) + if paste.owner_id == current_user.id: + return jsonify({'can_edit': True}), 200 + + shared_paste = SharedPaste.query.filter_by(paste_id=paste.id, user_id=current_user.id).first() + if shared_paste and shared_paste.can_edit: + return jsonify({'can_edit': True}), 200 + + return jsonify({'can_edit': False}), 403 + + @app.route('/api/shared_with_others', methods=['GET']) + @jwt_required + def shared_with_others(): + """ + Devuelve los pastes compartidos por el usuario autenticado con otros usuarios. + """ + try: + user = request.user # Usuario autenticado + + # Obtener los pastes compartidos por el usuario actual con otros + shared_pastes_query = db.session.query(Paste).join(shared_pastes).filter( + shared_pastes.c.user_id != user.id, # Excluir al propietario como destinatario + Paste.owner_id == user.id # El usuario es el propietario del paste + ).all() + + # Construir respuesta con información de usuarios (usando joins para obtener el username) + pastes = [] + for paste in shared_pastes_query: + shared_users = db.session.query(User.username, shared_pastes.c.can_edit).join( + shared_pastes, shared_pastes.c.user_id == User.id + ).filter( + shared_pastes.c.paste_id == paste.id, + shared_pastes.c.user_id != user.id # Excluir al propietario + ).all() + + shared_with_list = [ + {"username": shared_user.username, "can_edit": shared_user.can_edit} + for shared_user in shared_users + ] + + pastes.append({ + "id": paste.id, + "title": paste.title or "Untitled", + "created_at": paste.created_at.strftime('%Y-%m-%d %H:%M:%S'), + "shared_with": shared_with_list + }) + + return jsonify({"shared_with_others": pastes}), 200 + except Exception as e: + logging.error(f"Error retrieving shared_with_others: {e}") + return jsonify({"error": "Error retrieving shared_with_others"}), 500 + + + @app.route('/api/shared_with_me', methods=['GET']) + @jwt_required + def shared_with_me(): + """ + Devuelve los pastes compartidos con el usuario autenticado. + """ + try: + user = request.user # Usuario autenticado + + # Obtener los pastes compartidos con el usuario actual + shared_pastes_query = db.session.query(Paste).join(shared_pastes).filter( + shared_pastes.c.user_id == user.id # Pastes compartidos con el usuario + ).all() + + # Construir respuesta + pastes = [ + { + "id": paste.id, + "title": paste.title or "Untitled", + "created_at": paste.created_at.strftime('%Y-%m-%d %H:%M:%S'), + "owner": paste.owner.username, + "can_edit": db.session.query(shared_pastes).filter_by( + paste_id=paste.id, user_id=user.id + ).first().can_edit + } + for paste in shared_pastes_query + ] + + return jsonify({"shared_with_me": pastes}), 200 + except Exception as e: + logging.error(f"Error retrieving shared_with_me: {e}") + return jsonify({"error": "Error retrieving shared_with_me"}), 500 + + + @app.route('/api/paste//share', methods=['POST']) + @jwt_required # Asegúrate de usar el decorador correcto + def add_share_paste(paste_id): + try: + # Obtener el usuario actual + current_user = request.user + if not current_user: + return jsonify({"error": "User not found in request"}), 401 + + # Verificar que el paste pertenece al usuario autenticado + paste = Paste.query.filter_by(id=paste_id, owner_id=current_user.id).first() + if not paste: + return jsonify({"error": "Paste not found or you do not own it"}), 404 + + # Obtener datos del cuerpo de la solicitud + data = request.get_json() + if not data: + return jsonify({"error": "Invalid JSON data"}), 400 + + username = data.get("username") + can_edit = data.get("can_edit", False) + + if not username: + return jsonify({"error": "Username is required"}), 400 + + # Prevenir que el usuario comparta el paste consigo mismo + if username.lower() == current_user.username.lower(): + return jsonify({"error": "You cannot share a paste with yourself"}), 400 + + # Verificar que el usuario destinatario existe (sin sensibilidad a mayúsculas) + recipient = User.query.filter(func.lower(User.username) == username.lower()).first() + if not recipient: + return jsonify({"error": f"User '{username}' not found"}), 404 + + # Verificar si ya se ha compartido el paste con este usuario + existing_share = db.session.query(shared_pastes).filter( + shared_pastes.c.paste_id == paste_id, + shared_pastes.c.user_id == recipient.id + ).first() + + if existing_share: + return jsonify({"error": f"Paste is already shared with '{username}'"}), 400 + + # Asegurarse de que can_edit es un booleano + if not isinstance(can_edit, bool): + return jsonify({"error": "can_edit must be a boolean value"}), 400 + + # Añadir el registro en shared_pastes + db.session.execute(shared_pastes.insert().values( + paste_id=paste_id, + user_id=recipient.id, + can_edit=can_edit + )) + db.session.commit() + + # Registrar la acción para auditoría + app.logger.info(f"User '{current_user.username}' shared paste ID {paste_id} with '{recipient.username}' with can_edit={can_edit}") + + return jsonify({"message": f"Paste shared successfully with '{username}'", "can_edit": can_edit}), 200 + + except SQLAlchemyError as e: + db.session.rollback() + app.logger.error(f"Database error while sharing paste: {e}") + return jsonify({"error": "Database error", "details": str(e)}), 500 + except Exception as e: + app.logger.error(f"Unexpected error while sharing paste: {e}") + return jsonify({"error": "An unexpected error occurred", "details": str(e)}), 500 + + + @app.route('/api/paste//unshare', methods=['POST']) + @jwt_required + def unshare_paste(paste_id): + try: + # Verificar que el usuario autenticado está configurado correctamente + current_user = request.user + if not current_user: + return jsonify({"error": "User not found in request"}), 401 + + # Verificar que el paste pertenece al usuario autenticado + paste = Paste.query.filter_by(id=paste_id, owner_id=current_user.id).first() + if not paste: + return jsonify({"error": "Paste not found or you do not own it"}), 404 + + # Obtener datos del cuerpo de la solicitud + data = request.get_json() + username = data.get("username") + + if not username: + return jsonify({"error": "Username is required"}), 400 + + # Verificar que el usuario destinatario existe + recipient = User.query.filter_by(username=username).first() + if not recipient: + return jsonify({"error": f"User {username} not found"}), 404 + + # Verificar si el paste está compartido con este usuario + existing_share = db.session.query(shared_pastes).filter( + shared_pastes.c.paste_id == paste_id, + shared_pastes.c.user_id == recipient.id + ).scalar() + + if not existing_share: + return jsonify({"error": f"Paste is not shared with {username}"}), 400 + + # Eliminar el registro en shared_pastes + db.session.execute( + shared_pastes.delete().where( + (shared_pastes.c.paste_id == paste_id) & + (shared_pastes.c.user_id == recipient.id) + ) + ) + db.session.commit() + + return jsonify({"message": f"Paste unshared successfully from {username}"}), 200 + except Exception as e: + return jsonify({"error": "An unexpected error occurred", "details": str(e)}), 500 + + @app.route('/paste//unshare', methods=['POST']) + @login_required + def unshare_paste_web(paste_id): + """ + Versión con sesión/logueo normal para 'descompartir' un paste. + """ + try: + # Tomar el username del formulario o del JSON (como prefieras) + # Si lo envías desde un fetch con JSON, usas request.get_json() + # Si lo envías desde un formulario
, usas request.form + data = request.get_json() or {} + username = data.get("username") + + if not username: + return jsonify({"error": "Username is required"}), 400 + + # Verificar que el paste pertenece al usuario logueado + paste = Paste.query.filter_by(id=paste_id, owner_id=current_user.id).first() + if not paste: + return jsonify({"error": "Paste not found or you do not own it"}), 404 + + # Verificar que el usuario con el que se compartió existe + recipient = User.query.filter_by(username=username).first() + if not recipient: + return jsonify({"error": f"User {username} not found"}), 404 + + # Verificar si está compartido + existing_share = db.session.query(shared_pastes).filter( + (shared_pastes.c.paste_id == paste_id), + (shared_pastes.c.user_id == recipient.id) + ).scalar() + + if not existing_share: + return jsonify({"error": f"Paste is not shared with {username}"}), 400 + + # Eliminar el registro de shared_pastes + db.session.execute( + shared_pastes.delete().where( + (shared_pastes.c.paste_id == paste_id) & + (shared_pastes.c.user_id == recipient.id) + ) + ) + db.session.commit() + + return jsonify({"message": f"Paste unshared from {username} successfully"}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({"error": f"An unexpected error occurred: {str(e)}"}), 500 + + @app.route('/contact', methods=['GET', 'POST']) + def contact(): + if request.method == 'POST': + email = request.form.get('email') + subject = request.form.get('subject') or "Contact Form" + message = request.form.get('message') + + # Validar campos básicos + if not email or not message: + flash("Email and message are required.", "danger") + return redirect(url_for('contact')) + + # Lógica de envío de correo (idéntica a la que ya tienes en request_account) + try: + # Construir el mensaje + msg_body = f"Message from Contact Form:\n\nEmail: {email}\nSubject: {subject}\nMessage: {message}" + msg = MIMEText(msg_body) + msg['Subject'] = subject + msg['From'] = SMTP_USERNAME # El remitente que tienes configurado + msg['To'] = SMTP_USERNAME # El correo que recibirá el mensaje + + # Conexión SMTP (mismo patrón que request_account) + if SMTP_USE_SSL: + with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as server: + server.login(SMTP_USERNAME, SMTP_PASSWORD) + server.send_message(msg) + else: + with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server: + if SMTP_USE_TLS: + server.starttls() + server.login(SMTP_USERNAME, SMTP_PASSWORD) + server.send_message(msg) + + flash("Your message has been sent!", "success") + return redirect(url_for('contact')) + + except smtplib.SMTPException as smtp_error: + flash(f"SMTP error: {smtp_error}", "danger") + return redirect(url_for('contact')) + except Exception as e: + flash(f"An error occurred: {e}", "danger") + return redirect(url_for('contact')) + + # Si es GET, renderizar plantilla con el formulario + return render_template('contact.html') + + @app.route('/api/paste/', methods=['PUT']) + @jwt_required + def update_paste_file(id): + paste = Paste.query.get_or_404(id) + user = request.user # Usuario autenticado + + # Verificar permisos + if not paste.has_edit_permission(user): + return jsonify({"error": "No permission to edit this paste."}), 403 + + data = request.json or {} + new_content = data.get('content') + if not new_content: + return jsonify({"error": "Missing 'content' in JSON"}), 400 + + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + try: + # 1. Eliminar de Elasticsearch (opcional) + delete_paste_from_index(paste) + + # 2. Guardar contenido en el archivo + with open(file_path, 'w', encoding='utf-8') as f: + f.write(new_content) + + # 3. Actualizar la fecha de edición + paste.last_edited_at = datetime.utcnow() + db.session.commit() + + # 4. Indexar de nuevo + index_paste(paste) + + return jsonify({"message": "Paste updated successfully"}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({"error": str(e)}), 500 + + @app.route('/api/users', methods=['GET']) + @jwt_required + def list_users(): + try: + users = User.query.with_entities(User.username).all() + usernames = [user.username for user in users] + return jsonify({"users": usernames}), 200 + except Exception as e: + app.logger.error(f"Error retrieving users: {e}") + return jsonify({"error": "An unexpected error occurred."}), 500 + + + @app.route('/api/users/search', methods=['GET']) + @login_required # Asegura que solo usuarios autenticados puedan acceder + def search_users(): + query = request.args.get('q', '').strip() + logging.info(f"Search query received: {query}") + if not query: + return jsonify([]) # Retorna una lista vacía si la consulta está vacía + + # Obtener todos los nombres de usuario desde la base de datos + all_users = [user.username for user in User.query.all()] + logging.info(f"Total users fetched: {len(all_users)}") + + # Realizar una búsqueda difusa utilizando RapidFuzz + matches = process.extract(query, all_users, scorer=fuzz.WRatio, limit=10) + logging.info(f"Matches found: {matches}") + + # Definir un umbral para filtrar resultados poco relevantes + threshold = 60 # Puedes ajustar este valor según tus necesidades + + # Filtrar los resultados que superen el umbral + matched_users = [username for username, score, _ in matches if score >= threshold] + logging.info(f"Matched users after threshold: {matched_users}") + + # Crear una lista de diccionarios para retornar + user_list = [{'username': username} for username in matched_users] + + return jsonify(user_list) + + @app.route('/paste//save-edited', methods=['POST']) + @login_required + def save_edited_image(paste_id): + paste = Paste.query.get(paste_id) + if not paste or paste.owner_id != current_user.id: + return jsonify({"error": "Unauthorized"}), 403 + + data = request.get_json() + if not data or "image" not in data: + return jsonify({"error": "Invalid data"}), 400 + + image_data = data["image"].split(",")[1] # Extraer datos base64 + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + + with open(file_path, "wb") as f: + f.write(base64.b64decode(image_data)) + + return jsonify({"message": "Image updated successfully!"}), 200 + + @app.route('/paste//remove_gps', methods=['POST']) + @login_required + def remove_gps_metadata(paste_id): + paste = Paste.query.get(paste_id) + if not paste or not paste.filename: + return jsonify({"error": "Paste not found"}), 404 + + if paste.owner_id != current_user.id: + return jsonify({"error": "You do not have permission to modify this file"}), 403 + + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + try: + img = Image.open(file_path) + + # Obtener los metadatos EXIF + exif_data = img._getexif() + orientation_tag = None + + if exif_data: + for tag, value in exif_data.items(): + tag_name = ExifTags.TAGS.get(tag, tag) + if tag_name == "Orientation": + orientation_tag = value + break + + # Corregir la orientación antes de eliminar EXIF + if orientation_tag: + if orientation_tag == 3: + img = img.rotate(180, expand=True) + elif orientation_tag == 6: + img = img.rotate(270, expand=True) + elif orientation_tag == 8: + img = img.rotate(90, expand=True) + + # Convertir imagen a RGB para evitar problemas de formato + img = img.convert("RGB") + + # Crear una nueva imagen sin metadatos + temp_file_path = file_path.replace(".jpg", "_clean.jpg") + img.save(temp_file_path, format="JPEG") + + # Reemplazar la imagen original con la nueva sin EXIF + os.replace(temp_file_path, file_path) + + print(f"[INFO] Se eliminaron los metadatos EXIF y se corrigió la orientación de {file_path}") + + return jsonify({"success": True}), 200 + except Exception as e: + print(f"[ERROR] No se pudo eliminar EXIF de {file_path}: {e}") + return jsonify({"error": f"Error removing GPS: {e}"}), 500 + + + @app.route('/api/removegps', methods=['POST']) + @jwt_required + def api_remove_gps_metadata(): + data = request.json + paste_id = data.get("paste_id") + + if not paste_id: + return jsonify({"error": "Missing paste_id"}), 400 + + paste = Paste.query.get(paste_id) + if not paste or not paste.filename: + return jsonify({"error": "Paste not found"}), 404 + + # Verificar si el usuario autenticado es el propietario del archivo + if paste.owner_id != request.user.id: + return jsonify({"error": "You do not have permission to modify this file"}), 403 + + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + try: + img = Image.open(file_path) + + # Leer los metadatos EXIF y corregir la orientación + exif_data = img._getexif() + orientation_tag = None + + if exif_data: + for tag, value in exif_data.items(): + tag_name = ExifTags.TAGS.get(tag, tag) + if tag_name == "Orientation": + orientation_tag = value + break + + if orientation_tag: + if orientation_tag == 3: + img = img.rotate(180, expand=True) + elif orientation_tag == 6: + img = img.rotate(270, expand=True) + elif orientation_tag == 8: + img = img.rotate(90, expand=True) + + # Convertir a RGB para evitar problemas de formato + img = img.convert("RGB") + + # Guardar la imagen sin EXIF en un archivo temporal + temp_file_path = file_path.replace(".jpg", "_clean.jpg") + img.save(temp_file_path, format="JPEG") + + # Reemplazar la imagen original con la nueva sin metadatos + os.replace(temp_file_path, file_path) + + print(f"[INFO] API: Metadatos EXIF eliminados de {file_path}") + + return jsonify({"success": True}), 200 + except Exception as e: + print(f"[ERROR] API: No se pudo eliminar EXIF de {file_path}: {e}") + return jsonify({"error": f"Error removing GPS: {e}"}), 500 + + + @app.route('/user/paste/stats', methods=['GET']) + @login_required + def user_paste_stats(): + text_count = Paste.query.filter( + Paste.owner_id == current_user.id, + Paste.content_type.like('text%') + ).count() + + file_count = Paste.query.filter( + Paste.owner_id == current_user.id, + Paste.content_type.in_([ + 'application/zip', 'application/x-tar', + 'application/gzip', 'application/x-bzip2', + 'application/x-7z-compressed', 'application/x-rar-compressed' + ]) + ).count() + + media_count = Paste.query.filter( + Paste.owner_id == current_user.id, + Paste.content_type.like('image%') | + Paste.content_type.like('video%') | + Paste.content_type.like('audio%') + ).count() + + return jsonify({ + "languageLabels": ["Text", "Files", "Media"], + "textCounts": [text_count], + "fileCounts": [file_count], + "mediaCounts": [media_count] + }) + + @app.route('/generate_code_from_paste/', methods=['GET']) + def generate_code_from_paste(id): + """ + Generates code improvement suggestions from the current paste using DeepSeek-Chat with streaming. + """ + paste = Paste.query.get(id) + if not paste: + return jsonify({"error": "Paste not found"}), 404 + + # Read paste content + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + paste_content = f.read() + + # Limit content size to avoid exceeding token limits + truncated_content = paste_content[:1500] + + # Remove special characters + cleaned_content = re.sub(r"[^\x00-\x7F]+", "", truncated_content).strip() + + # Prompt in English to ensure DeepSeek responds in English + messages = [ + {"role": "system", "content": "You are an AI coding assistant. Your task is to improve, optimize, and explain the following code."}, + {"role": "user", "content": f"Analyze and improve this code:\n\n```{cleaned_content}```\n\nProvide a well-formatted output with explanations if necessary."} + ] + + payload = { + "model": "deepseek-chat", + "messages": messages, + "temperature": 0.3, + "max_tokens": 3000, + "stream": True # Enable streaming mode + } + + # ✅ Pasamos `payload` como argumento a `stream_response()` + return Response(stream_with_context(stream_response(payload)), content_type="text/plain; charset=utf-8") diff --git a/src/routes.py_backup b/src/routes.py_backup new file mode 100644 index 0000000..aa1a9fc --- /dev/null +++ b/src/routes.py_backup @@ -0,0 +1,2586 @@ +import smtplib +from email.mime.text import MIMEText +import os +import mimetypes +from flask import request, jsonify, send_from_directory, abort, render_template, redirect, url_for, flash, session +from src.models import db, Paste, User, shared_pastes +from src.auth import generate_token +from config import UPLOAD_FOLDER, SMTP_SERVER, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_USE_TLS, SMTP_USE_SSL, ROLE_STORAGE_LIMITS +from pygments import highlight +from pygments.lexers import guess_lexer, get_lexer_by_name, guess_lexer_for_filename +from pygments.formatters import HtmlFormatter +from pygments.util import ClassNotFound +from sqlalchemy import func +import magic +from werkzeug.utils import secure_filename +import logging +from io import BytesIO +from flask import send_file, Response +import uuid +from pymediainfo import MediaInfo +import secrets +from sqlalchemy.exc import SQLAlchemyError +from pygments.styles import get_all_styles +from datetime import datetime +from flask_login import login_required +from flask_login import login_user +from flask_login import logout_user +from werkzeug.security import check_password_hash +from src.auth import jwt_required +from flask_login import current_user +from markdown import markdown +from collections import defaultdict +import jwt +from flask import current_app +from elasticsearch import Elasticsearch +from datetime import datetime +from src.models import Favorite +from config import UPLOAD_FOLDER +from rapidfuzz import process, fuzz +from sqlalchemy import or_ +import json +from datetime import datetime, timedelta + +es = Elasticsearch(hosts=["http://elasticsearch:9200"]) + +def register_error_handlers(app): + @app.errorhandler(404) + def not_found_error(error): + return render_template("errors/404.html"), 404 + + @app.errorhandler(500) + def internal_error(error): + return render_template("errors/500.html"), 500 + + @app.errorhandler(403) + def forbidden_error(error): + return render_template("errors/403.html"), 403 + +def delete_expired_pastes(): + """Elimina los pastes expirados de la base de datos y Elasticsearch.""" + now = datetime.utcnow() + + # Obtener pastes expirados + expired_pastes = Paste.query.filter(Paste.expires_at < now).all() + + if not expired_pastes: + print("[INFO] No expired pastes found.") + return + + for paste in expired_pastes: + try: + # Eliminar de Elasticsearch + es.delete(index="pastes", id=paste.id, ignore=[404]) + print(f"[INFO] Deleted paste {paste.id} from Elasticsearch.") + except Exception as e: + print(f"[ERROR] Failed to delete paste {paste.id} from Elasticsearch: {e}") + + # Eliminar de la base de datos + delete_paste_from_index(paste) + db.session.delete(paste) + + db.session.commit() + print(f"[INFO] Deleted {len(expired_pastes)} expired pastes from the database.") + + +def calculate_storage_used(user_id): + """ + Calcula el almacenamiento utilizado por el usuario. + + :param user_id: ID del usuario. + :return: Almacenamiento utilizado en MB. + """ + total = db.session.query(db.func.sum(Paste.size)).filter_by(user_id=user_id).scalar() + total = total if total else 0 + mb = total / (1024 ** 2) # Convertir bytes a MB + return mb + + + +def get_shared_pastes(user, paste_filters=None, page=1, per_page=10): + """ + Obtiene los pastes compartidos con el usuario especificado, aplicando filtros si es necesario. + + :param user: Objeto de usuario actual. + :param paste_filters: Lista de filtros adicionales para aplicar a la consulta. + :param page: Número de página para la paginación. + :param per_page: Número de elementos por página. + :return: Objeto de paginación con los pastes compartidos. + """ + query = Paste.query.join(shared_pastes).filter(shared_pastes.c.user_id == user.id) + + if paste_filters: + query = query.filter(*paste_filters) + + query = query.order_by(Paste.created_at.desc()) + + pagination = query.paginate(page=page, per_page=per_page, error_out=False) + + return pagination + + +def delete_paste_from_index(paste): + try: + es.delete(index='pastes', id=paste.id) + print(f"Deleted paste {paste.id} from index.") + except Exception as e: + print(f"Error deleting paste {paste.id} from index: {e}") + +def calculate_stats(user_id=None, start_date=None, end_date=None): + query = Paste.query + + if user_id is not None: + query = query.filter_by(user_id=user_id) + + if start_date: + if isinstance(start_date, str): + start_date = datetime.strptime(start_date, '%Y-%m-%d') + query = query.filter(Paste.created_at >= start_date) + + if end_date: + if isinstance(end_date, str): + end_date = datetime.strptime(end_date, '%Y-%m-%d').replace(hour=23, minute=59, second=59) + query = query.filter(Paste.created_at <= end_date) + + pastes = query.all() + + stats = { + "total_pastes": len(pastes), + "total_size": sum(paste.size for paste in pastes), + "total_text_pastes": query.filter(Paste.content_type.like('text/%')).count(), + "total_file_pastes": query.filter(Paste.content_type.notlike('text/%')).count(), + "total_media_pastes": query.filter( + or_( + Paste.content_type.like('image/%'), + Paste.content_type.like('video/%'), + Paste.content_type.like('audio/%') + ) + ).count(), + "total_compressed_pastes": query.filter( + Paste.content_type.in_([ + "application/zip", "application/x-tar", "application/gzip", + "application/x-bzip2", "application/x-7z-compressed", "application/x-rar-compressed" + ]) + ).count(), + "languages": list(set(paste.language for paste in pastes if paste.language)), + "counts_text": [], + "counts_file": [], + "counts_media": [], + "counts_compressed": [], + "pastes": pastes # Lista de pastes + } + + # Agrupar estadísticas por lenguaje y tipo + language_groups = query.with_entities(Paste.language, Paste.content_type).all() + language_counts = {} + for lang, ctype in language_groups: + if lang not in language_counts: + language_counts[lang] = {"text": 0, "file": 0, "media": 0} + if ctype.startswith("text/"): + language_counts[lang]["text"] += 1 + elif ctype.startswith(("image/", "video/", "audio/")): + language_counts[lang]["media"] += 1 + else: + language_counts[lang]["file"] += 1 + + stats["counts_text"] = [language_counts[lang]["text"] for lang in stats["languages"]] + stats["counts_file"] = [language_counts[lang]["file"] for lang in stats["languages"]] + stats["counts_media"] = [language_counts[lang]["media"] for lang in stats["languages"]] + + return stats + + +def index_paste(paste): + try: + file_path = os.path.join("/app/uploads", paste.filename) + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + content = f.read() + + doc = { + "id": paste.id, + "title": paste.title or paste.filename, + "content": content, + "owner_id": paste.owner_id, + "created_at": paste.created_at.isoformat(), + } + es.index(index="pastes", id=paste.id, document=doc) + except Exception as e: + current_app.logger.error(f"Error indexing paste {paste.id}: {e}") + +def get_current_user(): + user_id = session.get('user_id') + if not user_id: + return None + # Supongamos que tienes un modelo User para buscar el usuario + return User.query.get(user_id) + +logging.basicConfig( + level=logging.DEBUG, # Cambiar a INFO si no quieres mensajes de depuración + format="%(asctime)s - %(levelname)s - %(message)s", + handlers=[ + logging.StreamHandler() # Envía los logs a la consola + ] +) + +# Define la función highlight_code +def highlight_code(content, language=None, filename=None): + if language: + try: + lexer = get_lexer_by_name(language) + except: + lexer = guess_lexer(content) + else: + if filename: + lexer = guess_lexer_for_filename(filename, content) + else: + lexer = guess_lexer(content) + + formatter = HtmlFormatter(linenos=True, cssclass="highlight") + html_code = highlight(content, lexer, formatter) + + return html_code + +MEDIA_MIME_TYPES = ( + 'image/', + 'video/', + 'audio/', + 'application/pdf', +) + +# Tipos MIME basados en texto que no comienzan con 'text/' +TEXT_BASED_APPLICATION_MIME_TYPES = ( + 'application/json', + 'application/javascript', + 'application/xml', + 'application/xhtml+xml', + 'application/sql', + 'text/xml', + # Agrega otros tipos basados en texto según tus necesidades +) + +LANGUAGE_TO_EXTENSION = { + "python": "py", + "javascript": "js", + "java": "java", + "csharp": "cs", + "cpp": "cpp", + "ruby": "rb", + "go": "go", + "html": "html", + "css": "css", + "php": "php", + "swift": "swift", + "kotlin": "kt", + "rust": "rs", + "typescript": "ts", + "bash": "sh", + "plaintext": "txt", + "sql": "sql", + "json": "json", + "yaml": "yaml", + "xml": "xml" +} + +# Mapeo personalizado de extensiones a lenguajes +EXTENSION_TO_LANGUAGE = { + ".sh": "bash", + ".bash": "bash", + ".py": "python", + ".json": "json", + ".js": "javascript", + ".ts": "typescript", + ".sql": "sql", + ".html": "html", + ".css": "css", + ".java": "java", + ".c": "c", + ".cpp": "cpp", + ".rb": "ruby", + ".go": "go", + ".png": "image", + ".jpg": "image", + ".jpeg": "image", + ".gif": "image", + ".mp4": "video", + ".avi": "video", + ".mp3": "audio", + ".webm": "video", + ".mov": "video", + ".mkv": "video", + ".pdf": "pdf", + ",log": "plaintext", + # Agrega más mapeos según tus necesidades +} + +FILENAME_TO_LANGUAGE = { + "PKGBUILD": "bash", + # Puedes añadir más archivos específicos aquí + # "INSTALL": "bash", + # "configure": "bash", +} + + +mime = magic.Magic(mime=True, uncompress=True) + +import os +import logging +from pygments.lexers import guess_lexer +from pygments.util import ClassNotFound + +# Definir MIME types y extensiones de archivos comprimidos +COMPRESSED_MIME_TYPES = { + "application/zip": "compressed", + "application/gzip": "compressed", + "application/x-tar": "compressed", + "application/x-bzip2": "compressed", + "application/x-7z-compressed": "compressed" +} + +COMPRESSED_EXTENSIONS = { + ".zip": "compressed", + ".gz": "compressed", + ".tgz": "compressed", + ".tar": "compressed", + ".bz2": "compressed", + ".7z": "compressed" +} + +import os +import logging +from pygments.lexers import guess_lexer +from pygments.util import ClassNotFound + +# Definir MIME types y extensiones de archivos comprimidos +COMPRESSED_MIME_TYPES = { + "application/zip": "compressed", + "application/gzip": "compressed", + "application/x-tar": "compressed", + "application/x-bzip2": "compressed", + "application/x-7z-compressed": "compressed" +} + +COMPRESSED_EXTENSIONS = { + ".zip": "compressed", + ".gz": "compressed", + ".tgz": "compressed", + ".tar": "compressed", + ".bz2": "compressed", + ".7z": "compressed" +} + +def detect_language(content, unique_filename=None, original_filename=None, detected_mime_type=None): + """ + Detecta el lenguaje del contenido utilizando el nombre del archivo original, extensión o MIME type. + + :param content: Contenido del paste (str) o None. + :param unique_filename: Nombre de archivo único (UUID) si aplica. + :param original_filename: Nombre de archivo original (e.g., PKGBUILD) si aplica. + :param detected_mime_type: Tipo MIME detectado por magic. + :return: Lenguaje detectado (str) o 'compressed' si es un archivo comprimido. + """ + + # Verificar si el archivo es comprimido basándose en MIME type + if detected_mime_type and detected_mime_type in COMPRESSED_MIME_TYPES: + logging.info(f"MIME type '{detected_mime_type}' detectado como archivo comprimido.") + return COMPRESSED_MIME_TYPES[detected_mime_type] + + # Verificar si el nombre del archivo original está en FILENAME_TO_LANGUAGE + if original_filename: + base_name = os.path.basename(original_filename) + if base_name.upper() in FILENAME_TO_LANGUAGE: + language = FILENAME_TO_LANGUAGE[base_name.upper()] + logging.info(f"Filename '{base_name}' mapeado a lenguaje '{language}'") + return language + + # Detectar por extensión usando unique_filename + if unique_filename: + ext = os.path.splitext(unique_filename)[1].lower() + if ext in COMPRESSED_EXTENSIONS: + logging.info(f"Extension '{ext}' detectada como archivo comprimido.") + return COMPRESSED_EXTENSIONS[ext] + + if ext in EXTENSION_TO_LANGUAGE: + logging.info(f"Extension '{ext}' mapeada a lenguaje '{EXTENSION_TO_LANGUAGE[ext]}'") + return EXTENSION_TO_LANGUAGE[ext] + + # Si no hay mapeo por extensión, usar magic para detectar el lenguaje + if detected_mime_type: + MIME_TO_LANGUAGE = { + "application/json": "json", + "application/x-shellscript": "bash", + "application/javascript": "javascript", + "text/plain": "plaintext", + "text/xml": "xml", + "text/html": "html", + "text/css": "css", + "text/x-python": "python", + "text/x-c": "c", + "text/x-c++": "cpp", + "text/x-java": "java", + "text/x-php": "php", + "text/x-shellscript": "bash", + "application/sql": "sql", + "application/x-yaml": "yaml", + "application/x-toml": "toml", + "text/x-markdown": "markdown", + "text/markdown": "markdown", + "text/x-lua": "lua", + "text/x-rust": "rust", + "application/x-perl": "perl", + "text/x-ruby": "ruby", + } + if detected_mime_type in MIME_TO_LANGUAGE: + logging.info(f"MIME type '{detected_mime_type}' mapeado a lenguaje '{MIME_TO_LANGUAGE[detected_mime_type]}'") + return MIME_TO_LANGUAGE[detected_mime_type] + + # Fallback a Pygments solo si content no es None + if content: + try: + lexer = guess_lexer(content) + language = lexer.aliases[0] + logging.info(f"Pygments detectó el lenguaje como '{language}'") + return language + except ClassNotFound: + logging.warning("Pygments no pudo detectar el lenguaje. Asignando 'plaintext'") + return "plaintext" + + # Si content es None y no hay mapeo, asignar 'unknown' + logging.warning("Content es None y no hay mapeo de lenguaje. Asignando 'unknown'") + return "unknown" + +def init_routes(app): + @app.route('/file/', methods=['GET']) + def get_file(filename): + file_path = os.path.join(UPLOAD_FOLDER, filename) + + if not os.path.exists(file_path): + abort(404, description="file not found") + + mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + return send_from_directory(UPLOAD_FOLDER, filename, mimetype=mime_type) + + + @app.route('/update-theme', methods=['POST']) + def update_theme(): + data = request.get_json() + theme = data.get('theme') + if theme not in ['light', 'dark']: + return jsonify({'error': 'Invalid theme'}), 400 + session['theme'] = theme + return jsonify({'message': 'Theme updated successfully'}), 200 + + + @app.route('/', methods=['GET']) + def index(): + base_url = request.host_url.rstrip('/') + user_name = None + + # Supongamos que el usuario está guardado en la sesión con su ID + if 'user_id' in session: + user = User.query.get(session['user_id']) # Recuperar el usuario de la base de datos + user_name = user.username if user else None # Obtener el nombre del usuario + + return render_template('index.html', base_url=base_url, user_name=user_name) + + + @app.route('/login', methods=['GET', 'POST']) + def login(): + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + user = User.query.filter_by(username=username).first() + + if user and user.check_password(password): + login_user(user) # Autentica al usuario + flash("Login successful!", "success") + return redirect(url_for('user_dashboard')) + + flash("Invalid username or password", "danger") + return redirect(url_for('login')) + + return render_template('login.html') + + @app.route('/paste//json', methods=['GET']) + @jwt_required + def get_paste_json(id): + paste = Paste.query.get(id) + if not paste: + return jsonify({"error": "Paste not found"}), 404 + + response_data = { + "id": paste.id, + "filename": paste.filename, + "language": paste.language, + "content_type": paste.content_type, + "size": paste.size, + "created_at": paste.created_at.strftime('%Y-%m-%d %H:%M:%S') if paste.created_at else None, + "content": None + } + + # Solo incluye el contenido si existe como atributo + if hasattr(paste, 'content') and paste.content: + response_data["content"] = paste.content + elif paste.filename: + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if os.path.exists(file_path): + with open(file_path, 'r', encoding='utf-8', errors='replace') as f: + response_data["content"] = f.read() + + return jsonify(response_data), 200 + + + @app.route('/paste//raw', methods=['GET']) + def get_paste_raw(id): + paste = Paste.query.get(id) + if not paste: + return jsonify({"error": "Paste not found"}), 404 + + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + # Leer el contenido del archivo para devolverlo como texto sin formato + with open(file_path, 'r', encoding='utf-8', errors='replace') as f: + content = f.read() + + return content, 200, {'Content-Type': 'text/plain; charset=utf-8'} + + + + @app.route('/paste', methods=['POST']) + @jwt_required + def create_paste(): + """Crea un nuevo paste con detección automática de lenguaje, opciones de expiración y soporte para compartir.""" + file_obj = request.files.get('c') + user_language = request.form.get('lang', '').strip().lower() + expire = request.form.get('expire', 'yes').strip().lower() # Normaliza el input + share_with = request.form.getlist('share_with') # Lista de usuarios con los que se comparte + can_edit = request.form.get('can_edit', 'false').strip().lower() == 'true' # Convertir a booleano + + logging.info(f"User provided language: {user_language}") + + if not file_obj: + logging.warning("No file provided for the paste.") + return jsonify({"error": "No content provided"}), 400 + + filename = None + language = None + content_type = None + + # Obtener usuario autenticado + user = request.user + + # Leer el archivo antes de calcular el tamaño + file_content = file_obj.read() + file_size = len(file_content) + file_obj.seek(0) # Resetear el puntero del archivo + + # Validar límites de usuario + if user.role == 'user': + if file_size > 100 * 1024**2: # 100 MB por paste + logging.warning(f"User {user.username} tried to upload a file exceeding 100 MB.") + return jsonify({"error": "File exceeds the 100 MB limit for your role"}), 403 + if user.storage_used + file_size > user.storage_limit: # Límite de almacenamiento total + logging.warning(f"User {user.username} exceeded their total storage limit.") + return jsonify({"error": "You have exceeded your total storage limit"}), 403 + + try: + # Obtener la extensión del archivo + original_filename = file_obj.filename + file_extension = os.path.splitext(original_filename)[1].lower() + + # Generar un nombre único para el archivo + unique_filename = f"{uuid.uuid4().hex}{file_extension}" + file_path = os.path.join(UPLOAD_FOLDER, unique_filename) + + # Leer una muestra del archivo para analizar MIME + file_sample = file_content[:8192] + detected_mime_type = magic.Magic(mime=True).from_buffer(file_sample) + + logging.info(f"Detected MIME type: {detected_mime_type}") + logging.info(f"Original filename: {original_filename}, Extension: {file_extension}, Size: {file_size} bytes") + content_type = detected_mime_type + + # Guardar el archivo en el sistema de archivos + with open(file_path, 'wb') as f: + f.write(file_content) + logging.info(f"Saved paste content to: {file_path}") + + # **Corrección del problema `lang=yes/no`** + if user_language in ["yes", "no", ""]: + logging.warning(f"Invalid or missing language '{user_language}', using detection.") + language = detect_language( + content=file_content.decode(errors="ignore"), + unique_filename=unique_filename, + original_filename=original_filename, + detected_mime_type=detected_mime_type + ) + else: + language = user_language + logging.info(f"User specified language: {language}") + + filename = unique_filename + + # Actualizar el almacenamiento usado por el usuario + user.storage_used += file_size + db.session.commit() + + except Exception as e: + logging.error(f"Exception occurred: {str(e)}") + return jsonify({"error": f"Error processing the content: {str(e)}"}), 500 + + # **Manejo de Expiración** + expires_at = datetime.utcnow() + timedelta(days=1) if expire == 'yes' else None + + # **Crear registro del paste en la base de datos** + paste = Paste( + content_type=content_type, + filename=filename, + owner_id=user.id, + user_id=user.id, + language=language, + size=file_size, + expires_at=expires_at + ) + + try: + db.session.add(paste) + db.session.commit() + logging.info(f"Saved paste with ID: {paste.id}") + + # **Indexar contenido en Elasticsearch** + index_paste(paste) + + # **Compartir paste con otros usuarios** + shared_users = [] + if share_with: + for username in share_with: + shared_user = User.query.filter_by(username=username).first() + if shared_user and shared_user.id != user.id: + db.session.execute(shared_pastes.insert().values( + paste_id=paste.id, + user_id=shared_user.id, + can_edit=can_edit + )) + shared_users.append(username) + + db.session.commit() + logging.info(f"Paste {paste.id} shared with: {shared_users} (Editable: {can_edit})") + + except Exception as e: + db.session.rollback() + logging.error(f"Error saving paste: {str(e)}") + return jsonify({"error": f"Error saving paste: {str(e)}"}), 500 + + base_url = request.host_url.rstrip('/') + paste_url = f"{base_url}/paste/{paste.id}" + + logging.info(f"Paste created successfully. ID: {paste.id}, URL: {paste_url}") + return jsonify({ + "url": paste_url, + "language": language, + "expires_at": expires_at, + "shared_with": shared_users, + "can_edit": can_edit + }), 201 + + + @app.route('/paste/', methods=['GET']) + def get_paste(id): + paste = Paste.query.get(id) + if not paste: + logging.warning(f"Paste ID {id} not found.") + return render_template("errors/404.html", message="This paste has been deleted or has expired."), 404 + if current_user.is_authenticated: + can_edit = paste.has_edit_permission(current_user) + else: + can_edit = False + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) if paste.filename else None + + # Recuperar usuarios con los que se compartió el paste (solo para el propietario) + shared_with = [] + if current_user.is_authenticated and paste.owner_id == current_user.id: + shared_with = db.session.query(User.username, shared_pastes.c.can_edit).join( + shared_pastes, User.id == shared_pastes.c.user_id + ).filter(shared_pastes.c.paste_id == paste.id).all() + # Definir los tipos MIME que consideras como binarios + BINARIO_MIME_TYPES = ( + 'font/', + 'model/', + # Agrega más tipos MIME según tus necesidades + ) + + # Tipos MIME basados en texto que no comienzan con 'text/' + TEXT_BASED_APPLICATION_MIME_TYPES = ( + 'application/json', + 'application/javascript', + 'application/xml', + 'application/xhtml+xml', + 'application/sql', + # Agrega otros tipos basados en texto según tus necesidades + ) + + # Tipos MIME multimedia + MEDIA_MIME_TYPES = ( + 'image/', + 'video/', + 'audio/', + 'application/pdf', + ) + + # Función para verificar si el MIME type es binario + def is_binary(mime_type): + return any(mime_type.startswith(prefix) for prefix in BINARIO_MIME_TYPES) + + # Función para verificar si el MIME type es multimedia + def is_media(mime_type): + return any(mime_type.startswith(prefix) for prefix in MEDIA_MIME_TYPES) + + # Determinar el tipo de contenido + if paste.content_type.startswith('text/') or paste.content_type in TEXT_BASED_APPLICATION_MIME_TYPES: + es_binario = False + es_media = False + elif is_media(paste.content_type): + es_binario = False + es_media = True + elif is_binary(paste.content_type): + es_binario = True + es_media = False + else: + # Por defecto, tratar como binario si no coincide con las anteriores + es_binario = True + es_media = False + + logging.debug(f"Content Type: {paste.content_type}") + logging.debug(f"Is binary: {es_binario}") + logging.debug(f"Is media: {es_media}") + + if es_binario: + if not paste.filename or not os.path.exists(file_path): + logging.error(f"File for paste ID {id} not found: {file_path}") + return jsonify({"error": "File not found"}), 404 + + try: + # Obtener información adicional + owner = User.query.get(paste.owner_id) + file_size = paste.size # Asegúrate de que el modelo Paste tenga el campo 'size' + + return render_template( + 'binary_view.html', + paste=paste, + filename=paste.filename, + mime_type=paste.content_type, + owner=owner, + size=file_size + ) + except Exception as e: + logging.error(f"Error while rendering binary view for paste ID {id}: {e}") + return jsonify({"error": "Error while processing the file"}), 500 + + elif es_media: + if not paste.filename or not os.path.exists(file_path): + logging.error(f"File for paste ID {id} not found: {file_path}") + return jsonify({"error": "File not found"}), 404 + + try: + # Extraer metadatos si es necesario + metadata = [] + if paste.content_type.startswith("image/"): + logging.info(f"Extracting metadata for image: {file_path}") + media_info = MediaInfo.parse(file_path) + for track in media_info.tracks: + track_data = {key: value for key, value in track.to_data().items() if value} + if track_data: + metadata.append(track_data) + + if not metadata: + metadata.append({"Info": "No metadata found in image file"}) + + elif paste.content_type.startswith(("video/", "audio/")): + logging.info(f"Extracting metadata for media: {file_path}") + media_info = MediaInfo.parse(file_path) + for track in media_info.tracks: + track_data = {key: value for key, value in track.to_data().items() if value} + if track_data: + metadata.append(track_data) + + if not metadata: + metadata.append({"Info": "No metadata found in media file"}) + + elif paste.content_type == "application/pdf": + logging.info(f"Extracting metadata for PDF: {file_path}") + # Implementa la extracción de metadatos para PDFs si es necesario + + return render_template( + 'media_view.html', + filename=paste.filename, + mime_type=paste.content_type, + metadata=metadata, + paste_id=paste.id + ), 200 + + except Exception as e: + logging.error(f"Error while rendering media view for paste ID {id}: {e}") + return jsonify({"error": "Error while processing the media file"}), 500 + + elif not es_binario and not es_media: + # Manejo de tipos de contenido soportados (texto) + try: + with open(file_path, 'r', encoding='utf-8', errors='replace') as f: + content = f.read() + + # Determinar si el contenido es Markdown + is_markdown = False + + # 1. Basado en la extensión del archivo + if paste.filename and paste.filename.lower().endswith('.md'): + is_markdown = True + logging.info("Detected Markdown based on file extension (.md).") + + # 2. Basado en el lenguaje especificado + elif paste.language and paste.language.lower() in ['md', 'markdown']: + is_markdown = True + logging.info("Detected Markdown based on specified language.") + + # 3. Basado en Tipo MIME + elif paste.content_type == 'text/markdown': + is_markdown = True + logging.info("Detected Markdown based on MIME type (text/markdown).") + + if is_markdown: + # Convertir Markdown a HTML usando python-markdown con extensiones + md_html = markdown( + content, + extensions=[ + 'tables', # Soporte para tablas + 'fenced_code', # Soporte para bloques de código con ``` + 'codehilite' # Soporte para resaltado de sintaxis con Pygments + ] + ) + logging.info("Converted Markdown to HTML.") + + return render_template( + 'text_paste.html', + paste=paste, + md_html_code=md_html, # Pasamos el HTML generado + is_markdown=True, + can_edit=can_edit, + shared_with=shared_with + ), 200 + else: + # Contenido no es Markdown -> resaltar con Pygments normal + html_code = highlight_code( + content, language=paste.language, filename=paste.filename + ) + logging.info("Converted text content with Pygments.") + + return render_template( + 'text_paste.html', + paste=paste, + html_code=html_code, + is_markdown=False, + can_edit=can_edit, + shared_with=shared_with + ), 200 + + except Exception as e: + logging.error(f"Error reading text file for paste ID {id}: {e}") + return jsonify({"error": "Error processing text file"}), 500 + + else: + # Si el tipo MIME no coincide con ninguno de los anteriores, manejarlo aquí + logging.warning(f"Unhandled content type: {paste.content_type}") + return jsonify({"error": "Unhandled content type"}), 400 + + @app.route('/pastes', methods=['GET']) + @jwt_required + def list_pastes(): + try: + # Obtener todos los pastes del usuario autenticado + user = request.user + pastes = Paste.query.filter_by(owner_id=user.id).all() + + if not pastes: + return jsonify({"message": "No pastes found"}), 200 + + base_url = request.host_url.rstrip('/') + response_list = [] + for paste in pastes: + response_list.append({ + "id": paste.id, + "url": f"{base_url}/paste/{paste.id}", + "title": paste.title or paste.filename, + "type": paste.get_type(), # Método definido en tu modelo Paste + "size": paste.size or 0, # Devuelve 0 si el tamaño no está definido + "created_at": paste.created_at.strftime('%Y-%m-%d %H:%M:%S') if paste.created_at else None, + "is_favorite": paste in user.favorite_pastes # Verifica si el paste es favorito + }) + + return jsonify(response_list), 200 + except Exception as e: + logging.error(f"Error retrieving pastes: {e}") + return jsonify({"error": "Error retrieving pastes"}), 500 + + @app.route('/register', methods=['POST']) + @jwt_required + def register_user(): + authenticated_user = request.user + + if authenticated_user.username != 'admin': + return jsonify({"error": "you have no access rights to register users"}), 403 + + data = request.get_json() + if not data: + return jsonify({"error": "No data provided"}), 400 + + username = data.get('username') + password = data.get('password') + + if not username or not password: + return jsonify({"error": "'username' and 'password' required"}), 400 + + existing_user = User.query.filter_by(username=username).first() + if existing_user: + return jsonify({"error": "El nombre de usuario ya existe"}), 400 + + try: + new_user = User(username=username) + new_user.set_password(password) + db.session.add(new_user) + db.session.commit() + return jsonify({"message": f"Usuario '{username}' registry successful"}), 201 + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Error registering user: {str(e)}"}), 500 + + @app.route('/admin/login', methods=['GET', 'POST']) + def admin_login(): + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + user = User.query.filter_by(username=username).first() + + if user and user.check_password(password) and username == 'admin': + session['admin'] = username + flash('Login successful', 'success') + return redirect(url_for('admin_dashboard')) + + flash('Invalid credentials', 'danger') + return render_template('admin_login.html') + + @app.route('/admin/logout') + def admin_logout(): + session.pop('admin', None) + flash('Logged out', 'info') + return redirect(url_for('admin_login')) + + @app.route('/admin') + def admin_dashboard(): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + users_count = User.query.count() + pastes_count = Paste.query.count() + return render_template('dashboard.html', users_count=users_count, pastes_count=pastes_count) + + @app.route('/admin/users') + def admin_users(): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + users = User.query.all() + return render_template('users.html', users=users) + + @app.route('/admin/users/add', methods=['GET', 'POST']) + def admin_add_user(): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + role = request.form.get('role') # Obtén el rol del formulario + + # Validar datos + if not username or not password or not role: + flash('All fields are required', 'danger') + return redirect(url_for('admin_add_user')) + + # Validar si el rol es válido + valid_roles = ['admin', 'advanced', 'user'] + if role not in valid_roles: + flash('Invalid role selected.', 'danger') + return redirect(url_for('admin_add_user')) + + # Crear usuario + try: + user = User(username=username, role=role, storage_limit=ROLE_STORAGE_LIMITS.get(role)) + user.set_password(password) + db.session.add(user) + db.session.commit() + flash('User added successfully', 'success') + return redirect(url_for('admin_users')) + except Exception as e: + db.session.rollback() + flash(f'Error: {str(e)}', 'danger') + return redirect(url_for('admin_add_user')) + + return render_template('add_user.html') + + @app.route('/admin/users/delete/', methods=['POST']) + def admin_delete_user(user_id): + if 'admin' not in session: + # Enviar JSON de error en lugar de redirigir + return jsonify({"error": "Unauthorized"}), 401 + + user = User.query.get(user_id) + if not user: + return jsonify({"error": "User not found"}), 404 + + db.session.delete(user) + db.session.commit() + return jsonify({"message": "User deleted successfully"}), 200 + + + user = User.query.get(user_id) + if not user: + flash('User not found', 'danger') + else: + db.session.delete(user) + db.session.commit() + flash('User deleted successfully', 'success') + return redirect(url_for('admin_users')) + + @app.route('/admin/pastes') + def admin_pastes(): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + pastes = Paste.query.all() + return render_template('pastes.html', pastes=pastes) + + @app.route('/admin/pastes/delete/', methods=['POST']) + def admin_delete_paste(paste_id): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + paste = Paste.query.get(paste_id) + if not paste: + flash('Paste not found', 'danger') + else: + delete_paste_from_index(paste) + paste.shared_with.clear() + db.session.delete(paste) + db.session.commit() + flash('Paste deleted successfully', 'success') + return redirect(url_for('admin_pastes')) + + @app.route('/stats', methods=['GET']) + def anonymous_stats(): + start_date = request.args.get('start_date') + end_date = request.args.get('end_date') + + stats = calculate_stats(start_date=start_date, end_date=end_date) + + return render_template( + 'anonymous_stats.html', + total_pastes=stats["total_pastes"], + total_text_pastes=stats["total_text_pastes"], + total_file_pastes=stats["total_file_pastes"], + total_media_pastes=stats["total_media_pastes"], + languages=stats["languages"], + counts_text=stats["counts_text"], + counts_file=stats["counts_file"], + counts_media=stats["counts_media"], + ) + + + @app.route('/api/docs') + def redoc(): + return ''' + + + + Pastebin API Docs + + + + + + + + ''' + @app.route('/paste//download', methods=['GET']) + def download_paste(id): + paste = Paste.query.get(id) + if not paste: + return jsonify({"error": "Paste not found"}), 404 + + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + try: + return send_from_directory( + directory=UPLOAD_FOLDER, + path=paste.filename, + as_attachment=True, + download_name=paste.filename, + mimetype=paste.content_type + ) + except TypeError: + # Para Flask < 2.0 + return send_from_directory( + directory=UPLOAD_FOLDER, + path=paste.filename, + as_attachment=True, + attachment_filename=paste.filename, # <--- Distinto nombre de argumento + mimetype=paste.content_type + ) + + + @app.route('/request-account', methods=['GET', 'POST']) + def request_account(): + if request.method == 'POST': + email = request.form.get('email') + username = request.form.get('username') + + # Validate basic input + if not email or not username: + logging.warning("Validation failed: Missing email or username") + flash("Email and username are required.", "danger") + return redirect(url_for('request_account')) + + logging.info(f"Account request initiated. Username: {username}, Email: {email}") + + # Log the SMTP environment variables for debugging + logging.debug(f"SMTP_SERVER={SMTP_SERVER}") + logging.debug(f"SMTP_PORT={SMTP_PORT}") + logging.debug(f"SMTP_USERNAME={SMTP_USERNAME}") + logging.debug(f"SMTP_USE_SSL={SMTP_USE_SSL}, SMTP_USE_TLS={SMTP_USE_TLS}") + + # Send email + try: + msg = MIMEText(f"New account request:\n\nUsername: {username}\nEmail: {email}") + msg['Subject'] = 'Account Request' + msg['From'] = SMTP_USERNAME + msg['To'] = SMTP_USERNAME # Ajusta a tu dirección de correo receptora + + # Connect to the SMTP server + if SMTP_USE_SSL: + with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as server: + logging.debug("Using SMTP over SSL") + server.login(SMTP_USERNAME, SMTP_PASSWORD) + logging.info("SMTP login successful") + server.send_message(msg) + else: + with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server: + if SMTP_USE_TLS: + logging.debug("Starting TLS session") + server.starttls() + server.login(SMTP_USERNAME, SMTP_PASSWORD) + logging.info("SMTP login successful") + server.send_message(msg) + + logging.info("Account request email sent successfully") + flash("Your account request has been submitted successfully!", "success") + return redirect(url_for('index')) + + except smtplib.SMTPException as smtp_error: + logging.error(f"SMTP error: {smtp_error}") + flash(f"SMTP error: {smtp_error}", "danger") + except Exception as general_error: + logging.error(f"An unexpected error occurred: {general_error}") + flash(f"An error occurred: {general_error}", "danger") + + return redirect(url_for('request_account')) + + return render_template('request_account.html') + + @app.route('/user/details', methods=['GET']) + @jwt_required + def get_user_details(): + user = request.user # Usuario autenticado + if not user: + return jsonify({"error": "User not found"}), 404 + + # Calcular contadores + pastes_count = Paste.query.filter_by(owner_id=user.id).count() + favorites_count = user.favorite_pastes.count() + shared_with_me_count = db.session.query(Paste).join(shared_pastes).filter( + shared_pastes.c.user_id == user.id + ).count() + shared_with_others_count = db.session.query(Paste).join(shared_pastes).filter( + Paste.owner_id == user.id, + shared_pastes.c.user_id != user.id + ).count() + + # Construir la respuesta + response = { + "id": user.id, + "username": user.username, + "role": user.role, + "storage_used": user.storage_used, + "storage_limit": user.storage_limit, + "storage_remaining": max(user.storage_limit - user.storage_used, 0) if user.storage_limit != -1 else None, + "theme_preference": user.theme_preference, + # Suponiendo que tienes un campo user.created_at + "created_at": user.created_at.isoformat() if hasattr(user, 'created_at') and user.created_at else None, + "pastes_count": pastes_count, + "favorites_count": favorites_count, + "shared_with_me_count": shared_with_me_count, + "shared_with_others_count": shared_with_others_count + } + + return jsonify(response), 200 + + + @app.route('/admin/users//change_role', methods=['POST']) + def change_user_role(user_id): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + try: + user = User.query.get(user_id) + if not user: + return jsonify({"error": "User not found"}), 404 + + new_role = request.form.get('role') + valid_roles = ['admin', 'advanced', 'user'] + if new_role not in valid_roles: + return jsonify({"error": "Invalid role"}), 400 + + # Actualizar rol y límite de almacenamiento + user.role = new_role + user.storage_limit = ROLE_STORAGE_LIMITS[new_role] + db.session.commit() + + return jsonify({"message": f"User role updated to {new_role}"}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Failed to update role: {str(e)}"}), 500 + + @app.route('/logout') + @login_required + def logout(): + logout_user() + flash("You have been logged out.", "info") + return redirect(url_for('login')) + + @app.route('/user-dashboard', methods=['GET']) + @login_required + def user_dashboard(): + user = current_user # Usuario autenticado automáticamente + + # Obtener el número de página desde los parámetros de consulta, por defecto es 1 + page = request.args.get('page', 1, type=int) + per_page = 10 # Número de pastes por página + favorite_page = request.args.get('favorite_page', 1, type=int) + + # Consultar los pastes del usuario con paginación + pastes_query = Paste.query.filter_by(user_id=user.id).order_by(Paste.created_at.desc()) + pagination = pastes_query.paginate(page=page, per_page=per_page, error_out=False) + pastes = pagination.items + + # Consultar los pastes favoritos del usuario con paginación + favorite_query = Paste.query.filter(Paste.favorites.any(id=user.id)) + favorite_pagination = favorite_query.paginate(page=favorite_page, per_page=10, error_out=False) + favorite_pastes = favorite_pagination.items + + # Consultar los pastes compartidos con el usuario + shared_pastes_list = Paste.query.join(shared_pastes, (shared_pastes.c.paste_id == Paste.id))\ + .filter(shared_pastes.c.user_id == user.id)\ + .all() + + # Calcular estadísticas generales + stats = calculate_stats(user.id) # Asegúrate de que esta función devuelve un diccionario con las claves usadas a continuación + + # **Nuevo Código: Cálculo de Almacenamiento** + + # Obtener el rol del usuario + user_role = user.role.lower() if user.role else 'user' # Asegúrate de que el campo 'role' existe en el modelo User + + # Obtener el límite de almacenamiento basado en el rol + storage_limits = current_app.config.get('ROLE_STORAGE_LIMITS', { + 'admin': -1, # Ilimitado + 'advanced': 2 * 1024**3, # 2GB en bytes + 'user': 1 * 1024**3, # 1GB en bytes + }) + + # Obtener límite en bytes y convertir a MB + storage_limit = storage_limits.get(user_role, 1 * 1024**3) # 1GB en bytes + storage_used = db.session.query(db.func.sum(Paste.size)).filter_by(user_id=user.id).scalar() or 0 + + # Calcular almacenamiento disponible + storage_available = storage_limit - storage_used if storage_limit != -1 else -1 + storage_available = max(storage_available, 0) # Evita valores negativos + + # CONVERSIÓN A MB + storage_used_mb = round(storage_used / (1024**2), 2) + storage_limit_mb = -1 if storage_limit == -1 else round(storage_limit / (1024**2), 2) + storage_available_mb = -1 if storage_available == -1 else round(storage_available / (1024**2), 2) + + # DEBUG LOG PARA COMPROBAR QUE SE CONVIERTE BIEN + app.logger.debug(f"Storage (MB) - Used: {storage_used_mb}, Limit: {storage_limit_mb}, Available: {storage_available_mb}") + + # Información del perfil del usuario (sin email ni bio) + user_profile = { + 'username': user.username, + 'role': user.role.capitalize() if user.role else 'User', + } + + return render_template( + 'user_dashboard.html', + user=user, + total_pastes=stats['total_pastes'], + total_size=stats['total_size'], + pastes=pastes, + pagination=pagination, # Pasar el objeto de paginación al template + total_text_pastes=stats.get('total_text_pastes', 0), + total_file_pastes=stats.get('total_file_pastes', 0), + total_media_pastes=stats.get('total_media_pastes', 0), + languages=stats.get('languages', []), + counts_text=stats.get('counts_text', []), + counts_file=stats.get('counts_file', []), + counts_media=stats.get('counts_media', []), + favorite_pastes=favorite_pastes, + favorite_pagination=favorite_pagination, + shared_pastes=shared_pastes_list, + storage_used=storage_used_mb, + storage_limit=storage_limit_mb, + storage_available=storage_available_mb, + user_profile=user_profile, + ) + + + @app.route('/paste/', methods=['DELETE']) + @jwt_required + def delete_paste(id): + paste = Paste.query.get(id) + if not paste: + return jsonify({"error": "Paste not found"}), 404 + + # Permitir eliminación solo si es el propietario o un administrador + if paste.owner_id != request.user.id and request.user.username != 'admin': + return jsonify({"error": "You do not have permission to delete this paste"}), 403 + + try: + # Eliminar favoritos asociados + paste.favorites.clear() + + # Eliminar el archivo si existe + if paste.filename: + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if os.path.exists(file_path): + os.remove(file_path) + + delete_paste_from_index(paste) + paste.shared_with.clear() + db.session.delete(paste) + db.session.commit() + return jsonify({"message": "Paste deleted successfully"}), 200 + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Error deleting paste: {str(e)}"}), 500 + + + @app.route('/change-password-form', methods=['GET']) + @login_required + def change_password_form(): + return render_template('change_password.html') + + + @app.route('/change-password', methods=['POST']) + @login_required + def change_password(): + current_password = request.form.get('current_password') + new_password = request.form.get('new_password') + confirm_password = request.form.get('confirm_password') + + # Validar que los campos no estén vacíos + if not current_password or not new_password or not confirm_password: + flash("All fields are required.", "danger") + return redirect(url_for('change_password_form')) + + # Verificar que las contraseñas coincidan + if new_password != confirm_password: + flash("New passwords do not match.", "danger") + return redirect(url_for('change_password_form')) + + # Verificar contraseña actual + if not current_user.check_password(current_password): # Método check_password en el modelo User + flash("Current password is incorrect.", "danger") + return redirect(url_for('change_password_form')) + + # Actualizar la contraseña + try: + current_user.set_password(new_password) # Método set_password en el modelo User + db.session.commit() + flash("Password updated successfully.", "success") + except Exception as e: + db.session.rollback() + flash(f"An error occurred: {str(e)}", "danger") + + return redirect(url_for('user_dashboard')) + + @app.context_processor + def inject_pygments_styles(): + styles = list(get_all_styles()) + current_theme = session.get('theme', 'light') # Obtener el tema actual de la sesión + default_pygments_style = 'monokai' if current_theme == 'dark' else 'default' + + # Obtener el nombre del usuario si está logueado + user_name = None + if 'user_id' in session: + user = User.query.get(session['user_id']) + user_name = user.username if user else None + + return dict( + pygments_styles=styles, + default_pygments_style=default_pygments_style, + user_name=user_name + ) + + @app.route('/paste//download_page', methods=['GET']) + def download_page(id): + paste = Paste.query.get(id) + if not paste: + flash("Paste no encontrado", "danger") + return redirect(url_for('index')) + + return render_template('download_page.html', paste=paste) + + @app.route('/paste//download_file', methods=['GET']) + def download_file(id): + logging.info(f"Confirmed download request for paste ID: {id}") + paste = Paste.query.get(id) + if not paste: + logging.warning(f"Paste ID {id} not found.") + return jsonify({"error": "Paste not found"}), 404 + + if paste.filename: # El paste está asociado a un archivo físico + logging.info(f"Handling confirmed file download for paste ID {id} with filename: {paste.filename}") + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + + if not os.path.exists(file_path): + logging.error(f"File {paste.filename} for paste ID {id} not found.") + return jsonify({"error": "File not found"}), 404 + + return send_from_directory( + directory=UPLOAD_FOLDER, + path=paste.filename, + as_attachment=True, + download_name=paste.filename, + mimetype=paste.content_type + ) + + elif paste.content: # El paste es de texto y está almacenado en la base de datos + logging.info(f"Handling confirmed text paste ID {id}") + + language_suffix = f".{paste.language}" if paste.language and paste.language != 'unknown' else ".txt" + filename = f"paste_{paste.id}{language_suffix}" + + file_stream = BytesIO() + file_stream.write(paste.content.encode('utf-8')) + file_stream.seek(0) + return send_file( + file_stream, + as_attachment=True, + download_name=filename, + mimetype='text/plain' + ) + else: + logging.error(f"Paste ID {id} has no associated content.") + return jsonify({"error": "Paste has no associated content"}), 400 + + + @app.route('/paste//stream_download', methods=['GET']) + def stream_download(id): + paste = Paste.query.get(id) + if not paste or not paste.filename: + return jsonify({"error": "Paste or file not found"}), 404 + + def generate(): + with open(os.path.join(UPLOAD_FOLDER, paste.filename), 'rb') as f: + while True: + chunk = f.read(4096) + if not chunk: + break + yield chunk + + response = Response(generate(), mimetype=paste.content_type) + response.headers.set('Content-Disposition', 'attachment', filename=paste.filename) + return response + + @app.route('/user/paste//delete', methods=['POST']) + @login_required + def user_delete_paste(id): + paste = Paste.query.get_or_404(id) + + # Verificar si el usuario es el propietario + if paste.owner_id != current_user.id: + return jsonify({"error": "You do not have permission to delete this paste"}), 403 + + try: + # Eliminar favoritos asociados + paste.favorites.clear() + + # Eliminar el archivo asociado, si existe + if paste.filename: + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if os.path.exists(file_path): + os.remove(file_path) + + # Eliminar el paste de la base de datos + delete_paste_from_index(paste) + db.session.delete(paste) + db.session.commit() + return jsonify({"message": "Paste deleted successfully"}), 200 + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Error deleting paste: {str(e)}"}), 500 + + @app.route('/user//stats', methods=['GET']) + @login_required + def user_stats(username): + if current_user.username != username: + abort(404) + + start_date_str = request.args.get('start_date') + end_date_str = request.args.get('end_date') + + app.logger.debug(f"Start Date: {start_date_str}, End Date: {end_date_str}") + + paste_filters = [] + start_date = None + end_date = None + + try: + if start_date_str: + start_date = datetime.strptime(start_date_str, '%Y-%m-%d') + paste_filters.append(Paste.created_at >= start_date) + if end_date_str: + end_date = datetime.strptime(end_date_str, '%Y-%m-%d').replace(hour=23, minute=59, second=59) + paste_filters.append(Paste.created_at <= end_date) + except ValueError as ve: + app.logger.error(f"Error al parsear las fechas: {ve}") + start_date = None + end_date = None + + # Obtener los pastes y favoritos con filtros aplicados + page = request.args.get('page', 1, type=int) + per_page = 10 + pastes_query = Paste.query.filter_by(user_id=current_user.id).order_by(Paste.created_at.desc()) + if paste_filters: + pastes_query = pastes_query.filter(*paste_filters) + pagination = pastes_query.paginate(page=page, per_page=per_page, error_out=False) + + favorite_query = Paste.query.filter(Paste.favorites.any(id=current_user.id)) + if paste_filters: + favorite_query = favorite_query.filter(*paste_filters) + favorite_pagination = favorite_query.paginate(page=1, per_page=10, error_out=False) + + # Calcular estadísticas + stats = calculate_stats(current_user.id, start_date, end_date) + + # Calcular almacenamiento con valores seguros + storage_used = 0 + storage_limit = -1 + storage_available = -1 + + try: + storage_used = calculate_storage_used(current_user.id) + storage_limit = current_user.get_storage_limit() or -1 + + app.logger.debug(f"current_user: {current_user}, type: {type(current_user)}") + + storage_available = max(storage_limit - storage_used, 0) if storage_limit != -1 else -1 + + # CONVERSIÓN A MB + storage_used_mb = round(storage_used / (1024**2), 2) + storage_limit_mb = -1 if storage_limit == -1 else round(storage_limit / (1024**2), 2) + storage_available_mb = -1 if storage_available == -1 else round(storage_available / (1024**2), 2) + + app.logger.debug(f"Storage (MB) - Used: {storage_used_mb}, Limit: {storage_limit_mb}, Available: {storage_available_mb}") + except Exception as e: + app.logger.error(f"Error al calcular el almacenamiento: {e}") + storage_used_mb = 0 + storage_limit_mb = -1 + storage_available_mb = -1 + + # Obtener pastes compartidos con filtros y paginación + shared_page = request.args.get('shared_page', 1, type=int) + shared_pagination = get_shared_pastes(current_user, paste_filters=paste_filters, page=shared_page, per_page=per_page) + + return render_template( + 'user_dashboard.html', + user=current_user, + user_profile=current_user.role, + total_pastes=stats['total_pastes'], + total_size=stats['total_size'], + total_text_pastes=stats['total_text_pastes'], + total_file_pastes=stats['total_file_pastes'], + total_media_pastes=stats['total_media_pastes'], + languages=stats['languages'], + counts_text=stats['counts_text'], + counts_file=stats['counts_file'], + counts_media=stats['counts_media'], + pastes=pagination.items, + pagination=pagination, + favorite_pastes=favorite_pagination.items, + favorite_pagination=favorite_pagination, + shared_pastes=shared_pagination.items, + shared_pagination=shared_pagination, + start_date=start_date_str, + end_date=end_date_str, + storage_used=storage_used_mb, + storage_limit=storage_limit_mb, + storage_available=storage_available_mb, + ) + + @app.route('/api/token', methods=['POST']) + def get_token(): + """ + Endpoint dedicado para generar un token JWT. + """ + data = request.json + if not data or 'username' not in data or 'password' not in data: + return jsonify({"error": "Username and password are required"}), 400 + + username = data['username'] + password = data['password'] + + user = User.query.filter_by(username=username).first() + if user and check_password_hash(user.password_hash, password): + token = generate_token(username) + return jsonify({"token": token}), 200 + + return jsonify({"error": "Invalid username or password"}), 401 + + @app.route('/paste//favorite', methods=['POST']) + @login_required + def add_to_favorites(id): + user = current_user # Usuario autenticado + paste = Paste.query.get_or_404(id) + + # Verificar si ya es favorito + if user in paste.favorites: + # Sigues devolviendo 200, pero ahora con "success": True + return jsonify({ + "success": True, + "message": "Paste already in favorites" + }), 200 + + try: + paste.favorites.append(user) + db.session.commit() + # ¡Aquí pones success: True! + return jsonify({ + "success": True, + "message": "Paste added to favorites" + }), 201 + except Exception as e: + db.session.rollback() + return jsonify({"error": str(e), "success": False}), 500 + + @app.route('/paste//unfavorite', methods=['POST']) + @login_required + def remove_from_favorites(id): + user = current_user # Usuario autenticado + paste = Paste.query.get_or_404(id) + + if user not in paste.favorites: + # Devuelve success: False + return jsonify({"error": "Paste not in favorites", "success": False}), 404 + + try: + paste.favorites.remove(user) + db.session.commit() + return jsonify({ + "success": True, + "message": "Paste removed from favorites" + }), 200 + except Exception as e: + db.session.rollback() + return jsonify({"error": str(e), "success": False}), 500 + + + @app.route('/favorites', methods=['GET']) + @login_required + def list_favorites(): + user = current_user + page = request.args.get('page', 1, type=int) + per_page = 10 + favorite_query = Favorite.query.filter_by(user_id=user.id) + pagination = favorite_query.paginate(page=page, per_page=per_page, error_out=False) + + favorites = [{ + "id": fav.paste.id, + "title": fav.paste.filename, + "type": fav.paste.get_type(), # Tipo de archivo + "created_at": fav.paste.created_at.strftime('%Y-%m-%d %H:%M:%S') # Fecha de creación + } for fav in pagination.items] + + return jsonify({ + "favorites": favorites, + "pagination": { + "page": pagination.page, + "per_page": pagination.per_page, + "total_pages": pagination.pages, + "total_items": pagination.total, + "has_next": pagination.has_next, + "has_prev": pagination.has_prev, + "next_page": pagination.next_num if pagination.has_next else None, + "prev_page": pagination.prev_num if pagination.has_prev else None + } + }) + + + @app.route('/media/', methods=['GET']) + def serve_media(id): + paste = Paste.query.get_or_404(id) + + # Ruta completa del archivo + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + + if not os.path.exists(file_path): + return "File not found", 404 + + # Sirve el archivo directamente como contenido de video + return send_file(file_path, mimetype=paste.content_type) + + @app.route('/create_paste_web', methods=['GET', 'POST']) + @login_required + def create_paste_web(): + if request.method == 'POST': + title = None + language = request.form.get('language') or '' # Puede venir vacío + paste_type = request.form.get('type') + content = request.form.get('content') + file = request.files.get('file') + expire = request.form.get('expire', 'yes').strip().lower() # Nuevo parámetro de expiración + + # Validar campos obligatorios + if (paste_type == 'Text' and not content) or (paste_type != 'Text' and not file): + flash("All fields are required.", "danger") + return redirect(url_for('create_paste_web')) + + try: + filename = None + content_type = None + size = 0 + + if file: + # Caso: El usuario ha subido un archivo + original_filename = secure_filename(file.filename) + filename = f"{uuid.uuid4().hex}_{original_filename}" + file_path = os.path.join(UPLOAD_FOLDER, filename) + file.save(file_path) + content_type = mime.from_file(file_path) + size = os.path.getsize(file_path) + + elif paste_type == 'Text': + # Caso: El usuario eligió “Text” + + # Buscar la extensión adecuada al "language" del dropdown. + ext = LANGUAGE_TO_EXTENSION.get(language.lower(), "txt") + # Generar el nombre con esa extensión + filename = f"{uuid.uuid4().hex}.{ext}" + + file_path = os.path.join(UPLOAD_FOLDER, filename) + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + + content_type = 'text/plain' + size = len(content) + + # Definir la expiración (1 día si expire="yes", permanente si expire="no") + expires_at = datetime.utcnow() + timedelta(days=1) if expire == 'yes' else None + + # Crear registro de Paste en la BD + paste = Paste( + title=title, + content_type=content_type, + filename=filename, + owner_id=current_user.id, + user_id=current_user.id, + language=language, + size=size, + expires_at=expires_at # Guardamos la fecha de expiración + ) + + db.session.add(paste) + db.session.commit() + index_paste(paste) + + flash("Paste created successfully!", "success") + return redirect(url_for('get_paste', id=paste.id)) + + except Exception as e: + db.session.rollback() + flash(f"An error occurred: {e}", "danger") + return redirect(url_for('create_paste_web')) + + # Si es GET, mostrar formulario + return render_template('create_paste_web.html') + + +# Función para descargar archivos + @app.route('/paste//download', methods=['GET']) + @login_required + def download_paste_file(id): + paste = Paste.query.get_or_404(id) + + if paste.owner_id != current_user.id: + flash('No tienes permiso para descargar este archivo.', 'danger') + return redirect(url_for('get_paste', id=id)) + + if not paste.filename: + flash('Este paste no tiene un archivo asociado.', 'warning') + return redirect(url_for('get_paste', id=id)) + + file_path = os.path.join(app.config['UPLOAD_FOLDER'], paste.filename) + + if not os.path.exists(file_path): + flash('El archivo no existe.', 'danger') + return redirect(url_for('get_paste', id=id)) + + return send_from_directory(app.config['UPLOAD_FOLDER'], paste.filename, as_attachment=True) + + @app.route('/paste/') + @login_required + def view_paste(paste_id): + paste = Paste.query.get_or_404(paste_id) + # Verificar que el usuario es el propietario o tiene permisos para ver el paste + if paste.owner_id != current_user.id: + flash('No tienes permiso para ver este paste.', 'danger') + return redirect(url_for('user_dashboard')) + return render_template('view_paste.html', paste=paste) + + @app.route('/pastes/search', methods=['GET']) + @jwt_required + def search_pastes(): + try: + # Obtener el query string + query = request.args.get('q', '') + if not query: + return jsonify({"error": "Search query is required"}), 400 + + # Obtener el usuario autenticado + user = request.user + if not user: + return jsonify({"error": "User not authenticated"}), 403 + + # Realizar la búsqueda en Elasticsearch + body = { + "query": { + "bool": { + "must": [ + {"match": {"content": query}} + ], + "filter": [ + {"term": {"owner_id": user.id}} + ] + } + } + } + results = es.search(index="pastes", body=body) + + # Construir la respuesta con la URL + base_url = request.host_url.rstrip('/') + hits = results["hits"]["hits"] + response = [{"id": hit["_id"], "url": f"{base_url}/paste/{hit['_id']}", "content": hit["_source"].get("content", "")} for hit in hits] + return jsonify(response), 200 + + except Exception as e: + current_app.logger.error(f"Error searching pastes: {e}") + return jsonify({"error": "Error searching pastes"}), 500 + + @app.route('/pastes/search_web', methods=['GET']) + @login_required + def search_pastes_web(): + try: + query = request.args.get('q', '').strip() + content_type = request.args.get('content_type', '').strip() + language = request.args.get('language', '').strip() + + # Obtener al usuario autenticado + user = current_user + if not user.is_authenticated: + return redirect(url_for('login')) + + # Construir la consulta para Elasticsearch + must_clauses = [] + if query: + must_clauses.append({"match": {"content": query}}) + if content_type: + must_clauses.append({"term": {"content_type": content_type}}) + if language: + must_clauses.append({"term": {"language": language}}) + + body = { + "query": { + "bool": { + "must": must_clauses, + "filter": [{"term": {"owner_id": user.id}}] + } + } + } + + # Consultar Elasticsearch + results = es.search(index="pastes", body=body) + hits = results["hits"]["hits"] + + # Construir los resultados + pastes = [ + { + "id": hit["_id"], + "url": f"{request.host_url.rstrip('/')}/paste/{hit['_id']}", + "content_type": hit["_source"].get("content_type", ""), + "language": hit["_source"].get("language", ""), + } + for hit in hits + ] + + # Obtener favoritos del usuario + favorite_query = Paste.query.filter(Paste.favorites.any(id=user.id)) + favorite_pagination = favorite_query.paginate(page=1, per_page=10, error_out=False) + favorite_pastes = favorite_pagination.items + + # Renderizar los resultados en la plantilla + return render_template( + 'search_results.html', + pastes=pastes, # Resultados de búsqueda + query=query, + content_type=content_type, + language=language, + favorite_pastes=favorite_pastes, # Lista de favoritos + favorite_pagination=favorite_pagination # Paginación de favoritos + ) + + except Exception as e: + current_app.logger.error(f"Error in web search: {e}") + return render_template('error.html', message="Search failed"), 500 + + @app.route('/api/paste//favorite', methods=['POST']) + @jwt_required + def api_add_to_favorites(id): + try: + # Usuario autenticado + user = request.user + paste = Paste.query.get_or_404(id) + + # Verificar si ya es favorito + if paste in user.favorite_pastes: + return jsonify({"message": "Paste already in favorites"}), 200 + + # Añadir a favoritos + user.favorite_pastes.append(paste) + db.session.commit() + return jsonify({"message": "Paste added to favorites"}), 201 + + except Exception as e: + logging.error(f"Error adding paste to favorites: {e}") + db.session.rollback() + return jsonify({"error": str(e)}), 500 + @app.route('/api/paste//unfavorite', methods=['POST']) + @jwt_required + def api_remove_from_favorites(id): + try: + # Usuario autenticado + user = request.user + paste = Paste.query.get_or_404(id) + + # Verificar si no está en favoritos + if paste not in user.favorite_pastes: + return jsonify({"message": "Paste is not in favorites"}), 200 + + # Quitar de favoritos + user.favorite_pastes.remove(paste) + db.session.commit() + return jsonify({"message": "Paste removed from favorites"}), 200 + + except Exception as e: + logging.error(f"Error removing paste from favorites: {e}") + db.session.rollback() + return jsonify({"error": str(e)}), 500 + + @app.route('/api/paste//download', methods=['GET']) + @jwt_required + def api_download_paste_file(id): + user = request.user # Usuario autenticado mediante JWT + paste = Paste.query.get_or_404(id) + + # Verificar permisos + if paste.owner_id != user.id: + return jsonify({"error": "You do not have permission to download this file"}), 403 + + # Verificar que el paste tiene un archivo asociado + if not paste.filename: + return jsonify({"error": "This paste does not have an associated file"}), 400 + + # Construir la ruta completa del archivo + file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], paste.filename) + + # Verificar que el archivo exista + if not os.path.exists(file_path): + return jsonify({"error": "The file does not exist"}), 404 + + # Determinar el tipo MIME del archivo + mime_type = paste.content_type or mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + + # Enviar el archivo con el nombre original y tipo MIME + return send_file( + file_path, + as_attachment=True, + download_name=paste.filename, # Flask >= 2.0 + mimetype=mime_type + ) + + @app.route('/api/favorites', methods=['GET']) + @jwt_required + def api_list_favorites(): + try: + # Obtener el usuario autenticado + user = request.user + + # Obtener todos los favoritos del usuario + favorite_pastes = user.favorite_pastes # Relación definida en el modelo User + + if not favorite_pastes: + return jsonify({"message": "No favorites found"}), 200 + + base_url = request.host_url.rstrip('/') + response_list = [] + for paste in favorite_pastes: + response_list.append({ + "id": paste.id, + "url": f"{base_url}/paste/{paste.id}", + "title": paste.title or paste.filename, + "type": paste.get_type(), # Método definido en tu modelo Paste + "size": paste.size or 0, # Devuelve 0 si el tamaño no está definido + "created_at": paste.created_at.strftime('%Y-%m-%d %H:%M:%S') if paste.created_at else None + }) + + return jsonify(response_list), 200 + except Exception as e: + logging.error(f"Error retrieving favorites: {e}") + return jsonify({"error": "Error retrieving favorites"}), 500 + + @app.route('/paste//toggle_editable', methods=['POST']) + @login_required + def toggle_editable(id): + paste = Paste.query.get_or_404(id) + + # Verificar que el usuario es el propietario + if paste.owner_id != current_user.id: + return jsonify({"error": "No tienes permiso para modificar este paste"}), 403 + + try: + # Toggle el estado de editable + paste.editable = not paste.editable + db.session.commit() + + status = "enabled" if paste.editable else "disabled" + message = f"Editable {status} successfully." + return jsonify({"success": True, "editable": paste.editable, "message": message}), 200 + + except Exception as e: + db.session.rollback() + logging.error(f"Error toggling editable: {e}") + return jsonify({"success": False, "error": "Error al actualizar el estado editable"}), 500 + + @app.route('/paste//edit', methods=['GET', 'POST']) + @login_required + def edit_paste_web(id): + paste = Paste.query.get_or_404(id) + + # Verificar permisos de edición + if not paste.has_edit_permission(current_user): + flash("No tienes permiso para editar este paste.", "danger") + return redirect(url_for('get_paste', id=id)) + + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) if paste.filename else None + + # Manejar GET y POST + if request.method == 'POST': + new_content = request.form.get('content') + if not new_content: + flash("El contenido no puede estar vacío.", "danger") + return redirect(url_for('edit_paste_web', id=id)) + + try: + # 1. Eliminar el paste del índice anterior en Elasticsearch + delete_paste_from_index(paste) + + # 2. Guardar el contenido actualizado en el archivo + with open(file_path, 'w', encoding='utf-8') as f: + f.write(new_content) + + # Actualizar la información del paste en la base de datos + paste.last_edited_at = datetime.utcnow() + db.session.commit() + + # 3. Indexar nuevamente el paste actualizado en Elasticsearch + index_paste(paste) + + flash("Paste actualizado correctamente.", "success") + return redirect(url_for('get_paste', id=id)) + except Exception as e: + db.session.rollback() + flash(f"Ocurrió un error al guardar el paste: {str(e)}", "danger") + return redirect(url_for('edit_paste_web', id=id)) + + # Cargar contenido actual para la edición + current_content = "" + if file_path and os.path.exists(file_path): + try: + with open(file_path, 'r', encoding='utf-8') as f: + current_content = f.read() + except Exception as e: + flash(f"Error al leer el contenido del paste: {str(e)}", "danger") + + return render_template('edit_paste.html', paste=paste, current_content=current_content) + + + @app.route('/paste//share', methods=['POST']) + @login_required + def share_paste(id): + data = request.json + username = data.get('username') + can_edit = data.get('can_edit', False) + + # Buscar al usuario con el que se compartirá el paste + user = User.query.filter_by(username=username).first() + if not user: + return jsonify({"error": "User not found"}), 404 + + # Buscar el paste + paste = Paste.query.get_or_404(id) + if paste.owner_id != current_user.id: + return jsonify({"error": "You do not own this paste"}), 403 + + try: + # Verificar si ya existe el registro + existing_entry = db.session.query(shared_pastes).filter_by( + paste_id=paste.id, + user_id=user.id + ).first() + + if existing_entry: + # Si el registro existe, actualizar el valor de `can_edit` + db.session.execute( + shared_pastes.update() + .where( + (shared_pastes.c.paste_id == paste.id) & + (shared_pastes.c.user_id == user.id) + ) + .values(can_edit=can_edit) + ) + else: + # Si el registro no existe, insertar uno nuevo + stmt = shared_pastes.insert().values( + paste_id=paste.id, + user_id=user.id, + can_edit=can_edit + ) + db.session.execute(stmt) + + db.session.commit() + return jsonify({"message": f"Paste shared with {username}, can_edit: {can_edit}."}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Error sharing paste: {str(e)}"}), 500 + + + @app.route('/paste//check-permissions', methods=['GET']) + @login_required + def check_permissions(id): + paste = Paste.query.get_or_404(id) + if paste.owner_id == current_user.id: + return jsonify({'can_edit': True}), 200 + + shared_paste = SharedPaste.query.filter_by(paste_id=paste.id, user_id=current_user.id).first() + if shared_paste and shared_paste.can_edit: + return jsonify({'can_edit': True}), 200 + + return jsonify({'can_edit': False}), 403 + + @app.route('/api/shared_with_others', methods=['GET']) + @jwt_required + def shared_with_others(): + """ + Devuelve los pastes compartidos por el usuario autenticado con otros usuarios. + """ + try: + user = request.user # Usuario autenticado + + # Obtener los pastes compartidos por el usuario actual con otros + shared_pastes_query = db.session.query(Paste).join(shared_pastes).filter( + shared_pastes.c.user_id != user.id, # Excluir al propietario como destinatario + Paste.owner_id == user.id # El usuario es el propietario del paste + ).all() + + # Construir respuesta con información de usuarios (usando joins para obtener el username) + pastes = [] + for paste in shared_pastes_query: + shared_users = db.session.query(User.username, shared_pastes.c.can_edit).join( + shared_pastes, shared_pastes.c.user_id == User.id + ).filter( + shared_pastes.c.paste_id == paste.id, + shared_pastes.c.user_id != user.id # Excluir al propietario + ).all() + + shared_with_list = [ + {"username": shared_user.username, "can_edit": shared_user.can_edit} + for shared_user in shared_users + ] + + pastes.append({ + "id": paste.id, + "title": paste.title or "Untitled", + "created_at": paste.created_at.strftime('%Y-%m-%d %H:%M:%S'), + "shared_with": shared_with_list + }) + + return jsonify({"shared_with_others": pastes}), 200 + except Exception as e: + logging.error(f"Error retrieving shared_with_others: {e}") + return jsonify({"error": "Error retrieving shared_with_others"}), 500 + + + @app.route('/api/shared_with_me', methods=['GET']) + @jwt_required + def shared_with_me(): + """ + Devuelve los pastes compartidos con el usuario autenticado. + """ + try: + user = request.user # Usuario autenticado + + # Obtener los pastes compartidos con el usuario actual + shared_pastes_query = db.session.query(Paste).join(shared_pastes).filter( + shared_pastes.c.user_id == user.id # Pastes compartidos con el usuario + ).all() + + # Construir respuesta + pastes = [ + { + "id": paste.id, + "title": paste.title or "Untitled", + "created_at": paste.created_at.strftime('%Y-%m-%d %H:%M:%S'), + "owner": paste.owner.username, + "can_edit": db.session.query(shared_pastes).filter_by( + paste_id=paste.id, user_id=user.id + ).first().can_edit + } + for paste in shared_pastes_query + ] + + return jsonify({"shared_with_me": pastes}), 200 + except Exception as e: + logging.error(f"Error retrieving shared_with_me: {e}") + return jsonify({"error": "Error retrieving shared_with_me"}), 500 + + + @app.route('/api/paste//share', methods=['POST']) + @jwt_required # Asegúrate de usar el decorador correcto + def add_share_paste(paste_id): + try: + # Obtener el usuario actual + current_user = request.user + if not current_user: + return jsonify({"error": "User not found in request"}), 401 + + # Verificar que el paste pertenece al usuario autenticado + paste = Paste.query.filter_by(id=paste_id, owner_id=current_user.id).first() + if not paste: + return jsonify({"error": "Paste not found or you do not own it"}), 404 + + # Obtener datos del cuerpo de la solicitud + data = request.get_json() + if not data: + return jsonify({"error": "Invalid JSON data"}), 400 + + username = data.get("username") + can_edit = data.get("can_edit", False) + + if not username: + return jsonify({"error": "Username is required"}), 400 + + # Prevenir que el usuario comparta el paste consigo mismo + if username.lower() == current_user.username.lower(): + return jsonify({"error": "You cannot share a paste with yourself"}), 400 + + # Verificar que el usuario destinatario existe (sin sensibilidad a mayúsculas) + recipient = User.query.filter(func.lower(User.username) == username.lower()).first() + if not recipient: + return jsonify({"error": f"User '{username}' not found"}), 404 + + # Verificar si ya se ha compartido el paste con este usuario + existing_share = db.session.query(shared_pastes).filter( + shared_pastes.c.paste_id == paste_id, + shared_pastes.c.user_id == recipient.id + ).first() + + if existing_share: + return jsonify({"error": f"Paste is already shared with '{username}'"}), 400 + + # Asegurarse de que can_edit es un booleano + if not isinstance(can_edit, bool): + return jsonify({"error": "can_edit must be a boolean value"}), 400 + + # Añadir el registro en shared_pastes + db.session.execute(shared_pastes.insert().values( + paste_id=paste_id, + user_id=recipient.id, + can_edit=can_edit + )) + db.session.commit() + + # Registrar la acción para auditoría + app.logger.info(f"User '{current_user.username}' shared paste ID {paste_id} with '{recipient.username}' with can_edit={can_edit}") + + return jsonify({"message": f"Paste shared successfully with '{username}'", "can_edit": can_edit}), 200 + + except SQLAlchemyError as e: + db.session.rollback() + app.logger.error(f"Database error while sharing paste: {e}") + return jsonify({"error": "Database error", "details": str(e)}), 500 + except Exception as e: + app.logger.error(f"Unexpected error while sharing paste: {e}") + return jsonify({"error": "An unexpected error occurred", "details": str(e)}), 500 + + + @app.route('/api/paste//unshare', methods=['POST']) + @jwt_required + def unshare_paste(paste_id): + try: + # Verificar que el usuario autenticado está configurado correctamente + current_user = request.user + if not current_user: + return jsonify({"error": "User not found in request"}), 401 + + # Verificar que el paste pertenece al usuario autenticado + paste = Paste.query.filter_by(id=paste_id, owner_id=current_user.id).first() + if not paste: + return jsonify({"error": "Paste not found or you do not own it"}), 404 + + # Obtener datos del cuerpo de la solicitud + data = request.get_json() + username = data.get("username") + + if not username: + return jsonify({"error": "Username is required"}), 400 + + # Verificar que el usuario destinatario existe + recipient = User.query.filter_by(username=username).first() + if not recipient: + return jsonify({"error": f"User {username} not found"}), 404 + + # Verificar si el paste está compartido con este usuario + existing_share = db.session.query(shared_pastes).filter( + shared_pastes.c.paste_id == paste_id, + shared_pastes.c.user_id == recipient.id + ).scalar() + + if not existing_share: + return jsonify({"error": f"Paste is not shared with {username}"}), 400 + + # Eliminar el registro en shared_pastes + db.session.execute( + shared_pastes.delete().where( + (shared_pastes.c.paste_id == paste_id) & + (shared_pastes.c.user_id == recipient.id) + ) + ) + db.session.commit() + + return jsonify({"message": f"Paste unshared successfully from {username}"}), 200 + except Exception as e: + return jsonify({"error": "An unexpected error occurred", "details": str(e)}), 500 + + @app.route('/paste//unshare', methods=['POST']) + @login_required + def unshare_paste_web(paste_id): + """ + Versión con sesión/logueo normal para 'descompartir' un paste. + """ + try: + # Tomar el username del formulario o del JSON (como prefieras) + # Si lo envías desde un fetch con JSON, usas request.get_json() + # Si lo envías desde un formulario , usas request.form + data = request.get_json() or {} + username = data.get("username") + + if not username: + return jsonify({"error": "Username is required"}), 400 + + # Verificar que el paste pertenece al usuario logueado + paste = Paste.query.filter_by(id=paste_id, owner_id=current_user.id).first() + if not paste: + return jsonify({"error": "Paste not found or you do not own it"}), 404 + + # Verificar que el usuario con el que se compartió existe + recipient = User.query.filter_by(username=username).first() + if not recipient: + return jsonify({"error": f"User {username} not found"}), 404 + + # Verificar si está compartido + existing_share = db.session.query(shared_pastes).filter( + (shared_pastes.c.paste_id == paste_id), + (shared_pastes.c.user_id == recipient.id) + ).scalar() + + if not existing_share: + return jsonify({"error": f"Paste is not shared with {username}"}), 400 + + # Eliminar el registro de shared_pastes + db.session.execute( + shared_pastes.delete().where( + (shared_pastes.c.paste_id == paste_id) & + (shared_pastes.c.user_id == recipient.id) + ) + ) + db.session.commit() + + return jsonify({"message": f"Paste unshared from {username} successfully"}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({"error": f"An unexpected error occurred: {str(e)}"}), 500 + + @app.route('/contact', methods=['GET', 'POST']) + def contact(): + if request.method == 'POST': + email = request.form.get('email') + subject = request.form.get('subject') or "Contact Form" + message = request.form.get('message') + + # Validar campos básicos + if not email or not message: + flash("Email and message are required.", "danger") + return redirect(url_for('contact')) + + # Lógica de envío de correo (idéntica a la que ya tienes en request_account) + try: + # Construir el mensaje + msg_body = f"Message from Contact Form:\n\nEmail: {email}\nSubject: {subject}\nMessage: {message}" + msg = MIMEText(msg_body) + msg['Subject'] = subject + msg['From'] = SMTP_USERNAME # El remitente que tienes configurado + msg['To'] = SMTP_USERNAME # El correo que recibirá el mensaje + + # Conexión SMTP (mismo patrón que request_account) + if SMTP_USE_SSL: + with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as server: + server.login(SMTP_USERNAME, SMTP_PASSWORD) + server.send_message(msg) + else: + with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server: + if SMTP_USE_TLS: + server.starttls() + server.login(SMTP_USERNAME, SMTP_PASSWORD) + server.send_message(msg) + + flash("Your message has been sent!", "success") + return redirect(url_for('contact')) + + except smtplib.SMTPException as smtp_error: + flash(f"SMTP error: {smtp_error}", "danger") + return redirect(url_for('contact')) + except Exception as e: + flash(f"An error occurred: {e}", "danger") + return redirect(url_for('contact')) + + # Si es GET, renderizar plantilla con el formulario + return render_template('contact.html') + + @app.route('/api/paste/', methods=['PUT']) + @jwt_required + def update_paste_file(id): + paste = Paste.query.get_or_404(id) + user = request.user # Usuario autenticado + + # Verificar permisos + if not paste.has_edit_permission(user): + return jsonify({"error": "No permission to edit this paste."}), 403 + + data = request.json or {} + new_content = data.get('content') + if not new_content: + return jsonify({"error": "Missing 'content' in JSON"}), 400 + + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + try: + # 1. Eliminar de Elasticsearch (opcional) + delete_paste_from_index(paste) + + # 2. Guardar contenido en el archivo + with open(file_path, 'w', encoding='utf-8') as f: + f.write(new_content) + + # 3. Actualizar la fecha de edición + paste.last_edited_at = datetime.utcnow() + db.session.commit() + + # 4. Indexar de nuevo + index_paste(paste) + + return jsonify({"message": "Paste updated successfully"}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({"error": str(e)}), 500 + + @app.route('/api/users', methods=['GET']) + @jwt_required + def list_users(): + try: + users = User.query.with_entities(User.username).all() + usernames = [user.username for user in users] + return jsonify({"users": usernames}), 200 + except Exception as e: + app.logger.error(f"Error retrieving users: {e}") + return jsonify({"error": "An unexpected error occurred."}), 500 + + + @app.route('/api/users/search', methods=['GET']) + @login_required # Asegura que solo usuarios autenticados puedan acceder + def search_users(): + query = request.args.get('q', '').strip() + logging.info(f"Search query received: {query}") + if not query: + return jsonify([]) # Retorna una lista vacía si la consulta está vacía + + # Obtener todos los nombres de usuario desde la base de datos + all_users = [user.username for user in User.query.all()] + logging.info(f"Total users fetched: {len(all_users)}") + + # Realizar una búsqueda difusa utilizando RapidFuzz + matches = process.extract(query, all_users, scorer=fuzz.WRatio, limit=10) + logging.info(f"Matches found: {matches}") + + # Definir un umbral para filtrar resultados poco relevantes + threshold = 60 # Puedes ajustar este valor según tus necesidades + + # Filtrar los resultados que superen el umbral + matched_users = [username for username, score, _ in matches if score >= threshold] + logging.info(f"Matched users after threshold: {matched_users}") + + # Crear una lista de diccionarios para retornar + user_list = [{'username': username} for username in matched_users] + + return jsonify(user_list) + diff --git a/src/routes.py_test b/src/routes.py_test new file mode 100644 index 0000000..3cc69c2 --- /dev/null +++ b/src/routes.py_test @@ -0,0 +1,2501 @@ +import smtplib +from email.mime.text import MIMEText +import os +import mimetypes +from flask import request, jsonify, send_from_directory, abort, render_template, redirect, url_for, flash, session +from src.models import db, Paste, User, shared_pastes +from src.auth import generate_token +from config import UPLOAD_FOLDER, SMTP_SERVER, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_USE_TLS, SMTP_USE_SSL, ROLE_STORAGE_LIMITS +from pygments import highlight +from pygments.lexers import guess_lexer, get_lexer_by_name, guess_lexer_for_filename +from pygments.formatters import HtmlFormatter +from pygments.util import ClassNotFound +from sqlalchemy import func +import magic +from werkzeug.utils import secure_filename +import logging +from io import BytesIO +from flask import send_file, Response +import uuid +from pymediainfo import MediaInfo +import secrets +from sqlalchemy.exc import SQLAlchemyError +from pygments.styles import get_all_styles +from datetime import datetime +from flask_login import login_required +from flask_login import login_user +from flask_login import logout_user +from werkzeug.security import check_password_hash +from src.auth import jwt_required +from flask_login import current_user +from markdown import markdown +from collections import defaultdict +import jwt +from flask import current_app +from elasticsearch import Elasticsearch +from datetime import datetime +from src.models import Favorite +from config import UPLOAD_FOLDER +from rapidfuzz import process, fuzz +from sqlalchemy import or_ +import json +es = Elasticsearch(hosts=["http://elasticsearch:9200"]) + +def calculate_storage_used(user_id): + """ + Calcula el almacenamiento utilizado por el usuario. + + :param user_id: ID del usuario. + :return: Almacenamiento utilizado en MB. + """ + total = db.session.query(db.func.sum(Paste.size)).filter_by(user_id=user_id).scalar() + total = total if total else 0 + mb = total / (1024 ** 2) # Convertir bytes a MB + return mb + + + +def get_shared_pastes(user, paste_filters=None, page=1, per_page=10): + """ + Obtiene los pastes compartidos con el usuario especificado, aplicando filtros si es necesario. + + :param user: Objeto de usuario actual. + :param paste_filters: Lista de filtros adicionales para aplicar a la consulta. + :param page: Número de página para la paginación. + :param per_page: Número de elementos por página. + :return: Objeto de paginación con los pastes compartidos. + """ + query = Paste.query.join(shared_pastes).filter(shared_pastes.c.user_id == user.id) + + if paste_filters: + query = query.filter(*paste_filters) + + query = query.order_by(Paste.created_at.desc()) + + pagination = query.paginate(page=page, per_page=per_page, error_out=False) + + return pagination + + +def delete_paste_from_index(paste): + try: + es.delete(index='pastes', id=paste.id) + print(f"Deleted paste {paste.id} from index.") + except Exception as e: + print(f"Error deleting paste {paste.id} from index: {e}") + +def calculate_stats(user_id=None, start_date=None, end_date=None): + query = Paste.query + + if user_id is not None: + query = query.filter_by(user_id=user_id) + + if start_date: + if isinstance(start_date, str): + start_date = datetime.strptime(start_date, '%Y-%m-%d') + query = query.filter(Paste.created_at >= start_date) + + if end_date: + if isinstance(end_date, str): + end_date = datetime.strptime(end_date, '%Y-%m-%d').replace(hour=23, minute=59, second=59) + query = query.filter(Paste.created_at <= end_date) + + pastes = query.all() + + stats = { + "total_pastes": len(pastes), + "total_size": sum(paste.size for paste in pastes), + "total_text_pastes": query.filter(Paste.content_type.like('text/%')).count(), + "total_file_pastes": query.filter(Paste.content_type.notlike('text/%')).count(), + "total_media_pastes": query.filter( + or_( + Paste.content_type.like('image/%'), + Paste.content_type.like('video/%'), + Paste.content_type.like('audio/%') + ) + ).count(), + "total_compressed_pastes": query.filter( + Paste.content_type.in_([ + "application/zip", "application/x-tar", "application/gzip", + "application/x-bzip2", "application/x-7z-compressed", "application/x-rar-compressed" + ]) + ).count(), + "languages": list(set(paste.language for paste in pastes if paste.language)), + "counts_text": [], + "counts_file": [], + "counts_media": [], + "counts_compressed": [], + "pastes": pastes # Lista de pastes + } + + # Agrupar estadísticas por lenguaje y tipo + language_groups = query.with_entities(Paste.language, Paste.content_type).all() + language_counts = {} + for lang, ctype in language_groups: + if lang not in language_counts: + language_counts[lang] = {"text": 0, "file": 0, "media": 0} + if ctype.startswith("text/"): + language_counts[lang]["text"] += 1 + elif ctype.startswith(("image/", "video/", "audio/")): + language_counts[lang]["media"] += 1 + else: + language_counts[lang]["file"] += 1 + + stats["counts_text"] = [language_counts[lang]["text"] for lang in stats["languages"]] + stats["counts_file"] = [language_counts[lang]["file"] for lang in stats["languages"]] + stats["counts_media"] = [language_counts[lang]["media"] for lang in stats["languages"]] + + return stats + + +def index_paste(paste): + try: + file_path = os.path.join("/app/uploads", paste.filename) + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + content = f.read() + + doc = { + "id": paste.id, + "title": paste.title or paste.filename, + "content": content, + "owner_id": paste.owner_id, + "created_at": paste.created_at.isoformat(), + } + es.index(index="pastes", id=paste.id, document=doc) + except Exception as e: + current_app.logger.error(f"Error indexing paste {paste.id}: {e}") + +def get_current_user(): + user_id = session.get('user_id') + if not user_id: + return None + # Supongamos que tienes un modelo User para buscar el usuario + return User.query.get(user_id) + +logging.basicConfig( + level=logging.DEBUG, # Cambiar a INFO si no quieres mensajes de depuración + format="%(asctime)s - %(levelname)s - %(message)s", + handlers=[ + logging.StreamHandler() # Envía los logs a la consola + ] +) + +# Define la función highlight_code +def highlight_code(content, language=None, filename=None): + if language: + try: + lexer = get_lexer_by_name(language) + except: + lexer = guess_lexer(content) + else: + if filename: + lexer = guess_lexer_for_filename(filename, content) + else: + lexer = guess_lexer(content) + + formatter = HtmlFormatter(linenos=True, cssclass="highlight") + html_code = highlight(content, lexer, formatter) + + return html_code + +MEDIA_MIME_TYPES = ( + 'image/', + 'video/', + 'audio/', + 'application/pdf', +) + +# Tipos MIME basados en texto que no comienzan con 'text/' +TEXT_BASED_APPLICATION_MIME_TYPES = ( + 'application/json', + 'application/javascript', + 'application/xml', + 'application/xhtml+xml', + 'application/sql', + 'text/xml', + # Agrega otros tipos basados en texto según tus necesidades +) + +LANGUAGE_TO_EXTENSION = { + "python": "py", + "javascript": "js", + "java": "java", + "csharp": "cs", + "cpp": "cpp", + "ruby": "rb", + "go": "go", + "html": "html", + "css": "css", + "php": "php", + "swift": "swift", + "kotlin": "kt", + "rust": "rs", + "typescript": "ts", + "bash": "sh", + "plaintext": "txt", + "sql": "sql", + "json": "json", + "yaml": "yaml", + "xml": "xml" +} + +# Mapeo personalizado de extensiones a lenguajes +EXTENSION_TO_LANGUAGE = { + ".sh": "bash", + ".bash": "bash", + ".py": "python", + ".json": "json", + ".js": "javascript", + ".ts": "typescript", + ".sql": "sql", + ".html": "html", + ".css": "css", + ".java": "java", + ".c": "c", + ".cpp": "cpp", + ".rb": "ruby", + ".go": "go", + ".png": "image", + ".jpg": "image", + ".jpeg": "image", + ".gif": "image", + ".mp4": "video", + ".avi": "video", + ".mp3": "audio", + ".webm": "video", + ".mov": "video", + ".mkv": "video", + ".pdf": "pdf", + ",log": "plaintext", + # Agrega más mapeos según tus necesidades +} + +FILENAME_TO_LANGUAGE = { + "PKGBUILD": "bash", + # Puedes añadir más archivos específicos aquí + # "INSTALL": "bash", + # "configure": "bash", +} + + +mime = magic.Magic(mime=True, uncompress=True) + +import os +import logging +from pygments.lexers import guess_lexer +from pygments.util import ClassNotFound + +# Definir MIME types y extensiones de archivos comprimidos +COMPRESSED_MIME_TYPES = { + "application/zip": "compressed", + "application/gzip": "compressed", + "application/x-tar": "compressed", + "application/x-bzip2": "compressed", + "application/x-7z-compressed": "compressed" +} + +COMPRESSED_EXTENSIONS = { + ".zip": "compressed", + ".gz": "compressed", + ".tgz": "compressed", + ".tar": "compressed", + ".bz2": "compressed", + ".7z": "compressed" +} + +import os +import logging +from pygments.lexers import guess_lexer +from pygments.util import ClassNotFound + +# Definir MIME types y extensiones de archivos comprimidos +COMPRESSED_MIME_TYPES = { + "application/zip": "compressed", + "application/gzip": "compressed", + "application/x-tar": "compressed", + "application/x-bzip2": "compressed", + "application/x-7z-compressed": "compressed" +} + +COMPRESSED_EXTENSIONS = { + ".zip": "compressed", + ".gz": "compressed", + ".tgz": "compressed", + ".tar": "compressed", + ".bz2": "compressed", + ".7z": "compressed" +} + +def detect_language(content, unique_filename=None, original_filename=None, detected_mime_type=None): + """ + Detecta el lenguaje del contenido utilizando el nombre del archivo original, extensión o MIME type. + + :param content: Contenido del paste (str) o None. + :param unique_filename: Nombre de archivo único (UUID) si aplica. + :param original_filename: Nombre de archivo original (e.g., PKGBUILD) si aplica. + :param detected_mime_type: Tipo MIME detectado por magic. + :return: Lenguaje detectado (str) o 'compressed' si es un archivo comprimido. + """ + + # Verificar si el archivo es comprimido basándose en MIME type + if detected_mime_type and detected_mime_type in COMPRESSED_MIME_TYPES: + logging.info(f"MIME type '{detected_mime_type}' detectado como archivo comprimido.") + return COMPRESSED_MIME_TYPES[detected_mime_type] + + # Verificar si el nombre del archivo original está en FILENAME_TO_LANGUAGE + if original_filename: + base_name = os.path.basename(original_filename) + if base_name.upper() in FILENAME_TO_LANGUAGE: + language = FILENAME_TO_LANGUAGE[base_name.upper()] + logging.info(f"Filename '{base_name}' mapeado a lenguaje '{language}'") + return language + + # Detectar por extensión usando unique_filename + if unique_filename: + ext = os.path.splitext(unique_filename)[1].lower() + if ext in COMPRESSED_EXTENSIONS: + logging.info(f"Extension '{ext}' detectada como archivo comprimido.") + return COMPRESSED_EXTENSIONS[ext] + + if ext in EXTENSION_TO_LANGUAGE: + logging.info(f"Extension '{ext}' mapeada a lenguaje '{EXTENSION_TO_LANGUAGE[ext]}'") + return EXTENSION_TO_LANGUAGE[ext] + + # Si no hay mapeo por extensión, usar magic para detectar el lenguaje + if detected_mime_type: + MIME_TO_LANGUAGE = { + "application/json": "json", + "application/x-shellscript": "bash", + "application/javascript": "javascript", + "text/plain": "plaintext", + "text/xml": "xml", + "text/html": "html", + "text/css": "css", + "text/x-python": "python", + "text/x-c": "c", + "text/x-c++": "cpp", + "text/x-java": "java", + "text/x-php": "php", + "text/x-shellscript": "bash", + "application/sql": "sql", + "application/x-yaml": "yaml", + "application/x-toml": "toml", + "text/x-markdown": "markdown", + "text/markdown": "markdown", + "text/x-lua": "lua", + "text/x-rust": "rust", + "application/x-perl": "perl", + "text/x-ruby": "ruby", + } + if detected_mime_type in MIME_TO_LANGUAGE: + logging.info(f"MIME type '{detected_mime_type}' mapeado a lenguaje '{MIME_TO_LANGUAGE[detected_mime_type]}'") + return MIME_TO_LANGUAGE[detected_mime_type] + + # Fallback a Pygments solo si content no es None + if content: + try: + lexer = guess_lexer(content) + language = lexer.aliases[0] + logging.info(f"Pygments detectó el lenguaje como '{language}'") + return language + except ClassNotFound: + logging.warning("Pygments no pudo detectar el lenguaje. Asignando 'plaintext'") + return "plaintext" + + # Si content es None y no hay mapeo, asignar 'unknown' + logging.warning("Content es None y no hay mapeo de lenguaje. Asignando 'unknown'") + return "unknown" + +def init_routes(app): + @app.route('/file/', methods=['GET']) + def get_file(filename): + file_path = os.path.join(UPLOAD_FOLDER, filename) + + if not os.path.exists(file_path): + abort(404, description="file not found") + + mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + return send_from_directory(UPLOAD_FOLDER, filename, mimetype=mime_type) + + + @app.route('/update-theme', methods=['POST']) + def update_theme(): + data = request.get_json() + theme = data.get('theme') + if theme not in ['light', 'dark']: + return jsonify({'error': 'Invalid theme'}), 400 + session['theme'] = theme + return jsonify({'message': 'Theme updated successfully'}), 200 + + + @app.route('/', methods=['GET']) + def index(): + base_url = request.host_url.rstrip('/') + user_name = None + + # Supongamos que el usuario está guardado en la sesión con su ID + if 'user_id' in session: + user = User.query.get(session['user_id']) # Recuperar el usuario de la base de datos + user_name = user.username if user else None # Obtener el nombre del usuario + + return render_template('index.html', base_url=base_url, user_name=user_name) + + + @app.route('/login', methods=['GET', 'POST']) + def login(): + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + user = User.query.filter_by(username=username).first() + + if user and user.check_password(password): + login_user(user) # Autentica al usuario + flash("Login successful!", "success") + return redirect(url_for('user_dashboard')) + + flash("Invalid username or password", "danger") + return redirect(url_for('login')) + + return render_template('login.html') + + @app.route('/paste//json', methods=['GET']) + @jwt_required + def get_paste_json(id): + paste = Paste.query.get(id) + if not paste: + return jsonify({"error": "Paste not found"}), 404 + + response_data = { + "id": paste.id, + "filename": paste.filename, + "language": paste.language, + "content_type": paste.content_type, + "size": paste.size, + "created_at": paste.created_at.strftime('%Y-%m-%d %H:%M:%S') if paste.created_at else None, + "content": None + } + + # Solo incluye el contenido si existe como atributo + if hasattr(paste, 'content') and paste.content: + response_data["content"] = paste.content + elif paste.filename: + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if os.path.exists(file_path): + with open(file_path, 'r', encoding='utf-8', errors='replace') as f: + response_data["content"] = f.read() + + return jsonify(response_data), 200 + + + @app.route('/paste//raw', methods=['GET']) + def get_paste_raw(id): + paste = Paste.query.get(id) + if not paste: + return jsonify({"error": "Paste not found"}), 404 + + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + # Leer el contenido del archivo para devolverlo como texto sin formato + with open(file_path, 'r', encoding='utf-8', errors='replace') as f: + content = f.read() + + return content, 200, {'Content-Type': 'text/plain; charset=utf-8'} + + @app.route('/paste', methods=['POST']) + @jwt_required + def create_paste(): + file_obj = request.files.get('c') + user_language = request.form.get('lang', None) + logging.info(f"User provided language: {user_language}") + + if not file_obj: + logging.warning("No file provided for the paste.") + return jsonify({"error": "no content provided"}), 400 + + filename = None + language = None + content_type = None + + # Obtener usuario autenticado + user = request.user + + # Validar límites de usuario + file_size = len(file_obj.read()) + file_obj.seek(0) # Resetear el puntero del archivo + + if user.role == 'user': + if file_size > 100 * 1024**2: # 100 MB por paste + logging.warning(f"User {user.username} tried to upload a file exceeding 100 MB.") + return jsonify({"error": "File exceeds the 100 MB limit for your role"}), 403 + if user.storage_used + file_size > user.storage_limit: # Límite de almacenamiento total + logging.warning(f"User {user.username} exceeded their total storage limit.") + return jsonify({"error": "You have exceeded your total storage limit"}), 403 + + try: + # Obtener la extensión del archivo + original_filename = file_obj.filename + file_extension = os.path.splitext(original_filename)[1].lower() + + # Generar un nombre único para el archivo + unique_filename = f"{uuid.uuid4().hex}{file_extension}" + file_path = os.path.join(UPLOAD_FOLDER, unique_filename) + + # Leer una muestra del archivo para analizar MIME + file_sample = file_obj.read(8192) + detected_mime_type = mime.from_buffer(file_sample) + file_obj.seek(0) # Resetear el puntero del archivo + + logging.info(f"Detected MIME type: {detected_mime_type}") + logging.info(f"Original filename: {original_filename}, Extension: {file_extension}, Size: {file_size} bytes") + content_type = detected_mime_type + + # Guardar el archivo en el sistema de archivos + with open(file_path, 'wb') as f: + f.write(file_obj.read()) + logging.info(f"Saved paste content to: {file_path}") + + # Detectar lenguaje + if user_language: + language = user_language + logging.info(f"User specified language: {language}") + else: + language = detect_language( + content=None, + unique_filename=unique_filename, + original_filename=original_filename, + detected_mime_type=detected_mime_type + ) + logging.info(f"Detected language: {language}") + + filename = unique_filename + + # Actualizar el almacenamiento usado por el usuario + user.storage_used += file_size + db.session.commit() + + except Exception as e: + logging.error(f"Exception occurred: {str(e)}") + return jsonify({"error": f"Error processing the content: {str(e)}"}), 500 + + # Crear registro del paste en la base de datos + paste = Paste( + content_type=content_type, + filename=filename, + owner_id=user.id, + user_id=user.id, + language=language, + size=file_size + ) + + try: + db.session.add(paste) + db.session.commit() + logging.info(f"Saved paste with ID: {paste.id}") + + # Indexar el contenido directamente desde el archivo + index_paste(paste) + + except Exception as e: + db.session.rollback() + logging.error(f"Error saving paste: {str(e)}") + return jsonify({"error": f"Error saving paste: {str(e)}"}), 500 + + base_url = request.host_url.rstrip('/') + paste_url = f"{base_url}/paste/{paste.id}" + + logging.info(f"Paste created successfully. ID: {paste.id}, URL: {paste_url}") + return jsonify({"url": paste_url}), 201 + + + @app.route('/paste/', methods=['GET']) + def get_paste(id): + paste = Paste.query.get(id) + if not paste: + logging.warning(f"Paste ID {id} not found.") + return jsonify({"error": "Paste not found"}), 404 + if current_user.is_authenticated: + can_edit = paste.has_edit_permission(current_user) + else: + can_edit = False + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) if paste.filename else None + + # Recuperar usuarios con los que se compartió el paste (solo para el propietario) + shared_with = [] + if current_user.is_authenticated and paste.owner_id == current_user.id: + shared_with = db.session.query(User.username, shared_pastes.c.can_edit).join( + shared_pastes, User.id == shared_pastes.c.user_id + ).filter(shared_pastes.c.paste_id == paste.id).all() + # Definir los tipos MIME que consideras como binarios + BINARIO_MIME_TYPES = ( + 'font/', + 'model/', + # Agrega más tipos MIME según tus necesidades + ) + + # Tipos MIME basados en texto que no comienzan con 'text/' + TEXT_BASED_APPLICATION_MIME_TYPES = ( + 'application/json', + 'application/javascript', + 'application/xml', + 'application/xhtml+xml', + 'application/sql', + # Agrega otros tipos basados en texto según tus necesidades + ) + + # Tipos MIME multimedia + MEDIA_MIME_TYPES = ( + 'image/', + 'video/', + 'audio/', + 'application/pdf', + ) + + # Función para verificar si el MIME type es binario + def is_binary(mime_type): + return any(mime_type.startswith(prefix) for prefix in BINARIO_MIME_TYPES) + + # Función para verificar si el MIME type es multimedia + def is_media(mime_type): + return any(mime_type.startswith(prefix) for prefix in MEDIA_MIME_TYPES) + + # Determinar el tipo de contenido + if paste.content_type.startswith('text/') or paste.content_type in TEXT_BASED_APPLICATION_MIME_TYPES: + es_binario = False + es_media = False + elif is_media(paste.content_type): + es_binario = False + es_media = True + elif is_binary(paste.content_type): + es_binario = True + es_media = False + else: + # Por defecto, tratar como binario si no coincide con las anteriores + es_binario = True + es_media = False + + logging.debug(f"Content Type: {paste.content_type}") + logging.debug(f"Is binary: {es_binario}") + logging.debug(f"Is media: {es_media}") + + if es_binario: + if not paste.filename or not os.path.exists(file_path): + logging.error(f"File for paste ID {id} not found: {file_path}") + return jsonify({"error": "File not found"}), 404 + + try: + # Obtener información adicional + owner = User.query.get(paste.owner_id) + file_size = paste.size # Asegúrate de que el modelo Paste tenga el campo 'size' + + return render_template( + 'binary_view.html', + paste=paste, + filename=paste.filename, + mime_type=paste.content_type, + owner=owner, + size=file_size + ) + except Exception as e: + logging.error(f"Error while rendering binary view for paste ID {id}: {e}") + return jsonify({"error": "Error while processing the file"}), 500 + + elif es_media: + if not paste.filename or not os.path.exists(file_path): + logging.error(f"File for paste ID {id} not found: {file_path}") + return jsonify({"error": "File not found"}), 404 + + try: + # Extraer metadatos si es necesario + metadata = [] + if paste.content_type.startswith("image/"): + logging.info(f"Extracting metadata for image: {file_path}") + media_info = MediaInfo.parse(file_path) + for track in media_info.tracks: + track_data = {key: value for key, value in track.to_data().items() if value} + if track_data: + metadata.append(track_data) + + if not metadata: + metadata.append({"Info": "No metadata found in image file"}) + + elif paste.content_type.startswith(("video/", "audio/")): + logging.info(f"Extracting metadata for media: {file_path}") + media_info = MediaInfo.parse(file_path) + for track in media_info.tracks: + track_data = {key: value for key, value in track.to_data().items() if value} + if track_data: + metadata.append(track_data) + + if not metadata: + metadata.append({"Info": "No metadata found in media file"}) + + elif paste.content_type == "application/pdf": + logging.info(f"Extracting metadata for PDF: {file_path}") + # Implementa la extracción de metadatos para PDFs si es necesario + + return render_template( + 'media_view.html', + filename=paste.filename, + mime_type=paste.content_type, + metadata=metadata, + paste_id=paste.id + ), 200 + + except Exception as e: + logging.error(f"Error while rendering media view for paste ID {id}: {e}") + return jsonify({"error": "Error while processing the media file"}), 500 + + elif not es_binario and not es_media: + # Manejo de tipos de contenido soportados (texto) + try: + with open(file_path, 'r', encoding='utf-8', errors='replace') as f: + content = f.read() + + # Determinar si el contenido es Markdown + is_markdown = False + + # 1. Basado en la extensión del archivo + if paste.filename and paste.filename.lower().endswith('.md'): + is_markdown = True + logging.info("Detected Markdown based on file extension (.md).") + + # 2. Basado en el lenguaje especificado + elif paste.language and paste.language.lower() in ['md', 'markdown']: + is_markdown = True + logging.info("Detected Markdown based on specified language.") + + # 3. Basado en Tipo MIME + elif paste.content_type == 'text/markdown': + is_markdown = True + logging.info("Detected Markdown based on MIME type (text/markdown).") + + if is_markdown: + # Convertir Markdown a HTML usando python-markdown con extensiones + md_html = markdown( + content, + extensions=[ + 'tables', # Soporte para tablas + 'fenced_code', # Soporte para bloques de código con ``` + 'codehilite' # Soporte para resaltado de sintaxis con Pygments + ] + ) + logging.info("Converted Markdown to HTML.") + + return render_template( + 'text_paste.html', + paste=paste, + md_html_code=md_html, # Pasamos el HTML generado + is_markdown=True, + can_edit=can_edit, + shared_with=shared_with + ), 200 + else: + # Contenido no es Markdown -> resaltar con Pygments normal + html_code = highlight_code( + content, language=paste.language, filename=paste.filename + ) + logging.info("Converted text content with Pygments.") + + return render_template( + 'text_paste.html', + paste=paste, + html_code=html_code, + is_markdown=False, + can_edit=can_edit, + shared_with=shared_with + ), 200 + + except Exception as e: + logging.error(f"Error reading text file for paste ID {id}: {e}") + return jsonify({"error": "Error processing text file"}), 500 + + else: + # Si el tipo MIME no coincide con ninguno de los anteriores, manejarlo aquí + logging.warning(f"Unhandled content type: {paste.content_type}") + return jsonify({"error": "Unhandled content type"}), 400 + + @app.route('/pastes', methods=['GET']) + @jwt_required + def list_pastes(): + try: + # Obtener todos los pastes del usuario autenticado + user = request.user + pastes = Paste.query.filter_by(owner_id=user.id).all() + + if not pastes: + return jsonify({"message": "No pastes found"}), 200 + + base_url = request.host_url.rstrip('/') + response_list = [] + for paste in pastes: + response_list.append({ + "id": paste.id, + "url": f"{base_url}/paste/{paste.id}", + "title": paste.title or paste.filename, + "type": paste.get_type(), # Método definido en tu modelo Paste + "size": paste.size or 0, # Devuelve 0 si el tamaño no está definido + "created_at": paste.created_at.strftime('%Y-%m-%d %H:%M:%S') if paste.created_at else None, + "is_favorite": paste in user.favorite_pastes # Verifica si el paste es favorito + }) + + return jsonify(response_list), 200 + except Exception as e: + logging.error(f"Error retrieving pastes: {e}") + return jsonify({"error": "Error retrieving pastes"}), 500 + + @app.route('/register', methods=['POST']) + @jwt_required + def register_user(): + authenticated_user = request.user + + if authenticated_user.username != 'admin': + return jsonify({"error": "you have no access rights to register users"}), 403 + + data = request.get_json() + if not data: + return jsonify({"error": "No data provided"}), 400 + + username = data.get('username') + password = data.get('password') + + if not username or not password: + return jsonify({"error": "'username' and 'password' required"}), 400 + + existing_user = User.query.filter_by(username=username).first() + if existing_user: + return jsonify({"error": "El nombre de usuario ya existe"}), 400 + + try: + new_user = User(username=username) + new_user.set_password(password) + db.session.add(new_user) + db.session.commit() + return jsonify({"message": f"Usuario '{username}' registry successful"}), 201 + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Error registering user: {str(e)}"}), 500 + + @app.route('/admin/login', methods=['GET', 'POST']) + def admin_login(): + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + user = User.query.filter_by(username=username).first() + + if user and user.check_password(password) and username == 'admin': + session['admin'] = username + flash('Login successful', 'success') + return redirect(url_for('admin_dashboard')) + + flash('Invalid credentials', 'danger') + return render_template('admin_login.html') + + @app.route('/admin/logout') + def admin_logout(): + session.pop('admin', None) + flash('Logged out', 'info') + return redirect(url_for('admin_login')) + + @app.route('/admin') + def admin_dashboard(): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + users_count = User.query.count() + pastes_count = Paste.query.count() + return render_template('dashboard.html', users_count=users_count, pastes_count=pastes_count) + + @app.route('/admin/users') + def admin_users(): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + users = User.query.all() + return render_template('users.html', users=users) + + @app.route('/admin/users/add', methods=['GET', 'POST']) + def admin_add_user(): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + role = request.form.get('role') # Obtén el rol del formulario + + # Validar datos + if not username or not password or not role: + flash('All fields are required', 'danger') + return redirect(url_for('admin_add_user')) + + # Validar si el rol es válido + valid_roles = ['admin', 'advanced', 'user'] + if role not in valid_roles: + flash('Invalid role selected.', 'danger') + return redirect(url_for('admin_add_user')) + + # Crear usuario + try: + user = User(username=username, role=role, storage_limit=ROLE_STORAGE_LIMITS.get(role)) + user.set_password(password) + db.session.add(user) + db.session.commit() + flash('User added successfully', 'success') + return redirect(url_for('admin_users')) + except Exception as e: + db.session.rollback() + flash(f'Error: {str(e)}', 'danger') + return redirect(url_for('admin_add_user')) + + return render_template('add_user.html') + + @app.route('/admin/users/delete/', methods=['POST']) + def admin_delete_user(user_id): + if 'admin' not in session: + # Enviar JSON de error en lugar de redirigir + return jsonify({"error": "Unauthorized"}), 401 + + user = User.query.get(user_id) + if not user: + return jsonify({"error": "User not found"}), 404 + + db.session.delete(user) + db.session.commit() + return jsonify({"message": "User deleted successfully"}), 200 + + + user = User.query.get(user_id) + if not user: + flash('User not found', 'danger') + else: + db.session.delete(user) + db.session.commit() + flash('User deleted successfully', 'success') + return redirect(url_for('admin_users')) + + @app.route('/admin/pastes') + def admin_pastes(): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + pastes = Paste.query.all() + return render_template('pastes.html', pastes=pastes) + + @app.route('/admin/pastes/delete/', methods=['POST']) + def admin_delete_paste(paste_id): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + paste = Paste.query.get(paste_id) + if not paste: + flash('Paste not found', 'danger') + else: + delete_paste_from_index(paste) + paste.shared_with.clear() + db.session.delete(paste) + db.session.commit() + flash('Paste deleted successfully', 'success') + return redirect(url_for('admin_pastes')) + + @app.route('/stats', methods=['GET']) + def anonymous_stats(): + start_date = request.args.get('start_date') + end_date = request.args.get('end_date') + + stats = calculate_stats(start_date=start_date, end_date=end_date) + + return render_template( + 'anonymous_stats.html', + total_pastes=stats["total_pastes"], + total_text_pastes=stats["total_text_pastes"], + total_file_pastes=stats["total_file_pastes"], + total_media_pastes=stats["total_media_pastes"], + languages=stats["languages"], + counts_text=stats["counts_text"], + counts_file=stats["counts_file"], + counts_media=stats["counts_media"], + ) + + + @app.route('/api/docs') + def redoc(): + return ''' + + + + Pastebin API Docs + + + + + + + + ''' + @app.route('/paste//download', methods=['GET']) + def download_paste(id): + paste = Paste.query.get(id) + if not paste: + return jsonify({"error": "Paste not found"}), 404 + + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + try: + return send_from_directory( + directory=UPLOAD_FOLDER, + path=paste.filename, + as_attachment=True, + download_name=paste.filename, + mimetype=paste.content_type + ) + except TypeError: + # Para Flask < 2.0 + return send_from_directory( + directory=UPLOAD_FOLDER, + path=paste.filename, + as_attachment=True, + attachment_filename=paste.filename, # <--- Distinto nombre de argumento + mimetype=paste.content_type + ) + + + @app.route('/request-account', methods=['GET', 'POST']) + def request_account(): + if request.method == 'POST': + email = request.form.get('email') + username = request.form.get('username') + + # Validate basic input + if not email or not username: + logging.warning("Validation failed: Missing email or username") + flash("Email and username are required.", "danger") + return redirect(url_for('request_account')) + + logging.info(f"Account request initiated. Username: {username}, Email: {email}") + + # Log the SMTP environment variables for debugging + logging.debug(f"SMTP_SERVER={SMTP_SERVER}") + logging.debug(f"SMTP_PORT={SMTP_PORT}") + logging.debug(f"SMTP_USERNAME={SMTP_USERNAME}") + logging.debug(f"SMTP_USE_SSL={SMTP_USE_SSL}, SMTP_USE_TLS={SMTP_USE_TLS}") + + # Send email + try: + msg = MIMEText(f"New account request:\n\nUsername: {username}\nEmail: {email}") + msg['Subject'] = 'Account Request' + msg['From'] = SMTP_USERNAME + msg['To'] = SMTP_USERNAME # Ajusta a tu dirección de correo receptora + + # Connect to the SMTP server + if SMTP_USE_SSL: + with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as server: + logging.debug("Using SMTP over SSL") + server.login(SMTP_USERNAME, SMTP_PASSWORD) + logging.info("SMTP login successful") + server.send_message(msg) + else: + with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server: + if SMTP_USE_TLS: + logging.debug("Starting TLS session") + server.starttls() + server.login(SMTP_USERNAME, SMTP_PASSWORD) + logging.info("SMTP login successful") + server.send_message(msg) + + logging.info("Account request email sent successfully") + flash("Your account request has been submitted successfully!", "success") + return redirect(url_for('index')) + + except smtplib.SMTPException as smtp_error: + logging.error(f"SMTP error: {smtp_error}") + flash(f"SMTP error: {smtp_error}", "danger") + except Exception as general_error: + logging.error(f"An unexpected error occurred: {general_error}") + flash(f"An error occurred: {general_error}", "danger") + + return redirect(url_for('request_account')) + + return render_template('request_account.html') + + @app.route('/user/details', methods=['GET']) + @jwt_required + def get_user_details(): + user = request.user # Usuario autenticado + if not user: + return jsonify({"error": "User not found"}), 404 + + # Calcular contadores + pastes_count = Paste.query.filter_by(owner_id=user.id).count() + favorites_count = user.favorite_pastes.count() + shared_with_me_count = db.session.query(Paste).join(shared_pastes).filter( + shared_pastes.c.user_id == user.id + ).count() + shared_with_others_count = db.session.query(Paste).join(shared_pastes).filter( + Paste.owner_id == user.id, + shared_pastes.c.user_id != user.id + ).count() + + # Construir la respuesta + response = { + "id": user.id, + "username": user.username, + "role": user.role, + "storage_used": user.storage_used, + "storage_limit": user.storage_limit, + "storage_remaining": max(user.storage_limit - user.storage_used, 0) if user.storage_limit != -1 else None, + "theme_preference": user.theme_preference, + # Suponiendo que tienes un campo user.created_at + "created_at": user.created_at.isoformat() if hasattr(user, 'created_at') and user.created_at else None, + "pastes_count": pastes_count, + "favorites_count": favorites_count, + "shared_with_me_count": shared_with_me_count, + "shared_with_others_count": shared_with_others_count + } + + return jsonify(response), 200 + + + @app.route('/admin/users//change_role', methods=['POST']) + def change_user_role(user_id): + if 'admin' not in session: + flash('Please log in to access the admin panel', 'warning') + return redirect(url_for('admin_login')) + + try: + user = User.query.get(user_id) + if not user: + return jsonify({"error": "User not found"}), 404 + + new_role = request.form.get('role') + valid_roles = ['admin', 'advanced', 'user'] + if new_role not in valid_roles: + return jsonify({"error": "Invalid role"}), 400 + + # Actualizar rol y límite de almacenamiento + user.role = new_role + user.storage_limit = ROLE_STORAGE_LIMITS[new_role] + db.session.commit() + + return jsonify({"message": f"User role updated to {new_role}"}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Failed to update role: {str(e)}"}), 500 + + @app.route('/logout') + @login_required + def logout(): + logout_user() + flash("You have been logged out.", "info") + return redirect(url_for('login')) + + @app.route('/user-dashboard', methods=['GET']) + @login_required + def user_dashboard(): + user = current_user # Usuario autenticado automáticamente + + # Obtener el número de página desde los parámetros de consulta, por defecto es 1 + page = request.args.get('page', 1, type=int) + per_page = 10 # Número de pastes por página + favorite_page = request.args.get('favorite_page', 1, type=int) + + # Consultar los pastes del usuario con paginación + pastes_query = Paste.query.filter_by(user_id=user.id).order_by(Paste.created_at.desc()) + pagination = pastes_query.paginate(page=page, per_page=per_page, error_out=False) + pastes = pagination.items + + # Consultar los pastes favoritos del usuario con paginación + favorite_query = Paste.query.filter(Paste.favorites.any(id=user.id)) + favorite_pagination = favorite_query.paginate(page=favorite_page, per_page=10, error_out=False) + favorite_pastes = favorite_pagination.items + + # Consultar los pastes compartidos con el usuario + shared_pastes_list = Paste.query.join(shared_pastes, (shared_pastes.c.paste_id == Paste.id))\ + .filter(shared_pastes.c.user_id == user.id)\ + .all() + + # Calcular estadísticas generales + stats = calculate_stats(user.id) # Asegúrate de que esta función devuelve un diccionario con las claves usadas a continuación + + # **Nuevo Código: Cálculo de Almacenamiento** + + # Obtener el rol del usuario + user_role = user.role.lower() if user.role else 'user' # Asegúrate de que el campo 'role' existe en el modelo User + + # Obtener el límite de almacenamiento basado en el rol + storage_limits = current_app.config.get('ROLE_STORAGE_LIMITS', { + 'admin': -1, # Ilimitado + 'advanced': 2 * 1024**3, # 2GB en bytes + 'user': 1 * 1024**3, # 1GB en bytes + }) + + # Obtener límite en bytes y convertir a MB + storage_limit = storage_limits.get(user_role, 1 * 1024**3) # 1GB en bytes + storage_used = db.session.query(db.func.sum(Paste.size)).filter_by(user_id=user.id).scalar() or 0 + + # Calcular almacenamiento disponible + storage_available = storage_limit - storage_used if storage_limit != -1 else -1 + storage_available = max(storage_available, 0) # Evita valores negativos + + # CONVERSIÓN A MB + storage_used_mb = round(storage_used / (1024**2), 2) + storage_limit_mb = -1 if storage_limit == -1 else round(storage_limit / (1024**2), 2) + storage_available_mb = -1 if storage_available == -1 else round(storage_available / (1024**2), 2) + + # DEBUG LOG PARA COMPROBAR QUE SE CONVIERTE BIEN + app.logger.debug(f"Storage (MB) - Used: {storage_used_mb}, Limit: {storage_limit_mb}, Available: {storage_available_mb}") + + # Información del perfil del usuario (sin email ni bio) + user_profile = { + 'username': user.username, + 'role': user.role.capitalize() if user.role else 'User', + } + + return render_template( + 'user_dashboard.html', + user=user, + total_pastes=stats['total_pastes'], + total_size=stats['total_size'], + pastes=pastes, + pagination=pagination, # Pasar el objeto de paginación al template + total_text_pastes=stats.get('total_text_pastes', 0), + total_file_pastes=stats.get('total_file_pastes', 0), + total_media_pastes=stats.get('total_media_pastes', 0), + languages=stats.get('languages', []), + counts_text=stats.get('counts_text', []), + counts_file=stats.get('counts_file', []), + counts_media=stats.get('counts_media', []), + favorite_pastes=favorite_pastes, + favorite_pagination=favorite_pagination, + shared_pastes=shared_pastes_list, + storage_used=storage_used_mb, + storage_limit=storage_limit_mb, + storage_available=storage_available_mb, + user_profile=user_profile, + ) + + + @app.route('/paste/', methods=['DELETE']) + @jwt_required + def delete_paste(id): + paste = Paste.query.get(id) + if not paste: + return jsonify({"error": "Paste not found"}), 404 + + # Permitir eliminación solo si es el propietario o un administrador + if paste.owner_id != request.user.id and request.user.username != 'admin': + return jsonify({"error": "You do not have permission to delete this paste"}), 403 + + try: + # Eliminar favoritos asociados + paste.favorites.clear() + + # Eliminar el archivo si existe + if paste.filename: + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if os.path.exists(file_path): + os.remove(file_path) + + delete_paste_from_index(paste) + paste.shared_with.clear() + db.session.delete(paste) + db.session.commit() + return jsonify({"message": "Paste deleted successfully"}), 200 + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Error deleting paste: {str(e)}"}), 500 + + + @app.route('/change-password-form', methods=['GET']) + @login_required + def change_password_form(): + return render_template('change_password.html') + + + @app.route('/change-password', methods=['POST']) + @login_required + def change_password(): + current_password = request.form.get('current_password') + new_password = request.form.get('new_password') + confirm_password = request.form.get('confirm_password') + + # Validar que los campos no estén vacíos + if not current_password or not new_password or not confirm_password: + flash("All fields are required.", "danger") + return redirect(url_for('change_password_form')) + + # Verificar que las contraseñas coincidan + if new_password != confirm_password: + flash("New passwords do not match.", "danger") + return redirect(url_for('change_password_form')) + + # Verificar contraseña actual + if not current_user.check_password(current_password): # Método check_password en el modelo User + flash("Current password is incorrect.", "danger") + return redirect(url_for('change_password_form')) + + # Actualizar la contraseña + try: + current_user.set_password(new_password) # Método set_password en el modelo User + db.session.commit() + flash("Password updated successfully.", "success") + except Exception as e: + db.session.rollback() + flash(f"An error occurred: {str(e)}", "danger") + + return redirect(url_for('user_dashboard')) + + @app.context_processor + def inject_pygments_styles(): + styles = list(get_all_styles()) + current_theme = session.get('theme', 'light') # Obtener el tema actual de la sesión + default_pygments_style = 'monokai' if current_theme == 'dark' else 'default' + + # Obtener el nombre del usuario si está logueado + user_name = None + if 'user_id' in session: + user = User.query.get(session['user_id']) + user_name = user.username if user else None + + return dict( + pygments_styles=styles, + default_pygments_style=default_pygments_style, + user_name=user_name + ) + + @app.route('/paste//download_page', methods=['GET']) + def download_page(id): + paste = Paste.query.get(id) + if not paste: + flash("Paste no encontrado", "danger") + return redirect(url_for('index')) + + return render_template('download_page.html', paste=paste) + + @app.route('/paste//download_file', methods=['GET']) + def download_file(id): + logging.info(f"Confirmed download request for paste ID: {id}") + paste = Paste.query.get(id) + if not paste: + logging.warning(f"Paste ID {id} not found.") + return jsonify({"error": "Paste not found"}), 404 + + if paste.filename: # El paste está asociado a un archivo físico + logging.info(f"Handling confirmed file download for paste ID {id} with filename: {paste.filename}") + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + + if not os.path.exists(file_path): + logging.error(f"File {paste.filename} for paste ID {id} not found.") + return jsonify({"error": "File not found"}), 404 + + return send_from_directory( + directory=UPLOAD_FOLDER, + path=paste.filename, + as_attachment=True, + download_name=paste.filename, + mimetype=paste.content_type + ) + + elif paste.content: # El paste es de texto y está almacenado en la base de datos + logging.info(f"Handling confirmed text paste ID {id}") + + language_suffix = f".{paste.language}" if paste.language and paste.language != 'unknown' else ".txt" + filename = f"paste_{paste.id}{language_suffix}" + + file_stream = BytesIO() + file_stream.write(paste.content.encode('utf-8')) + file_stream.seek(0) + return send_file( + file_stream, + as_attachment=True, + download_name=filename, + mimetype='text/plain' + ) + else: + logging.error(f"Paste ID {id} has no associated content.") + return jsonify({"error": "Paste has no associated content"}), 400 + + + @app.route('/paste//stream_download', methods=['GET']) + def stream_download(id): + paste = Paste.query.get(id) + if not paste or not paste.filename: + return jsonify({"error": "Paste or file not found"}), 404 + + def generate(): + with open(os.path.join(UPLOAD_FOLDER, paste.filename), 'rb') as f: + while True: + chunk = f.read(4096) + if not chunk: + break + yield chunk + + response = Response(generate(), mimetype=paste.content_type) + response.headers.set('Content-Disposition', 'attachment', filename=paste.filename) + return response + + @app.route('/user/paste//delete', methods=['POST']) + @login_required + def user_delete_paste(id): + paste = Paste.query.get_or_404(id) + + # Verificar si el usuario es el propietario + if paste.owner_id != current_user.id: + return jsonify({"error": "You do not have permission to delete this paste"}), 403 + + try: + # Eliminar favoritos asociados + paste.favorites.clear() + + # Eliminar el archivo asociado, si existe + if paste.filename: + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if os.path.exists(file_path): + os.remove(file_path) + + # Eliminar el paste de la base de datos + delete_paste_from_index(paste) + db.session.delete(paste) + db.session.commit() + return jsonify({"message": "Paste deleted successfully"}), 200 + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Error deleting paste: {str(e)}"}), 500 + + @app.route('/user//stats', methods=['GET']) + @login_required + def user_stats(username): + if current_user.username != username: + abort(404) + + start_date_str = request.args.get('start_date') + end_date_str = request.args.get('end_date') + + app.logger.debug(f"Start Date: {start_date_str}, End Date: {end_date_str}") + + paste_filters = [] + start_date = None + end_date = None + + try: + if start_date_str: + start_date = datetime.strptime(start_date_str, '%Y-%m-%d') + paste_filters.append(Paste.created_at >= start_date) + if end_date_str: + end_date = datetime.strptime(end_date_str, '%Y-%m-%d').replace(hour=23, minute=59, second=59) + paste_filters.append(Paste.created_at <= end_date) + except ValueError as ve: + app.logger.error(f"Error al parsear las fechas: {ve}") + start_date = None + end_date = None + + # Obtener los pastes y favoritos con filtros aplicados + page = request.args.get('page', 1, type=int) + per_page = 10 + pastes_query = Paste.query.filter_by(user_id=current_user.id).order_by(Paste.created_at.desc()) + if paste_filters: + pastes_query = pastes_query.filter(*paste_filters) + pagination = pastes_query.paginate(page=page, per_page=per_page, error_out=False) + + favorite_query = Paste.query.filter(Paste.favorites.any(id=current_user.id)) + if paste_filters: + favorite_query = favorite_query.filter(*paste_filters) + favorite_pagination = favorite_query.paginate(page=1, per_page=10, error_out=False) + + # Calcular estadísticas + stats = calculate_stats(current_user.id, start_date, end_date) + + # Calcular almacenamiento con valores seguros + storage_used = 0 + storage_limit = -1 + storage_available = -1 + + try: + storage_used = calculate_storage_used(current_user.id) + storage_limit = current_user.get_storage_limit() or -1 + + app.logger.debug(f"current_user: {current_user}, type: {type(current_user)}") + + storage_available = max(storage_limit - storage_used, 0) if storage_limit != -1 else -1 + + # CONVERSIÓN A MB + storage_used_mb = round(storage_used / (1024**2), 2) + storage_limit_mb = -1 if storage_limit == -1 else round(storage_limit / (1024**2), 2) + storage_available_mb = -1 if storage_available == -1 else round(storage_available / (1024**2), 2) + + app.logger.debug(f"Storage (MB) - Used: {storage_used_mb}, Limit: {storage_limit_mb}, Available: {storage_available_mb}") + except Exception as e: + app.logger.error(f"Error al calcular el almacenamiento: {e}") + storage_used_mb = 0 + storage_limit_mb = -1 + storage_available_mb = -1 + + return render_template( + 'user_dashboard.html', + user=current_user, + user_profile=current_user.role, + total_pastes=stats['total_pastes'], + total_size=stats['total_size'], + total_text_pastes=stats['total_text_pastes'], + total_file_pastes=stats['total_file_pastes'], + total_media_pastes=stats['total_media_pastes'], + languages=stats['languages'], + counts_text=stats['counts_text'], + counts_file=stats['counts_file'], + counts_media=stats['counts_media'], + pastes=pagination.items, + pagination=pagination, + favorite_pastes=favorite_pagination.items, + favorite_pagination=favorite_pagination, + shared_pastes=shared_pagination.items, + shared_pagination=shared_pagination, + start_date=start_date_str, + end_date=end_date_str, + storage_used=storage_used_mb, + storage_limit=storage_limit_mb, + storage_available=storage_available_mb, + ) + + @app.route('/api/token', methods=['POST']) + def get_token(): + """ + Endpoint dedicado para generar un token JWT. + """ + data = request.json + if not data or 'username' not in data or 'password' not in data: + return jsonify({"error": "Username and password are required"}), 400 + + username = data['username'] + password = data['password'] + + user = User.query.filter_by(username=username).first() + if user and check_password_hash(user.password_hash, password): + token = generate_token(username) + return jsonify({"token": token}), 200 + + return jsonify({"error": "Invalid username or password"}), 401 + + @app.route('/paste//favorite', methods=['POST']) + @login_required + def add_to_favorites(id): + user = current_user # Usuario autenticado + paste = Paste.query.get_or_404(id) + + # Verificar si ya es favorito + if user in paste.favorites: + # Sigues devolviendo 200, pero ahora con "success": True + return jsonify({ + "success": True, + "message": "Paste already in favorites" + }), 200 + + try: + paste.favorites.append(user) + db.session.commit() + # ¡Aquí pones success: True! + return jsonify({ + "success": True, + "message": "Paste added to favorites" + }), 201 + except Exception as e: + db.session.rollback() + return jsonify({"error": str(e), "success": False}), 500 + + @app.route('/paste//unfavorite', methods=['POST']) + @login_required + def remove_from_favorites(id): + user = current_user # Usuario autenticado + paste = Paste.query.get_or_404(id) + + if user not in paste.favorites: + # Devuelve success: False + return jsonify({"error": "Paste not in favorites", "success": False}), 404 + + try: + paste.favorites.remove(user) + db.session.commit() + return jsonify({ + "success": True, + "message": "Paste removed from favorites" + }), 200 + except Exception as e: + db.session.rollback() + return jsonify({"error": str(e), "success": False}), 500 + + + @app.route('/favorites', methods=['GET']) + @login_required + def list_favorites(): + user = current_user + page = request.args.get('page', 1, type=int) + per_page = 10 + favorite_query = Favorite.query.filter_by(user_id=user.id) + pagination = favorite_query.paginate(page=page, per_page=per_page, error_out=False) + + favorites = [{ + "id": fav.paste.id, + "title": fav.paste.filename, + "type": fav.paste.get_type(), # Tipo de archivo + "created_at": fav.paste.created_at.strftime('%Y-%m-%d %H:%M:%S') # Fecha de creación + } for fav in pagination.items] + + return jsonify({ + "favorites": favorites, + "pagination": { + "page": pagination.page, + "per_page": pagination.per_page, + "total_pages": pagination.pages, + "total_items": pagination.total, + "has_next": pagination.has_next, + "has_prev": pagination.has_prev, + "next_page": pagination.next_num if pagination.has_next else None, + "prev_page": pagination.prev_num if pagination.has_prev else None + } + }) + + + @app.route('/media/', methods=['GET']) + def serve_media(id): + paste = Paste.query.get_or_404(id) + + # Ruta completa del archivo + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + + if not os.path.exists(file_path): + return "File not found", 404 + + # Sirve el archivo directamente como contenido de video + return send_file(file_path, mimetype=paste.content_type) + + @app.route('/create_paste_web', methods=['GET', 'POST']) + @login_required + def create_paste_web(): + if request.method == 'POST': + title = None + language = request.form.get('language') or '' # Puede venir vacío + paste_type = request.form.get('type') + content = request.form.get('content') + file = request.files.get('file') + + # Validar campos obligatorios + if (paste_type == 'Text' and not content) or (paste_type != 'Text' and not file): + flash("All fields are required.", "danger") + return redirect(url_for('create_paste_web')) + + try: + filename = None + content_type = None + size = 0 + + if file: + # Caso: El usuario ha subido un archivo + original_filename = secure_filename(file.filename) + filename = f"{uuid.uuid4().hex}_{original_filename}" + file_path = os.path.join(UPLOAD_FOLDER, filename) + file.save(file_path) + content_type = mime.from_file(file_path) + size = os.path.getsize(file_path) + + elif paste_type == 'Text': + # Caso: El usuario eligió “Text” + + # Buscar la extensión adecuada al "language" del dropdown. + ext = LANGUAGE_TO_EXTENSION.get(language.lower(), "txt") + # Generar el nombre con esa extensión + filename = f"{uuid.uuid4().hex}.{ext}" + + file_path = os.path.join(UPLOAD_FOLDER, filename) + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + + content_type = 'text/plain' + size = len(content) + + # Crear registro de Paste en la BD + paste = Paste( + title=title, + content_type=content_type, + filename=filename, + owner_id=current_user.id, + user_id=current_user.id, + language=language, + size=size + ) + + db.session.add(paste) + db.session.commit() + index_paste(paste) + + flash("Paste created successfully!", "success") + return redirect(url_for('get_paste', id=paste.id)) + + except Exception as e: + db.session.rollback() + flash(f"An error occurred: {e}", "danger") + return redirect(url_for('create_paste_web')) + + # Si es GET, mostrar formulario + return render_template('create_paste_web.html') + + +# Función para descargar archivos + @app.route('/paste//download', methods=['GET']) + @login_required + def download_paste_file(id): + paste = Paste.query.get_or_404(id) + + if paste.owner_id != current_user.id: + flash('No tienes permiso para descargar este archivo.', 'danger') + return redirect(url_for('get_paste', id=id)) + + if not paste.filename: + flash('Este paste no tiene un archivo asociado.', 'warning') + return redirect(url_for('get_paste', id=id)) + + file_path = os.path.join(app.config['UPLOAD_FOLDER'], paste.filename) + + if not os.path.exists(file_path): + flash('El archivo no existe.', 'danger') + return redirect(url_for('get_paste', id=id)) + + return send_from_directory(app.config['UPLOAD_FOLDER'], paste.filename, as_attachment=True) + + @app.route('/paste/') + @login_required + def view_paste(paste_id): + paste = Paste.query.get_or_404(paste_id) + # Verificar que el usuario es el propietario o tiene permisos para ver el paste + if paste.owner_id != current_user.id: + flash('No tienes permiso para ver este paste.', 'danger') + return redirect(url_for('user_dashboard')) + return render_template('view_paste.html', paste=paste) + + @app.route('/pastes/search', methods=['GET']) + @jwt_required + def search_pastes(): + try: + # Obtener el query string + query = request.args.get('q', '') + if not query: + return jsonify({"error": "Search query is required"}), 400 + + # Obtener el usuario autenticado + user = request.user + if not user: + return jsonify({"error": "User not authenticated"}), 403 + + # Realizar la búsqueda en Elasticsearch + body = { + "query": { + "bool": { + "must": [ + {"match": {"content": query}} + ], + "filter": [ + {"term": {"owner_id": user.id}} + ] + } + } + } + results = es.search(index="pastes", body=body) + + # Construir la respuesta con la URL + base_url = request.host_url.rstrip('/') + hits = results["hits"]["hits"] + response = [{"id": hit["_id"], "url": f"{base_url}/paste/{hit['_id']}", "content": hit["_source"].get("content", "")} for hit in hits] + return jsonify(response), 200 + + except Exception as e: + current_app.logger.error(f"Error searching pastes: {e}") + return jsonify({"error": "Error searching pastes"}), 500 + + @app.route('/pastes/search_web', methods=['GET']) + @login_required + def search_pastes_web(): + try: + query = request.args.get('q', '').strip() + content_type = request.args.get('content_type', '').strip() + language = request.args.get('language', '').strip() + + # Obtener al usuario autenticado + user = current_user + if not user.is_authenticated: + return redirect(url_for('login')) + + # Construir la consulta para Elasticsearch + must_clauses = [] + if query: + must_clauses.append({"match": {"content": query}}) + if content_type: + must_clauses.append({"term": {"content_type": content_type}}) + if language: + must_clauses.append({"term": {"language": language}}) + + body = { + "query": { + "bool": { + "must": must_clauses, + "filter": [{"term": {"owner_id": user.id}}] + } + } + } + + # Consultar Elasticsearch + results = es.search(index="pastes", body=body) + hits = results["hits"]["hits"] + + # Construir los resultados + pastes = [ + { + "id": hit["_id"], + "url": f"{request.host_url.rstrip('/')}/paste/{hit['_id']}", + "content_type": hit["_source"].get("content_type", ""), + "language": hit["_source"].get("language", ""), + } + for hit in hits + ] + + # Obtener favoritos del usuario + favorite_query = Paste.query.filter(Paste.favorites.any(id=user.id)) + favorite_pagination = favorite_query.paginate(page=1, per_page=10, error_out=False) + favorite_pastes = favorite_pagination.items + + # Renderizar los resultados en la plantilla + return render_template( + 'search_results.html', + pastes=pastes, # Resultados de búsqueda + query=query, + content_type=content_type, + language=language, + favorite_pastes=favorite_pastes, # Lista de favoritos + favorite_pagination=favorite_pagination # Paginación de favoritos + ) + + except Exception as e: + current_app.logger.error(f"Error in web search: {e}") + return render_template('error.html', message="Search failed"), 500 + + @app.route('/api/paste//favorite', methods=['POST']) + @jwt_required + def api_add_to_favorites(id): + try: + # Usuario autenticado + user = request.user + paste = Paste.query.get_or_404(id) + + # Verificar si ya es favorito + if paste in user.favorite_pastes: + return jsonify({"message": "Paste already in favorites"}), 200 + + # Añadir a favoritos + user.favorite_pastes.append(paste) + db.session.commit() + return jsonify({"message": "Paste added to favorites"}), 201 + + except Exception as e: + logging.error(f"Error adding paste to favorites: {e}") + db.session.rollback() + return jsonify({"error": str(e)}), 500 + @app.route('/api/paste//unfavorite', methods=['POST']) + @jwt_required + def api_remove_from_favorites(id): + try: + # Usuario autenticado + user = request.user + paste = Paste.query.get_or_404(id) + + # Verificar si no está en favoritos + if paste not in user.favorite_pastes: + return jsonify({"message": "Paste is not in favorites"}), 200 + + # Quitar de favoritos + user.favorite_pastes.remove(paste) + db.session.commit() + return jsonify({"message": "Paste removed from favorites"}), 200 + + except Exception as e: + logging.error(f"Error removing paste from favorites: {e}") + db.session.rollback() + return jsonify({"error": str(e)}), 500 + + @app.route('/api/paste//download', methods=['GET']) + @jwt_required + def api_download_paste_file(id): + user = request.user # Usuario autenticado mediante JWT + paste = Paste.query.get_or_404(id) + + # Verificar permisos + if paste.owner_id != user.id: + return jsonify({"error": "You do not have permission to download this file"}), 403 + + # Verificar que el paste tiene un archivo asociado + if not paste.filename: + return jsonify({"error": "This paste does not have an associated file"}), 400 + + # Construir la ruta completa del archivo + file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], paste.filename) + + # Verificar que el archivo exista + if not os.path.exists(file_path): + return jsonify({"error": "The file does not exist"}), 404 + + # Determinar el tipo MIME del archivo + mime_type = paste.content_type or mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + + # Enviar el archivo con el nombre original y tipo MIME + return send_file( + file_path, + as_attachment=True, + download_name=paste.filename, # Flask >= 2.0 + mimetype=mime_type + ) + + @app.route('/api/favorites', methods=['GET']) + @jwt_required + def api_list_favorites(): + try: + # Obtener el usuario autenticado + user = request.user + + # Obtener todos los favoritos del usuario + favorite_pastes = user.favorite_pastes # Relación definida en el modelo User + + if not favorite_pastes: + return jsonify({"message": "No favorites found"}), 200 + + base_url = request.host_url.rstrip('/') + response_list = [] + for paste in favorite_pastes: + response_list.append({ + "id": paste.id, + "url": f"{base_url}/paste/{paste.id}", + "title": paste.title or paste.filename, + "type": paste.get_type(), # Método definido en tu modelo Paste + "size": paste.size or 0, # Devuelve 0 si el tamaño no está definido + "created_at": paste.created_at.strftime('%Y-%m-%d %H:%M:%S') if paste.created_at else None + }) + + return jsonify(response_list), 200 + except Exception as e: + logging.error(f"Error retrieving favorites: {e}") + return jsonify({"error": "Error retrieving favorites"}), 500 + + @app.route('/paste//toggle_editable', methods=['POST']) + @login_required + def toggle_editable(id): + paste = Paste.query.get_or_404(id) + + # Verificar que el usuario es el propietario + if paste.owner_id != current_user.id: + return jsonify({"error": "No tienes permiso para modificar este paste"}), 403 + + try: + # Toggle el estado de editable + paste.editable = not paste.editable + db.session.commit() + + status = "enabled" if paste.editable else "disabled" + message = f"Editable {status} successfully." + return jsonify({"success": True, "editable": paste.editable, "message": message}), 200 + + except Exception as e: + db.session.rollback() + logging.error(f"Error toggling editable: {e}") + return jsonify({"success": False, "error": "Error al actualizar el estado editable"}), 500 + + @app.route('/paste//edit', methods=['GET', 'POST']) + @login_required + def edit_paste_web(id): + paste = Paste.query.get_or_404(id) + + # Verificar permisos de edición + if not paste.has_edit_permission(current_user): + flash("No tienes permiso para editar este paste.", "danger") + return redirect(url_for('get_paste', id=id)) + + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) if paste.filename else None + + # Manejar GET y POST + if request.method == 'POST': + new_content = request.form.get('content') + if not new_content: + flash("El contenido no puede estar vacío.", "danger") + return redirect(url_for('edit_paste_web', id=id)) + + try: + # 1. Eliminar el paste del índice anterior en Elasticsearch + delete_paste_from_index(paste) + + # 2. Guardar el contenido actualizado en el archivo + with open(file_path, 'w', encoding='utf-8') as f: + f.write(new_content) + + # Actualizar la información del paste en la base de datos + paste.last_edited_at = datetime.utcnow() + db.session.commit() + + # 3. Indexar nuevamente el paste actualizado en Elasticsearch + index_paste(paste) + + flash("Paste actualizado correctamente.", "success") + return redirect(url_for('get_paste', id=id)) + except Exception as e: + db.session.rollback() + flash(f"Ocurrió un error al guardar el paste: {str(e)}", "danger") + return redirect(url_for('edit_paste_web', id=id)) + + # Cargar contenido actual para la edición + current_content = "" + if file_path and os.path.exists(file_path): + try: + with open(file_path, 'r', encoding='utf-8') as f: + current_content = f.read() + except Exception as e: + flash(f"Error al leer el contenido del paste: {str(e)}", "danger") + + return render_template('edit_paste.html', paste=paste, current_content=current_content) + + + @app.route('/paste//share', methods=['POST']) + @login_required + def share_paste(id): + data = request.json + username = data.get('username') + can_edit = data.get('can_edit', False) + + # Buscar al usuario con el que se compartirá el paste + user = User.query.filter_by(username=username).first() + if not user: + return jsonify({"error": "User not found"}), 404 + + # Buscar el paste + paste = Paste.query.get_or_404(id) + if paste.owner_id != current_user.id: + return jsonify({"error": "You do not own this paste"}), 403 + + try: + # Verificar si ya existe el registro + existing_entry = db.session.query(shared_pastes).filter_by( + paste_id=paste.id, + user_id=user.id + ).first() + + if existing_entry: + # Si el registro existe, actualizar el valor de `can_edit` + db.session.execute( + shared_pastes.update() + .where( + (shared_pastes.c.paste_id == paste.id) & + (shared_pastes.c.user_id == user.id) + ) + .values(can_edit=can_edit) + ) + else: + # Si el registro no existe, insertar uno nuevo + stmt = shared_pastes.insert().values( + paste_id=paste.id, + user_id=user.id, + can_edit=can_edit + ) + db.session.execute(stmt) + + db.session.commit() + return jsonify({"message": f"Paste shared with {username}, can_edit: {can_edit}."}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Error sharing paste: {str(e)}"}), 500 + + + @app.route('/paste//check-permissions', methods=['GET']) + @login_required + def check_permissions(id): + paste = Paste.query.get_or_404(id) + if paste.owner_id == current_user.id: + return jsonify({'can_edit': True}), 200 + + shared_paste = SharedPaste.query.filter_by(paste_id=paste.id, user_id=current_user.id).first() + if shared_paste and shared_paste.can_edit: + return jsonify({'can_edit': True}), 200 + + return jsonify({'can_edit': False}), 403 + + @app.route('/api/shared_with_others', methods=['GET']) + @jwt_required + def shared_with_others(): + """ + Devuelve los pastes compartidos por el usuario autenticado con otros usuarios. + """ + try: + user = request.user # Usuario autenticado + + # Obtener los pastes compartidos por el usuario actual con otros + shared_pastes_query = db.session.query(Paste).join(shared_pastes).filter( + shared_pastes.c.user_id != user.id, # Excluir al propietario como destinatario + Paste.owner_id == user.id # El usuario es el propietario del paste + ).all() + + # Construir respuesta con información de usuarios (usando joins para obtener el username) + pastes = [] + for paste in shared_pastes_query: + shared_users = db.session.query(User.username, shared_pastes.c.can_edit).join( + shared_pastes, shared_pastes.c.user_id == User.id + ).filter( + shared_pastes.c.paste_id == paste.id, + shared_pastes.c.user_id != user.id # Excluir al propietario + ).all() + + shared_with_list = [ + {"username": shared_user.username, "can_edit": shared_user.can_edit} + for shared_user in shared_users + ] + + pastes.append({ + "id": paste.id, + "title": paste.title or "Untitled", + "created_at": paste.created_at.strftime('%Y-%m-%d %H:%M:%S'), + "shared_with": shared_with_list + }) + + return jsonify({"shared_with_others": pastes}), 200 + except Exception as e: + logging.error(f"Error retrieving shared_with_others: {e}") + return jsonify({"error": "Error retrieving shared_with_others"}), 500 + + + @app.route('/api/shared_with_me', methods=['GET']) + @jwt_required + def shared_with_me(): + """ + Devuelve los pastes compartidos con el usuario autenticado. + """ + try: + user = request.user # Usuario autenticado + + # Obtener los pastes compartidos con el usuario actual + shared_pastes_query = db.session.query(Paste).join(shared_pastes).filter( + shared_pastes.c.user_id == user.id # Pastes compartidos con el usuario + ).all() + + # Construir respuesta + pastes = [ + { + "id": paste.id, + "title": paste.title or "Untitled", + "created_at": paste.created_at.strftime('%Y-%m-%d %H:%M:%S'), + "owner": paste.owner.username, + "can_edit": db.session.query(shared_pastes).filter_by( + paste_id=paste.id, user_id=user.id + ).first().can_edit + } + for paste in shared_pastes_query + ] + + return jsonify({"shared_with_me": pastes}), 200 + except Exception as e: + logging.error(f"Error retrieving shared_with_me: {e}") + return jsonify({"error": "Error retrieving shared_with_me"}), 500 + + + @app.route('/api/paste//share', methods=['POST']) + @jwt_required # Asegúrate de usar el decorador correcto + def add_share_paste(paste_id): + try: + # Obtener el usuario actual + current_user = request.user + if not current_user: + return jsonify({"error": "User not found in request"}), 401 + + # Verificar que el paste pertenece al usuario autenticado + paste = Paste.query.filter_by(id=paste_id, owner_id=current_user.id).first() + if not paste: + return jsonify({"error": "Paste not found or you do not own it"}), 404 + + # Obtener datos del cuerpo de la solicitud + data = request.get_json() + if not data: + return jsonify({"error": "Invalid JSON data"}), 400 + + username = data.get("username") + can_edit = data.get("can_edit", False) + + if not username: + return jsonify({"error": "Username is required"}), 400 + + # Prevenir que el usuario comparta el paste consigo mismo + if username.lower() == current_user.username.lower(): + return jsonify({"error": "You cannot share a paste with yourself"}), 400 + + # Verificar que el usuario destinatario existe (sin sensibilidad a mayúsculas) + recipient = User.query.filter(func.lower(User.username) == username.lower()).first() + if not recipient: + return jsonify({"error": f"User '{username}' not found"}), 404 + + # Verificar si ya se ha compartido el paste con este usuario + existing_share = db.session.query(shared_pastes).filter( + shared_pastes.c.paste_id == paste_id, + shared_pastes.c.user_id == recipient.id + ).first() + + if existing_share: + return jsonify({"error": f"Paste is already shared with '{username}'"}), 400 + + # Asegurarse de que can_edit es un booleano + if not isinstance(can_edit, bool): + return jsonify({"error": "can_edit must be a boolean value"}), 400 + + # Añadir el registro en shared_pastes + db.session.execute(shared_pastes.insert().values( + paste_id=paste_id, + user_id=recipient.id, + can_edit=can_edit + )) + db.session.commit() + + # Registrar la acción para auditoría + app.logger.info(f"User '{current_user.username}' shared paste ID {paste_id} with '{recipient.username}' with can_edit={can_edit}") + + return jsonify({"message": f"Paste shared successfully with '{username}'", "can_edit": can_edit}), 200 + + except SQLAlchemyError as e: + db.session.rollback() + app.logger.error(f"Database error while sharing paste: {e}") + return jsonify({"error": "Database error", "details": str(e)}), 500 + except Exception as e: + app.logger.error(f"Unexpected error while sharing paste: {e}") + return jsonify({"error": "An unexpected error occurred", "details": str(e)}), 500 + + + @app.route('/api/paste//unshare', methods=['POST']) + @jwt_required + def unshare_paste(paste_id): + try: + # Verificar que el usuario autenticado está configurado correctamente + current_user = request.user + if not current_user: + return jsonify({"error": "User not found in request"}), 401 + + # Verificar que el paste pertenece al usuario autenticado + paste = Paste.query.filter_by(id=paste_id, owner_id=current_user.id).first() + if not paste: + return jsonify({"error": "Paste not found or you do not own it"}), 404 + + # Obtener datos del cuerpo de la solicitud + data = request.get_json() + username = data.get("username") + + if not username: + return jsonify({"error": "Username is required"}), 400 + + # Verificar que el usuario destinatario existe + recipient = User.query.filter_by(username=username).first() + if not recipient: + return jsonify({"error": f"User {username} not found"}), 404 + + # Verificar si el paste está compartido con este usuario + existing_share = db.session.query(shared_pastes).filter( + shared_pastes.c.paste_id == paste_id, + shared_pastes.c.user_id == recipient.id + ).scalar() + + if not existing_share: + return jsonify({"error": f"Paste is not shared with {username}"}), 400 + + # Eliminar el registro en shared_pastes + db.session.execute( + shared_pastes.delete().where( + (shared_pastes.c.paste_id == paste_id) & + (shared_pastes.c.user_id == recipient.id) + ) + ) + db.session.commit() + + return jsonify({"message": f"Paste unshared successfully from {username}"}), 200 + except Exception as e: + return jsonify({"error": "An unexpected error occurred", "details": str(e)}), 500 + + @app.route('/paste//unshare', methods=['POST']) + @login_required + def unshare_paste_web(paste_id): + """ + Versión con sesión/logueo normal para 'descompartir' un paste. + """ + try: + # Tomar el username del formulario o del JSON (como prefieras) + # Si lo envías desde un fetch con JSON, usas request.get_json() + # Si lo envías desde un formulario , usas request.form + data = request.get_json() or {} + username = data.get("username") + + if not username: + return jsonify({"error": "Username is required"}), 400 + + # Verificar que el paste pertenece al usuario logueado + paste = Paste.query.filter_by(id=paste_id, owner_id=current_user.id).first() + if not paste: + return jsonify({"error": "Paste not found or you do not own it"}), 404 + + # Verificar que el usuario con el que se compartió existe + recipient = User.query.filter_by(username=username).first() + if not recipient: + return jsonify({"error": f"User {username} not found"}), 404 + + # Verificar si está compartido + existing_share = db.session.query(shared_pastes).filter( + (shared_pastes.c.paste_id == paste_id), + (shared_pastes.c.user_id == recipient.id) + ).scalar() + + if not existing_share: + return jsonify({"error": f"Paste is not shared with {username}"}), 400 + + # Eliminar el registro de shared_pastes + db.session.execute( + shared_pastes.delete().where( + (shared_pastes.c.paste_id == paste_id) & + (shared_pastes.c.user_id == recipient.id) + ) + ) + db.session.commit() + + return jsonify({"message": f"Paste unshared from {username} successfully"}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({"error": f"An unexpected error occurred: {str(e)}"}), 500 + + @app.route('/contact', methods=['GET', 'POST']) + def contact(): + if request.method == 'POST': + email = request.form.get('email') + subject = request.form.get('subject') or "Contact Form" + message = request.form.get('message') + + # Validar campos básicos + if not email or not message: + flash("Email and message are required.", "danger") + return redirect(url_for('contact')) + + # Lógica de envío de correo (idéntica a la que ya tienes en request_account) + try: + # Construir el mensaje + msg_body = f"Message from Contact Form:\n\nEmail: {email}\nSubject: {subject}\nMessage: {message}" + msg = MIMEText(msg_body) + msg['Subject'] = subject + msg['From'] = SMTP_USERNAME # El remitente que tienes configurado + msg['To'] = SMTP_USERNAME # El correo que recibirá el mensaje + + # Conexión SMTP (mismo patrón que request_account) + if SMTP_USE_SSL: + with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as server: + server.login(SMTP_USERNAME, SMTP_PASSWORD) + server.send_message(msg) + else: + with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server: + if SMTP_USE_TLS: + server.starttls() + server.login(SMTP_USERNAME, SMTP_PASSWORD) + server.send_message(msg) + + flash("Your message has been sent!", "success") + return redirect(url_for('contact')) + + except smtplib.SMTPException as smtp_error: + flash(f"SMTP error: {smtp_error}", "danger") + return redirect(url_for('contact')) + except Exception as e: + flash(f"An error occurred: {e}", "danger") + return redirect(url_for('contact')) + + # Si es GET, renderizar plantilla con el formulario + return render_template('contact.html') + + @app.route('/api/paste/', methods=['PUT']) + @jwt_required + def update_paste_file(id): + paste = Paste.query.get_or_404(id) + user = request.user # Usuario autenticado + + # Verificar permisos + if not paste.has_edit_permission(user): + return jsonify({"error": "No permission to edit this paste."}), 403 + + data = request.json or {} + new_content = data.get('content') + if not new_content: + return jsonify({"error": "Missing 'content' in JSON"}), 400 + + file_path = os.path.join(UPLOAD_FOLDER, paste.filename) + if not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + try: + # 1. Eliminar de Elasticsearch (opcional) + delete_paste_from_index(paste) + + # 2. Guardar contenido en el archivo + with open(file_path, 'w', encoding='utf-8') as f: + f.write(new_content) + + # 3. Actualizar la fecha de edición + paste.last_edited_at = datetime.utcnow() + db.session.commit() + + # 4. Indexar de nuevo + index_paste(paste) + + return jsonify({"message": "Paste updated successfully"}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({"error": str(e)}), 500 + + @app.route('/api/users', methods=['GET']) + @jwt_required + def list_users(): + try: + users = User.query.with_entities(User.username).all() + usernames = [user.username for user in users] + return jsonify({"users": usernames}), 200 + except Exception as e: + app.logger.error(f"Error retrieving users: {e}") + return jsonify({"error": "An unexpected error occurred."}), 500 + + + @app.route('/api/users/search', methods=['GET']) + @login_required # Asegura que solo usuarios autenticados puedan acceder + def search_users(): + query = request.args.get('q', '').strip() + logging.info(f"Search query received: {query}") + if not query: + return jsonify([]) # Retorna una lista vacía si la consulta está vacía + + # Obtener todos los nombres de usuario desde la base de datos + all_users = [user.username for user in User.query.all()] + logging.info(f"Total users fetched: {len(all_users)}") + + # Realizar una búsqueda difusa utilizando RapidFuzz + matches = process.extract(query, all_users, scorer=fuzz.WRatio, limit=10) + logging.info(f"Matches found: {matches}") + + # Definir un umbral para filtrar resultados poco relevantes + threshold = 60 # Puedes ajustar este valor según tus necesidades + + # Filtrar los resultados que superen el umbral + matched_users = [username for username, score, _ in matches if score >= threshold] + logging.info(f"Matched users after threshold: {matched_users}") + + # Crear una lista de diccionarios para retornar + user_list = [{'username': username} for username in matched_users] + + return jsonify(user_list) + diff --git a/static/css/abap.css b/static/css/abap.css new file mode 100644 index 0000000..93d42a6 --- /dev/null +++ b/static/css/abap.css @@ -0,0 +1,65 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #ffffff; } +.highlight .c { color: #888888; font-style: italic } /* Comment */ +.highlight .err { color: #FF0000 } /* Error */ +.highlight .k { color: #0000ff } /* Keyword */ +.highlight .n { color: #000000 } /* Name */ +.highlight .ch { color: #888888; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #888888; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #888888; font-style: italic } /* Comment.Preproc */ +.highlight .cpf { color: #888888; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #888888; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #888888; font-style: italic } /* Comment.Special */ +.highlight .kc { color: #0000ff } /* Keyword.Constant */ +.highlight .kd { color: #0000ff } /* Keyword.Declaration */ +.highlight .kn { color: #0000ff } /* Keyword.Namespace */ +.highlight .kp { color: #0000ff } /* Keyword.Pseudo */ +.highlight .kr { color: #0000ff } /* Keyword.Reserved */ +.highlight .kt { color: #0000ff } /* Keyword.Type */ +.highlight .m { color: #33aaff } /* Literal.Number */ +.highlight .s { color: #55aa22 } /* Literal.String */ +.highlight .na { color: #000000 } /* Name.Attribute */ +.highlight .nb { color: #000000 } /* Name.Builtin */ +.highlight .nc { color: #000000 } /* Name.Class */ +.highlight .no { color: #000000 } /* Name.Constant */ +.highlight .nd { color: #000000 } /* Name.Decorator */ +.highlight .ni { color: #000000 } /* Name.Entity */ +.highlight .ne { color: #000000 } /* Name.Exception */ +.highlight .nf { color: #000000 } /* Name.Function */ +.highlight .nl { color: #000000 } /* Name.Label */ +.highlight .nn { color: #000000 } /* Name.Namespace */ +.highlight .nx { color: #000000 } /* Name.Other */ +.highlight .py { color: #000000 } /* Name.Property */ +.highlight .nt { color: #000000 } /* Name.Tag */ +.highlight .nv { color: #000000 } /* Name.Variable */ +.highlight .ow { color: #0000ff } /* Operator.Word */ +.highlight .mb { color: #33aaff } /* Literal.Number.Bin */ +.highlight .mf { color: #33aaff } /* Literal.Number.Float */ +.highlight .mh { color: #33aaff } /* Literal.Number.Hex */ +.highlight .mi { color: #33aaff } /* Literal.Number.Integer */ +.highlight .mo { color: #33aaff } /* Literal.Number.Oct */ +.highlight .sa { color: #55aa22 } /* Literal.String.Affix */ +.highlight .sb { color: #55aa22 } /* Literal.String.Backtick */ +.highlight .sc { color: #55aa22 } /* Literal.String.Char */ +.highlight .dl { color: #55aa22 } /* Literal.String.Delimiter */ +.highlight .sd { color: #55aa22 } /* Literal.String.Doc */ +.highlight .s2 { color: #55aa22 } /* Literal.String.Double */ +.highlight .se { color: #55aa22 } /* Literal.String.Escape */ +.highlight .sh { color: #55aa22 } /* Literal.String.Heredoc */ +.highlight .si { color: #55aa22 } /* Literal.String.Interpol */ +.highlight .sx { color: #55aa22 } /* Literal.String.Other */ +.highlight .sr { color: #55aa22 } /* Literal.String.Regex */ +.highlight .s1 { color: #55aa22 } /* Literal.String.Single */ +.highlight .ss { color: #55aa22 } /* Literal.String.Symbol */ +.highlight .bp { color: #000000 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #000000 } /* Name.Function.Magic */ +.highlight .vc { color: #000000 } /* Name.Variable.Class */ +.highlight .vg { color: #000000 } /* Name.Variable.Global */ +.highlight .vi { color: #000000 } /* Name.Variable.Instance */ +.highlight .vm { color: #000000 } /* Name.Variable.Magic */ +.highlight .il { color: #33aaff } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/static/css/algol.css b/static/css/algol.css new file mode 100644 index 0000000..0969273 --- /dev/null +++ b/static/css/algol.css @@ -0,0 +1,49 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #ffffff; } +.highlight .c { color: #888888; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #FF0000 } /* Error */ +.highlight .k { font-weight: bold; text-decoration: underline } /* Keyword */ +.highlight .ch { color: #888888; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #888888; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #888888; font-weight: bold } /* Comment.Preproc */ +.highlight .cpf { color: #888888; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #888888; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #888888; font-weight: bold } /* Comment.Special */ +.highlight .kc { font-weight: bold; text-decoration: underline } /* Keyword.Constant */ +.highlight .kd { font-weight: bold; font-style: italic; text-decoration: underline } /* Keyword.Declaration */ +.highlight .kn { font-weight: bold; text-decoration: underline } /* Keyword.Namespace */ +.highlight .kp { font-weight: bold; text-decoration: underline } /* Keyword.Pseudo */ +.highlight .kr { font-weight: bold; text-decoration: underline } /* Keyword.Reserved */ +.highlight .kt { font-weight: bold; text-decoration: underline } /* Keyword.Type */ +.highlight .s { color: #666666; font-style: italic } /* Literal.String */ +.highlight .nb { font-weight: bold; font-style: italic } /* Name.Builtin */ +.highlight .nc { color: #666666; font-weight: bold; font-style: italic } /* Name.Class */ +.highlight .no { color: #666666; font-weight: bold; font-style: italic } /* Name.Constant */ +.highlight .nf { color: #666666; font-weight: bold; font-style: italic } /* Name.Function */ +.highlight .nn { color: #666666; font-weight: bold; font-style: italic } /* Name.Namespace */ +.highlight .nv { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable */ +.highlight .ow { font-weight: bold } /* Operator.Word */ +.highlight .sa { color: #666666; font-style: italic } /* Literal.String.Affix */ +.highlight .sb { color: #666666; font-style: italic } /* Literal.String.Backtick */ +.highlight .sc { color: #666666; font-style: italic } /* Literal.String.Char */ +.highlight .dl { color: #666666; font-style: italic } /* Literal.String.Delimiter */ +.highlight .sd { color: #666666; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #666666; font-style: italic } /* Literal.String.Double */ +.highlight .se { color: #666666; font-style: italic } /* Literal.String.Escape */ +.highlight .sh { color: #666666; font-style: italic } /* Literal.String.Heredoc */ +.highlight .si { color: #666666; font-style: italic } /* Literal.String.Interpol */ +.highlight .sx { color: #666666; font-style: italic } /* Literal.String.Other */ +.highlight .sr { color: #666666; font-style: italic } /* Literal.String.Regex */ +.highlight .s1 { color: #666666; font-style: italic } /* Literal.String.Single */ +.highlight .ss { color: #666666; font-style: italic } /* Literal.String.Symbol */ +.highlight .bp { font-weight: bold; font-style: italic } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #666666; font-weight: bold; font-style: italic } /* Name.Function.Magic */ +.highlight .vc { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable.Class */ +.highlight .vg { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable.Global */ +.highlight .vi { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable.Instance */ +.highlight .vm { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable.Magic */ \ No newline at end of file diff --git a/static/css/algol_nu.css b/static/css/algol_nu.css new file mode 100644 index 0000000..ef85bc7 --- /dev/null +++ b/static/css/algol_nu.css @@ -0,0 +1,49 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #ffffff; } +.highlight .c { color: #888888; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #FF0000 } /* Error */ +.highlight .k { font-weight: bold } /* Keyword */ +.highlight .ch { color: #888888; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #888888; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #888888; font-weight: bold } /* Comment.Preproc */ +.highlight .cpf { color: #888888; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #888888; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #888888; font-weight: bold } /* Comment.Special */ +.highlight .kc { font-weight: bold } /* Keyword.Constant */ +.highlight .kd { font-weight: bold; font-style: italic } /* Keyword.Declaration */ +.highlight .kn { font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { font-weight: bold } /* Keyword.Pseudo */ +.highlight .kr { font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { font-weight: bold } /* Keyword.Type */ +.highlight .s { color: #666666; font-style: italic } /* Literal.String */ +.highlight .nb { font-weight: bold; font-style: italic } /* Name.Builtin */ +.highlight .nc { color: #666666; font-weight: bold; font-style: italic } /* Name.Class */ +.highlight .no { color: #666666; font-weight: bold; font-style: italic } /* Name.Constant */ +.highlight .nf { color: #666666; font-weight: bold; font-style: italic } /* Name.Function */ +.highlight .nn { color: #666666; font-weight: bold; font-style: italic } /* Name.Namespace */ +.highlight .nv { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable */ +.highlight .ow { font-weight: bold } /* Operator.Word */ +.highlight .sa { color: #666666; font-style: italic } /* Literal.String.Affix */ +.highlight .sb { color: #666666; font-style: italic } /* Literal.String.Backtick */ +.highlight .sc { color: #666666; font-style: italic } /* Literal.String.Char */ +.highlight .dl { color: #666666; font-style: italic } /* Literal.String.Delimiter */ +.highlight .sd { color: #666666; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #666666; font-style: italic } /* Literal.String.Double */ +.highlight .se { color: #666666; font-style: italic } /* Literal.String.Escape */ +.highlight .sh { color: #666666; font-style: italic } /* Literal.String.Heredoc */ +.highlight .si { color: #666666; font-style: italic } /* Literal.String.Interpol */ +.highlight .sx { color: #666666; font-style: italic } /* Literal.String.Other */ +.highlight .sr { color: #666666; font-style: italic } /* Literal.String.Regex */ +.highlight .s1 { color: #666666; font-style: italic } /* Literal.String.Single */ +.highlight .ss { color: #666666; font-style: italic } /* Literal.String.Symbol */ +.highlight .bp { font-weight: bold; font-style: italic } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #666666; font-weight: bold; font-style: italic } /* Name.Function.Magic */ +.highlight .vc { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable.Class */ +.highlight .vg { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable.Global */ +.highlight .vi { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable.Instance */ +.highlight .vm { color: #666666; font-weight: bold; font-style: italic } /* Name.Variable.Magic */ \ No newline at end of file diff --git a/static/css/arduino.css b/static/css/arduino.css new file mode 100644 index 0000000..fd1e1c6 --- /dev/null +++ b/static/css/arduino.css @@ -0,0 +1,66 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #ffffff; } +.highlight .c { color: #95a5a6 } /* Comment */ +.highlight .err { color: #a61717 } /* Error */ +.highlight .k { color: #728E00 } /* Keyword */ +.highlight .n { color: #434f54 } /* Name */ +.highlight .o { color: #728E00 } /* Operator */ +.highlight .ch { color: #95a5a6 } /* Comment.Hashbang */ +.highlight .cm { color: #95a5a6 } /* Comment.Multiline */ +.highlight .cp { color: #728E00 } /* Comment.Preproc */ +.highlight .cpf { color: #95a5a6 } /* Comment.PreprocFile */ +.highlight .c1 { color: #95a5a6 } /* Comment.Single */ +.highlight .cs { color: #95a5a6 } /* Comment.Special */ +.highlight .kc { color: #00979D } /* Keyword.Constant */ +.highlight .kd { color: #728E00 } /* Keyword.Declaration */ +.highlight .kn { color: #728E00 } /* Keyword.Namespace */ +.highlight .kp { color: #00979D } /* Keyword.Pseudo */ +.highlight .kr { color: #00979D } /* Keyword.Reserved */ +.highlight .kt { color: #00979D } /* Keyword.Type */ +.highlight .m { color: #8A7B52 } /* Literal.Number */ +.highlight .s { color: #7F8C8D } /* Literal.String */ +.highlight .na { color: #434f54 } /* Name.Attribute */ +.highlight .nb { color: #728E00 } /* Name.Builtin */ +.highlight .nc { color: #434f54 } /* Name.Class */ +.highlight .no { color: #434f54 } /* Name.Constant */ +.highlight .nd { color: #434f54 } /* Name.Decorator */ +.highlight .ni { color: #434f54 } /* Name.Entity */ +.highlight .ne { color: #434f54 } /* Name.Exception */ +.highlight .nf { color: #D35400 } /* Name.Function */ +.highlight .nl { color: #434f54 } /* Name.Label */ +.highlight .nn { color: #434f54 } /* Name.Namespace */ +.highlight .nx { color: #728E00 } /* Name.Other */ +.highlight .py { color: #434f54 } /* Name.Property */ +.highlight .nt { color: #434f54 } /* Name.Tag */ +.highlight .nv { color: #434f54 } /* Name.Variable */ +.highlight .ow { color: #728E00 } /* Operator.Word */ +.highlight .mb { color: #8A7B52 } /* Literal.Number.Bin */ +.highlight .mf { color: #8A7B52 } /* Literal.Number.Float */ +.highlight .mh { color: #8A7B52 } /* Literal.Number.Hex */ +.highlight .mi { color: #8A7B52 } /* Literal.Number.Integer */ +.highlight .mo { color: #8A7B52 } /* Literal.Number.Oct */ +.highlight .sa { color: #7F8C8D } /* Literal.String.Affix */ +.highlight .sb { color: #7F8C8D } /* Literal.String.Backtick */ +.highlight .sc { color: #7F8C8D } /* Literal.String.Char */ +.highlight .dl { color: #7F8C8D } /* Literal.String.Delimiter */ +.highlight .sd { color: #7F8C8D } /* Literal.String.Doc */ +.highlight .s2 { color: #7F8C8D } /* Literal.String.Double */ +.highlight .se { color: #7F8C8D } /* Literal.String.Escape */ +.highlight .sh { color: #7F8C8D } /* Literal.String.Heredoc */ +.highlight .si { color: #7F8C8D } /* Literal.String.Interpol */ +.highlight .sx { color: #7F8C8D } /* Literal.String.Other */ +.highlight .sr { color: #7F8C8D } /* Literal.String.Regex */ +.highlight .s1 { color: #7F8C8D } /* Literal.String.Single */ +.highlight .ss { color: #7F8C8D } /* Literal.String.Symbol */ +.highlight .bp { color: #728E00 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #D35400 } /* Name.Function.Magic */ +.highlight .vc { color: #434f54 } /* Name.Variable.Class */ +.highlight .vg { color: #434f54 } /* Name.Variable.Global */ +.highlight .vi { color: #434f54 } /* Name.Variable.Instance */ +.highlight .vm { color: #434f54 } /* Name.Variable.Magic */ +.highlight .il { color: #8A7B52 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/static/css/autumn.css b/static/css/autumn.css new file mode 100644 index 0000000..650b527 --- /dev/null +++ b/static/css/autumn.css @@ -0,0 +1,72 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #ffffff; } +.highlight .c { color: #aaaaaa; font-style: italic } /* Comment */ +.highlight .err { color: #FF0000; background-color: #FFAAAA } /* Error */ +.highlight .k { color: #0000aa } /* Keyword */ +.highlight .ch { color: #aaaaaa; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #aaaaaa; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #4c8317 } /* Comment.Preproc */ +.highlight .cpf { color: #aaaaaa; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #aaaaaa; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #0000aa; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #aa0000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.highlight .gr { color: #aa0000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00aa00 } /* Generic.Inserted */ +.highlight .go { color: #888888 } /* Generic.Output */ +.highlight .gp { color: #555555 } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #aa0000 } /* Generic.Traceback */ +.highlight .kc { color: #0000aa } /* Keyword.Constant */ +.highlight .kd { color: #0000aa } /* Keyword.Declaration */ +.highlight .kn { color: #0000aa } /* Keyword.Namespace */ +.highlight .kp { color: #0000aa } /* Keyword.Pseudo */ +.highlight .kr { color: #0000aa } /* Keyword.Reserved */ +.highlight .kt { color: #00aaaa } /* Keyword.Type */ +.highlight .m { color: #009999 } /* Literal.Number */ +.highlight .s { color: #aa5500 } /* Literal.String */ +.highlight .na { color: #1e90ff } /* Name.Attribute */ +.highlight .nb { color: #00aaaa } /* Name.Builtin */ +.highlight .nc { color: #00aa00; text-decoration: underline } /* Name.Class */ +.highlight .no { color: #aa0000 } /* Name.Constant */ +.highlight .nd { color: #888888 } /* Name.Decorator */ +.highlight .ni { color: #880000; font-weight: bold } /* Name.Entity */ +.highlight .nf { color: #00aa00 } /* Name.Function */ +.highlight .nn { color: #00aaaa; text-decoration: underline } /* Name.Namespace */ +.highlight .nt { color: #1e90ff; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #aa0000 } /* Name.Variable */ +.highlight .ow { color: #0000aa } /* Operator.Word */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mb { color: #009999 } /* Literal.Number.Bin */ +.highlight .mf { color: #009999 } /* Literal.Number.Float */ +.highlight .mh { color: #009999 } /* Literal.Number.Hex */ +.highlight .mi { color: #009999 } /* Literal.Number.Integer */ +.highlight .mo { color: #009999 } /* Literal.Number.Oct */ +.highlight .sa { color: #aa5500 } /* Literal.String.Affix */ +.highlight .sb { color: #aa5500 } /* Literal.String.Backtick */ +.highlight .sc { color: #aa5500 } /* Literal.String.Char */ +.highlight .dl { color: #aa5500 } /* Literal.String.Delimiter */ +.highlight .sd { color: #aa5500 } /* Literal.String.Doc */ +.highlight .s2 { color: #aa5500 } /* Literal.String.Double */ +.highlight .se { color: #aa5500 } /* Literal.String.Escape */ +.highlight .sh { color: #aa5500 } /* Literal.String.Heredoc */ +.highlight .si { color: #aa5500 } /* Literal.String.Interpol */ +.highlight .sx { color: #aa5500 } /* Literal.String.Other */ +.highlight .sr { color: #009999 } /* Literal.String.Regex */ +.highlight .s1 { color: #aa5500 } /* Literal.String.Single */ +.highlight .ss { color: #0000aa } /* Literal.String.Symbol */ +.highlight .bp { color: #00aaaa } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #00aa00 } /* Name.Function.Magic */ +.highlight .vc { color: #aa0000 } /* Name.Variable.Class */ +.highlight .vg { color: #aa0000 } /* Name.Variable.Global */ +.highlight .vi { color: #aa0000 } /* Name.Variable.Instance */ +.highlight .vm { color: #aa0000 } /* Name.Variable.Magic */ +.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/static/css/bootstrap.min.css b/static/css/bootstrap.min.css new file mode 100644 index 0000000..5d87a52 --- /dev/null +++ b/static/css/bootstrap.min.css @@ -0,0 +1,12 @@ +@charset "UTF-8";/*! + * Bootswatch v5.3.3 (https://bootswatch.com) + * Theme: darkly + * Copyright 2012-2024 Thomas Park + * Licensed under MIT + * Based on Bootstrap +*//*! + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */@import url(https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400&display=swap);:root,[data-bs-theme=light]{--bs-blue:#375a7f;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#e83e8c;--bs-red:#e74c3c;--bs-orange:#fd7e14;--bs-yellow:#f39c12;--bs-green:#00bc8c;--bs-teal:#20c997;--bs-cyan:#3498db;--bs-black:#000;--bs-white:#fff;--bs-gray:#888;--bs-gray-dark:#303030;--bs-gray-100:#f8f9fa;--bs-gray-200:#ebebeb;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#888;--bs-gray-700:#444;--bs-gray-800:#303030;--bs-gray-900:#222;--bs-primary:#375a7f;--bs-secondary:#444;--bs-success:#00bc8c;--bs-info:#3498db;--bs-warning:#f39c12;--bs-danger:#e74c3c;--bs-light:#adb5bd;--bs-dark:#303030;--bs-primary-rgb:55,90,127;--bs-secondary-rgb:68,68,68;--bs-success-rgb:0,188,140;--bs-info-rgb:52,152,219;--bs-warning-rgb:243,156,18;--bs-danger-rgb:231,76,60;--bs-light-rgb:173,181,189;--bs-dark-rgb:48,48,48;--bs-primary-text-emphasis:#162433;--bs-secondary-text-emphasis:#1b1b1b;--bs-success-text-emphasis:#004b38;--bs-info-text-emphasis:#153d58;--bs-warning-text-emphasis:#613e07;--bs-danger-text-emphasis:#5c1e18;--bs-light-text-emphasis:#444;--bs-dark-text-emphasis:#444;--bs-primary-bg-subtle:#d7dee5;--bs-secondary-bg-subtle:#dadada;--bs-success-bg-subtle:#ccf2e8;--bs-info-bg-subtle:#d6eaf8;--bs-warning-bg-subtle:#fdebd0;--bs-danger-bg-subtle:#fadbd8;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#afbdcc;--bs-secondary-border-subtle:#b4b4b4;--bs-success-border-subtle:#99e4d1;--bs-info-border-subtle:#aed6f1;--bs-warning-border-subtle:#fad7a0;--bs-danger-border-subtle:#f5b7b1;--bs-light-border-subtle:#ebebeb;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:Lato,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#fff;--bs-body-color-rgb:255,255,255;--bs-body-bg:#222;--bs-body-bg-rgb:34,34,34;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(255, 255, 255, 0.75);--bs-secondary-color-rgb:255,255,255;--bs-secondary-bg:#ebebeb;--bs-secondary-bg-rgb:235,235,235;--bs-tertiary-color:rgba(255, 255, 255, 0.5);--bs-tertiary-color-rgb:255,255,255;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#00bc8c;--bs-link-color-rgb:0,188,140;--bs-link-decoration:underline;--bs-link-hover-color:#009670;--bs-link-hover-color-rgb:0,150,112;--bs-code-color:#e83e8c;--bs-highlight-color:#fff;--bs-highlight-bg:#fdebd0;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(55, 90, 127, 0.25);--bs-form-valid-color:#00bc8c;--bs-form-valid-border-color:#00bc8c;--bs-form-invalid-color:#e74c3c;--bs-form-invalid-border-color:#e74c3c}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#222;--bs-body-bg-rgb:34,34,34;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#303030;--bs-secondary-bg-rgb:48,48,48;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#292929;--bs-tertiary-bg-rgb:41,41,41;--bs-primary-text-emphasis:#879cb2;--bs-secondary-text-emphasis:#8f8f8f;--bs-success-text-emphasis:#66d7ba;--bs-info-text-emphasis:#85c1e9;--bs-warning-text-emphasis:#f8c471;--bs-danger-text-emphasis:#f1948a;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#0b1219;--bs-secondary-bg-subtle:#0e0e0e;--bs-success-bg-subtle:#00261c;--bs-info-bg-subtle:#0a1e2c;--bs-warning-bg-subtle:#311f04;--bs-danger-bg-subtle:#2e0f0c;--bs-light-bg-subtle:#303030;--bs-dark-bg-subtle:#181818;--bs-primary-border-subtle:#21364c;--bs-secondary-border-subtle:#292929;--bs-success-border-subtle:#007154;--bs-info-border-subtle:#1f5b83;--bs-warning-border-subtle:#925e0b;--bs-danger-border-subtle:#8b2e24;--bs-light-border-subtle:#444;--bs-dark-border-subtle:#303030;--bs-heading-color:inherit;--bs-link-color:#879cb2;--bs-link-hover-color:#9fb0c1;--bs-link-color-rgb:135,156,178;--bs-link-hover-color-rgb:159,176,193;--bs-code-color:#f18bba;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#613e07;--bs-border-color:#444;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#66d7ba;--bs-form-valid-border-color:#66d7ba;--bs-form-invalid-color:#f1948a;--bs-form-invalid-border-color:#f1948a}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.425rem + 2.1vw)}@media (min-width:1200px){.h1,h1{font-size:3rem}}.h2,h2{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h2,h2{font-size:2.5rem}}.h3,h3{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h3,h3{font-size:2rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}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(--bs-highlight-color);background-color:var(--bs-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(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em;color:inherit}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}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(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::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:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#888}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.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{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@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{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@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{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@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{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@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{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@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{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:#fff;--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:#444;--bs-table-accent-bg:transparent;--bs-table-striped-color:#fff;--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:#fff;--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:#fff;--bs-table-hover-bg:rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-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(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#fff;--bs-table-bg:#375a7f;--bs-table-border-color:#5f7b99;--bs-table-striped-bg:#416285;--bs-table-striped-color:#fff;--bs-table-active-bg:#4b6b8c;--bs-table-active-color:#fff;--bs-table-hover-bg:#466689;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#fff;--bs-table-bg:#444444;--bs-table-border-color:dimgray;--bs-table-striped-bg:#4d4d4d;--bs-table-striped-color:#fff;--bs-table-active-bg:#575757;--bs-table-active-color:#fff;--bs-table-hover-bg:#525252;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#fff;--bs-table-bg:#00bc8c;--bs-table-border-color:#33c9a3;--bs-table-striped-bg:#0dbf92;--bs-table-striped-color:#fff;--bs-table-active-bg:#1ac398;--bs-table-active-color:#fff;--bs-table-hover-bg:#13c195;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#fff;--bs-table-bg:#3498db;--bs-table-border-color:#5dade2;--bs-table-striped-bg:#3e9ddd;--bs-table-striped-color:#fff;--bs-table-active-bg:#48a2df;--bs-table-active-color:#fff;--bs-table-hover-bg:#43a0de;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#fff;--bs-table-bg:#f39c12;--bs-table-border-color:#f5b041;--bs-table-striped-bg:#f4a11e;--bs-table-striped-color:#fff;--bs-table-active-bg:#f4a62a;--bs-table-active-color:#fff;--bs-table-hover-bg:#f4a324;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#fff;--bs-table-bg:#e74c3c;--bs-table-border-color:#ec7063;--bs-table-striped-bg:#e85546;--bs-table-striped-color:#fff;--bs-table-active-bg:#e95e50;--bs-table-active-color:#fff;--bs-table-hover-bg:#e9594b;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#fff;--bs-table-bg:#adb5bd;--bs-table-border-color:#bdc4ca;--bs-table-striped-bg:#b1b9c0;--bs-table-striped-color:#fff;--bs-table-active-bg:#b5bcc4;--bs-table-active-color:#fff;--bs-table-hover-bg:#b3bbc2;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#303030;--bs-table-border-color:#595959;--bs-table-striped-bg:#3a3a3a;--bs-table-striped-color:#fff;--bs-table-active-bg:#454545;--bs-table-active-color:#fff;--bs-table-hover-bg:#404040;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-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}.col-form-label{padding-top:calc(.375rem + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#303030;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-clip:padding-box;border:var(--bs-border-width) solid #222;border-radius:var(--bs-border-radius);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:#303030;background-color:#fff;border-color:#9badbf;outline:0;box-shadow:0 0 0 .25rem rgba(55,90,127,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:#888;opacity:1}.form-control::placeholder{color:#888;opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#adb5bd;background-color:#444;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#adb5bd;background-color:#444;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-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::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#373737}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#373737}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-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='%23303030' 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:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#303030;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid #222;border-radius:var(--bs-border-radius);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:#9badbf;outline:0;box-shadow:0 0 0 .25rem rgba(55,90,127,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{color:#888;background-color:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #303030}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-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='%23dee2e6' 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.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:#fff;flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:none;-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#9badbf;outline:0;box-shadow:0 0 0 .25rem rgba(55,90,127,.25)}.form-check-input:checked{background-color:#375a7f;border-color:#375a7f}.form-check-input:checked[type=checkbox]{--bs-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='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#375a7f;border-color:#375a7f;--bs-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='%23fff' 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:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-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%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-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='%239badbf'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-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='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;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:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-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.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #222,0 0 0 .25rem rgba(55,90,127,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #222,0 0 0 .25rem rgba(55,90,127,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;appearance:none;background-color:#375a7f;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#c3ced9}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#375a7f;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#c3ced9}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-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(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-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 .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:not(:-moz-placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:#fff;border-radius:var(--bs-border-radius)}.form-floating>.form-control-plaintext~label::after,.form-floating>.form-control:focus~label::after,.form-floating>.form-control:not(:placeholder-shown)~label::after,.form-floating>.form-select~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:#fff;border-radius:var(--bs-border-radius)}.form-floating>.form-control:-webkit-autofill~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#888}.form-floating>.form-control:disabled~label::after,.form-floating>:disabled~label::after{background-color:var(--bs-secondary-bg)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#adb5bd;text-align:center;white-space:nowrap;background-color:#444;border:var(--bs-border-width) solid #222;border-radius:var(--bs-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(var(--bs-border-width) * -1);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(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23e74c3c'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23e74c3c' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23e74c3c'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23e74c3c' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);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(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#375a7f;--bs-btn-border-color:#375a7f;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#2f4d6c;--bs-btn-hover-border-color:#2c4866;--bs-btn-focus-shadow-rgb:85,115,146;--bs-btn-active-color:#fff;--bs-btn-active-bg:#2c4866;--bs-btn-active-border-color:#29445f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#375a7f;--bs-btn-disabled-border-color:#375a7f}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#444;--bs-btn-border-color:#444;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#3a3a3a;--bs-btn-hover-border-color:#363636;--bs-btn-focus-shadow-rgb:96,96,96;--bs-btn-active-color:#fff;--bs-btn-active-bg:#363636;--bs-btn-active-border-color:#333333;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#444;--bs-btn-disabled-border-color:#444}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#00bc8c;--bs-btn-border-color:#00bc8c;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#00a077;--bs-btn-hover-border-color:#009670;--bs-btn-focus-shadow-rgb:38,198,157;--bs-btn-active-color:#fff;--bs-btn-active-bg:#009670;--bs-btn-active-border-color:#008d69;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#00bc8c;--bs-btn-disabled-border-color:#00bc8c}.btn-info{--bs-btn-color:#fff;--bs-btn-bg:#3498db;--bs-btn-border-color:#3498db;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#2c81ba;--bs-btn-hover-border-color:#2a7aaf;--bs-btn-focus-shadow-rgb:82,167,224;--bs-btn-active-color:#fff;--bs-btn-active-bg:#2a7aaf;--bs-btn-active-border-color:#2772a4;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#3498db;--bs-btn-disabled-border-color:#3498db}.btn-warning{--bs-btn-color:#fff;--bs-btn-bg:#f39c12;--bs-btn-border-color:#f39c12;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#cf850f;--bs-btn-hover-border-color:#c27d0e;--bs-btn-focus-shadow-rgb:245,171,54;--bs-btn-active-color:#fff;--bs-btn-active-bg:#c27d0e;--bs-btn-active-border-color:#b6750e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#f39c12;--bs-btn-disabled-border-color:#f39c12}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#e74c3c;--bs-btn-border-color:#e74c3c;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#c44133;--bs-btn-hover-border-color:#b93d30;--bs-btn-focus-shadow-rgb:235,103,89;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b93d30;--bs-btn-active-border-color:#ad392d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#e74c3c;--bs-btn-disabled-border-color:#e74c3c}.btn-light{--bs-btn-color:#fff;--bs-btn-bg:#adb5bd;--bs-btn-border-color:#adb5bd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#939aa1;--bs-btn-hover-border-color:#8a9197;--bs-btn-focus-shadow-rgb:185,192,199;--bs-btn-active-color:#fff;--bs-btn-active-bg:#8a9197;--bs-btn-active-border-color:#82888e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#adb5bd;--bs-btn-disabled-border-color:#adb5bd}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#303030;--bs-btn-border-color:#303030;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#4f4f4f;--bs-btn-hover-border-color:#454545;--bs-btn-focus-shadow-rgb:79,79,79;--bs-btn-active-color:#fff;--bs-btn-active-bg:#595959;--bs-btn-active-border-color:#454545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#303030;--bs-btn-disabled-border-color:#303030}.btn-outline-primary{--bs-btn-color:#375a7f;--bs-btn-border-color:#375a7f;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#375a7f;--bs-btn-hover-border-color:#375a7f;--bs-btn-focus-shadow-rgb:55,90,127;--bs-btn-active-color:#fff;--bs-btn-active-bg:#375a7f;--bs-btn-active-border-color:#375a7f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#375a7f;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#375a7f;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#444;--bs-btn-border-color:#444;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#444;--bs-btn-hover-border-color:#444;--bs-btn-focus-shadow-rgb:68,68,68;--bs-btn-active-color:#fff;--bs-btn-active-bg:#444;--bs-btn-active-border-color:#444;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#444;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#444;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#00bc8c;--bs-btn-border-color:#00bc8c;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#00bc8c;--bs-btn-hover-border-color:#00bc8c;--bs-btn-focus-shadow-rgb:0,188,140;--bs-btn-active-color:#fff;--bs-btn-active-bg:#00bc8c;--bs-btn-active-border-color:#00bc8c;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#00bc8c;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#00bc8c;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#3498db;--bs-btn-border-color:#3498db;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#3498db;--bs-btn-hover-border-color:#3498db;--bs-btn-focus-shadow-rgb:52,152,219;--bs-btn-active-color:#fff;--bs-btn-active-bg:#3498db;--bs-btn-active-border-color:#3498db;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#3498db;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#3498db;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#f39c12;--bs-btn-border-color:#f39c12;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#f39c12;--bs-btn-hover-border-color:#f39c12;--bs-btn-focus-shadow-rgb:243,156,18;--bs-btn-active-color:#fff;--bs-btn-active-bg:#f39c12;--bs-btn-active-border-color:#f39c12;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f39c12;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f39c12;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#e74c3c;--bs-btn-border-color:#e74c3c;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#e74c3c;--bs-btn-hover-border-color:#e74c3c;--bs-btn-focus-shadow-rgb:231,76,60;--bs-btn-active-color:#fff;--bs-btn-active-bg:#e74c3c;--bs-btn-active-border-color:#e74c3c;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#e74c3c;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#e74c3c;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#adb5bd;--bs-btn-border-color:#adb5bd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#adb5bd;--bs-btn-hover-border-color:#adb5bd;--bs-btn-focus-shadow-rgb:173,181,189;--bs-btn-active-color:#fff;--bs-btn-active-bg:#adb5bd;--bs-btn-active-border-color:#adb5bd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#adb5bd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#adb5bd;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#303030;--bs-btn-border-color:#303030;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#303030;--bs-btn-hover-border-color:#303030;--bs-btn-focus-shadow-rgb:48,48,48;--bs-btn-active-color:#fff;--bs-btn-active-bg:#303030;--bs-btn-active-border-color:#303030;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#303030;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#303030;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#888;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:38,198,157;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:#222;--bs-dropdown-border-color:#444;--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:#444;--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:#fff;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-link-hover-bg:#375a7f;--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#375a7f;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#888;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-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(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.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(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#303030;--bs-dropdown-border-color:#444;--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:#444;--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#375a7f;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(var(--bs-border-width) * -1)}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(var(--bs-border-width) * -1)}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:2rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:#adb5bd;display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(55,90,127,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:#444;--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:#444 #444 transparent;--bs-nav-tabs-link-active-color:#fff;--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:#444 #444 transparent;border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#375a7f}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;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{--bs-navbar-padding-x:0;--bs-navbar-padding-y:1rem;--bs-navbar-color:rgba(34, 34, 34, 0.7);--bs-navbar-hover-color:#222;--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:#222;--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:#222;--bs-navbar-brand-hover-color:#222;--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-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.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(34, 34, 34, 0.1);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-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(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-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(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-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(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-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;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(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-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;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(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-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;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(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-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;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(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-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;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(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-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;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]{--bs-navbar-color:rgba(255, 255, 255, 0.6);--bs-navbar-hover-color:#fff;--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-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.6%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{--bs-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.6%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:#444;--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:#303030;--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.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(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-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(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23162433' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(55, 90, 127, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:var(--bs-primary-text-emphasis);--bs-accordion-active-bg:var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.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-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}.accordion-flush>.accordion-item>.accordion-collapse{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23879cb2'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23879cb2'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0.75rem;--bs-breadcrumb-padding-y:0.375rem;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg:#444;--bs-breadcrumb-border-radius:0.25rem;--bs-breadcrumb-divider-color:var(--bs-secondary-color);--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:#fff;--bs-pagination-bg:#00bc8c;--bs-pagination-border-width:0;--bs-pagination-border-color:transparent;--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:#fff;--bs-pagination-hover-bg:#00efb2;--bs-pagination-hover-border-color:transparent;--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(55, 90, 127, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#00efb2;--bs-pagination-active-border-color:transparent;--bs-pagination-disabled-color:#fff;--bs-pagination-disabled-bg:#007053;--bs-pagination-disabled-border-color:transparent;display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-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(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(0 * -1)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:#444;--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#375a7f;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-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(--bs-progress-height) var(--bs-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{--bs-list-group-color:#fff;--bs-list-group-bg:#303030;--bs-list-group-border-color:#444;--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:var(--bs-secondary-color);--bs-list-group-action-hover-color:#fff;--bs-list-group-action-hover-bg:#444;--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:#222;--bs-list-group-disabled-color:var(--bs-secondary-color);--bs-list-group-disabled-bg:#303030;--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#375a7f;--bs-list-group-active-border-color:#375a7f;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-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-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-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(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-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(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-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(--bs-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(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-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(--bs-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(--bs-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(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-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(--bs-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(--bs-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(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-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(--bs-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(--bs-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(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-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(--bs-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(--bs-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(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-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(--bs-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(--bs-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(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#fff;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%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.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.4;--bs-btn-close-hover-opacity:1;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(55, 90, 127, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;--bs-btn-close-white-filter:invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{filter:var(--bs-btn-close-white-filter)}[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:#444;--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:var(--bs-secondary-color);--bs-toast-header-bg:#303030;--bs-toast-header-border-color:var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color: ;--bs-modal-bg:#303030;--bs-modal-border-color:#444;--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:#444;--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:#444;--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@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(--bs-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(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;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(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:#303030;--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:#444;--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;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(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-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%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;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' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%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:#fff;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:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:#444;--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-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(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-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(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-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(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-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(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-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(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-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(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-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(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-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(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-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(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-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(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin:calc(-.5 * var(--bs-offcanvas-padding-y)) calc(-.5 * var(--bs-offcanvas-padding-x)) calc(-.5 * var(--bs-offcanvas-padding-y)) auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.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:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#fff!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#fff!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#fff!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(44,72,102,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(44,72,102,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(44,72,102,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(54,54,54,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(54,54,54,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(54,54,54,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(0,150,112,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(0,150,112,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(0,150,112,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(42,122,175,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(42,122,175,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(42,122,175,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(194,125,14,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(194,125,14,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(194,125,14,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(185,61,48,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(185,61,48,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(185,61,48,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(138,145,151,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(138,145,151,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(138,145,151,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(38,38,38,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(38,38,38,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(38,38,38,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0) var(--bs-focus-ring-y,0) var(--bs-focus-ring-blur,0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-underline-offset:0.25em;-webkit-backface-visibility:hidden;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.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:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.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}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-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:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-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{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!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:3rem!important;margin-left:3rem!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:3rem!important;margin-bottom:3rem!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:3rem!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:3rem!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:3rem!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:3rem!important}.ms-auto{margin-left:auto!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:3rem!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:3rem!important;padding-left:3rem!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:3rem!important;padding-bottom:3rem!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:3rem!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:3rem!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:3rem!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:3rem!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:3rem!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:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.425rem + 2.1vw)!important}.fs-2{font-size:calc(1.375rem + 1.5vw)!important}.fs-3{font-size:calc(1.325rem + .9vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!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{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-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(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-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(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-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(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-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(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-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(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-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(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-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(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-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(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-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}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!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:3rem!important;margin-left:3rem!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:3rem!important;margin-bottom:3rem!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:3rem!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:3rem!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:3rem!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:3rem!important}.ms-sm-auto{margin-left:auto!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:3rem!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:3rem!important;padding-left:3rem!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:3rem!important;padding-bottom:3rem!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:3rem!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:3rem!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:3rem!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:3rem!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:3rem!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:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!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:3rem!important;margin-left:3rem!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:3rem!important;margin-bottom:3rem!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:3rem!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:3rem!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:3rem!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:3rem!important}.ms-md-auto{margin-left:auto!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:3rem!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:3rem!important;padding-left:3rem!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:3rem!important;padding-bottom:3rem!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:3rem!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:3rem!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:3rem!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:3rem!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:3rem!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:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!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:3rem!important;margin-left:3rem!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:3rem!important;margin-bottom:3rem!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:3rem!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:3rem!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:3rem!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:3rem!important}.ms-lg-auto{margin-left:auto!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:3rem!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:3rem!important;padding-left:3rem!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:3rem!important;padding-bottom:3rem!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:3rem!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:3rem!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:3rem!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:3rem!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:3rem!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:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!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:3rem!important;margin-left:3rem!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:3rem!important;margin-bottom:3rem!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:3rem!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:3rem!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:3rem!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:3rem!important}.ms-xl-auto{margin-left:auto!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:3rem!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:3rem!important;padding-left:3rem!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:3rem!important;padding-bottom:3rem!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:3rem!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:3rem!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:3rem!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:3rem!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:3rem!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:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!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:3rem!important;margin-left:3rem!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:3rem!important;margin-bottom:3rem!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:3rem!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:3rem!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:3rem!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:3rem!important}.ms-xxl-auto{margin-left:auto!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:3rem!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:3rem!important;padding-left:3rem!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:3rem!important;padding-bottom:3rem!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:3rem!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:3rem!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:3rem!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:3rem!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:3rem!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:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:3rem!important}.fs-2{font-size:2.5rem!important}.fs-3{font-size:2rem!important}.fs-4{font-size:1.5rem!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}}.blockquote-footer{color:#888}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{color:#888}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>label{color:#888}.nav-pills .nav-item.open .nav-link,.nav-pills .nav-item.open .nav-link:focus,.nav-pills .nav-item.open .nav-link:hover,.nav-pills .nav-link,.nav-pills .nav-link.active,.nav-pills .nav-link.active:focus,.nav-pills .nav-link.active:hover,.nav-tabs .nav-item.open .nav-link,.nav-tabs .nav-item.open .nav-link:focus,.nav-tabs .nav-item.open .nav-link:hover,.nav-tabs .nav-link,.nav-tabs .nav-link.active,.nav-tabs .nav-link.active:focus,.nav-tabs .nav-link.active:hover{color:#fff}.breadcrumb a{color:#fff}.pagination a:hover{text-decoration:none}.alert{color:#fff;border:none}.alert .alert-link,.alert a{color:#fff;text-decoration:underline}.alert-primary{background-color:#375a7f}.alert-secondary{background-color:#444}.alert-success{background-color:#00bc8c}.alert-info{background-color:#3498db}.alert-warning{background-color:#f39c12}.alert-danger{background-color:#e74c3c}.alert-light{background-color:#adb5bd}.alert-dark{background-color:#303030}.tooltip{--bs-tooltip-bg:var(--bs-tertiary-bg);--bs-tooltip-color:var(--bs-emphasis-color)} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/static/css/bootstrap.min.css.map b/static/css/bootstrap.min.css.map new file mode 100644 index 0000000..90ce798 --- /dev/null +++ b/static/css/bootstrap.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/mixins/_banner.scss","../../scss/_root.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_color-mode.scss","../../scss/_reboot.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_color-bg.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_focus-ring.scss","../../scss/helpers/_icon-link.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"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","sourcesContent":["@mixin bsBanner($file) {\n /*!\n * Bootstrap #{$file} v5.3.3 (https://getbootstrap.com/)\n * Copyright 2011-2024 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n}\n",":root,\n[data-bs-theme=\"light\"] {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$prefix}#{$color}-rgb: #{$value};\n }\n\n @each $color, $value in $theme-colors-text {\n --#{$prefix}#{$color}-text-emphasis: #{$value};\n }\n\n @each $color, $value in $theme-colors-bg-subtle {\n --#{$prefix}#{$color}-bg-subtle: #{$value};\n }\n\n @each $color, $value in $theme-colors-border-subtle {\n --#{$prefix}#{$color}-border-subtle: #{$value};\n }\n\n --#{$prefix}white-rgb: #{to-rgb($white)};\n --#{$prefix}black-rgb: #{to-rgb($black)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$prefix}gradient: #{$gradient};\n\n // Root and body\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$prefix}root-font-size: #{$font-size-root};\n }\n --#{$prefix}body-font-family: #{inspect($font-family-base)};\n @include rfs($font-size-base, --#{$prefix}body-font-size);\n --#{$prefix}body-font-weight: #{$font-weight-base};\n --#{$prefix}body-line-height: #{$line-height-base};\n @if $body-text-align != null {\n --#{$prefix}body-text-align: #{$body-text-align};\n }\n\n --#{$prefix}body-color: #{$body-color};\n --#{$prefix}body-color-rgb: #{to-rgb($body-color)};\n --#{$prefix}body-bg: #{$body-bg};\n --#{$prefix}body-bg-rgb: #{to-rgb($body-bg)};\n\n --#{$prefix}emphasis-color: #{$body-emphasis-color};\n --#{$prefix}emphasis-color-rgb: #{to-rgb($body-emphasis-color)};\n\n --#{$prefix}secondary-color: #{$body-secondary-color};\n --#{$prefix}secondary-color-rgb: #{to-rgb($body-secondary-color)};\n --#{$prefix}secondary-bg: #{$body-secondary-bg};\n --#{$prefix}secondary-bg-rgb: #{to-rgb($body-secondary-bg)};\n\n --#{$prefix}tertiary-color: #{$body-tertiary-color};\n --#{$prefix}tertiary-color-rgb: #{to-rgb($body-tertiary-color)};\n --#{$prefix}tertiary-bg: #{$body-tertiary-bg};\n --#{$prefix}tertiary-bg-rgb: #{to-rgb($body-tertiary-bg)};\n // scss-docs-end root-body-variables\n\n --#{$prefix}heading-color: #{$headings-color};\n\n --#{$prefix}link-color: #{$link-color};\n --#{$prefix}link-color-rgb: #{to-rgb($link-color)};\n --#{$prefix}link-decoration: #{$link-decoration};\n\n --#{$prefix}link-hover-color: #{$link-hover-color};\n --#{$prefix}link-hover-color-rgb: #{to-rgb($link-hover-color)};\n\n @if $link-hover-decoration != null {\n --#{$prefix}link-hover-decoration: #{$link-hover-decoration};\n }\n\n --#{$prefix}code-color: #{$code-color};\n --#{$prefix}highlight-color: #{$mark-color};\n --#{$prefix}highlight-bg: #{$mark-bg};\n\n // scss-docs-start root-border-var\n --#{$prefix}border-width: #{$border-width};\n --#{$prefix}border-style: #{$border-style};\n --#{$prefix}border-color: #{$border-color};\n --#{$prefix}border-color-translucent: #{$border-color-translucent};\n\n --#{$prefix}border-radius: #{$border-radius};\n --#{$prefix}border-radius-sm: #{$border-radius-sm};\n --#{$prefix}border-radius-lg: #{$border-radius-lg};\n --#{$prefix}border-radius-xl: #{$border-radius-xl};\n --#{$prefix}border-radius-xxl: #{$border-radius-xxl};\n --#{$prefix}border-radius-2xl: var(--#{$prefix}border-radius-xxl); // Deprecated in v5.3.0 for consistency\n --#{$prefix}border-radius-pill: #{$border-radius-pill};\n // scss-docs-end root-border-var\n\n --#{$prefix}box-shadow: #{$box-shadow};\n --#{$prefix}box-shadow-sm: #{$box-shadow-sm};\n --#{$prefix}box-shadow-lg: #{$box-shadow-lg};\n --#{$prefix}box-shadow-inset: #{$box-shadow-inset};\n\n // Focus styles\n // scss-docs-start root-focus-variables\n --#{$prefix}focus-ring-width: #{$focus-ring-width};\n --#{$prefix}focus-ring-opacity: #{$focus-ring-opacity};\n --#{$prefix}focus-ring-color: #{$focus-ring-color};\n // scss-docs-end root-focus-variables\n\n // scss-docs-start root-form-validation-variables\n --#{$prefix}form-valid-color: #{$form-valid-color};\n --#{$prefix}form-valid-border-color: #{$form-valid-border-color};\n --#{$prefix}form-invalid-color: #{$form-invalid-color};\n --#{$prefix}form-invalid-border-color: #{$form-invalid-border-color};\n // scss-docs-end root-form-validation-variables\n}\n\n@if $enable-dark-mode {\n @include color-mode(dark, true) {\n color-scheme: dark;\n\n // scss-docs-start root-dark-mode-vars\n --#{$prefix}body-color: #{$body-color-dark};\n --#{$prefix}body-color-rgb: #{to-rgb($body-color-dark)};\n --#{$prefix}body-bg: #{$body-bg-dark};\n --#{$prefix}body-bg-rgb: #{to-rgb($body-bg-dark)};\n\n --#{$prefix}emphasis-color: #{$body-emphasis-color-dark};\n --#{$prefix}emphasis-color-rgb: #{to-rgb($body-emphasis-color-dark)};\n\n --#{$prefix}secondary-color: #{$body-secondary-color-dark};\n --#{$prefix}secondary-color-rgb: #{to-rgb($body-secondary-color-dark)};\n --#{$prefix}secondary-bg: #{$body-secondary-bg-dark};\n --#{$prefix}secondary-bg-rgb: #{to-rgb($body-secondary-bg-dark)};\n\n --#{$prefix}tertiary-color: #{$body-tertiary-color-dark};\n --#{$prefix}tertiary-color-rgb: #{to-rgb($body-tertiary-color-dark)};\n --#{$prefix}tertiary-bg: #{$body-tertiary-bg-dark};\n --#{$prefix}tertiary-bg-rgb: #{to-rgb($body-tertiary-bg-dark)};\n\n @each $color, $value in $theme-colors-text-dark {\n --#{$prefix}#{$color}-text-emphasis: #{$value};\n }\n\n @each $color, $value in $theme-colors-bg-subtle-dark {\n --#{$prefix}#{$color}-bg-subtle: #{$value};\n }\n\n @each $color, $value in $theme-colors-border-subtle-dark {\n --#{$prefix}#{$color}-border-subtle: #{$value};\n }\n\n --#{$prefix}heading-color: #{$headings-color-dark};\n\n --#{$prefix}link-color: #{$link-color-dark};\n --#{$prefix}link-hover-color: #{$link-hover-color-dark};\n --#{$prefix}link-color-rgb: #{to-rgb($link-color-dark)};\n --#{$prefix}link-hover-color-rgb: #{to-rgb($link-hover-color-dark)};\n\n --#{$prefix}code-color: #{$code-color-dark};\n --#{$prefix}highlight-color: #{$mark-color-dark};\n --#{$prefix}highlight-bg: #{$mark-bg-dark};\n\n --#{$prefix}border-color: #{$border-color-dark};\n --#{$prefix}border-color-translucent: #{$border-color-translucent-dark};\n\n --#{$prefix}form-valid-color: #{$form-valid-color-dark};\n --#{$prefix}form-valid-border-color: #{$form-valid-border-color-dark};\n --#{$prefix}form-invalid-color: #{$form-invalid-color-dark};\n --#{$prefix}form-invalid-border-color: #{$form-invalid-border-color-dark};\n // scss-docs-end root-dark-mode-vars\n }\n}\n","@charset \"UTF-8\";\n/*!\n * Bootstrap v5.3.3 (https://getbootstrap.com/)\n * Copyright 2011-2024 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n:root,\n[data-bs-theme=light] {\n --bs-blue: #0d6efd;\n --bs-indigo: #6610f2;\n --bs-purple: #6f42c1;\n --bs-pink: #d63384;\n --bs-red: #dc3545;\n --bs-orange: #fd7e14;\n --bs-yellow: #ffc107;\n --bs-green: #198754;\n --bs-teal: #20c997;\n --bs-cyan: #0dcaf0;\n --bs-black: #000;\n --bs-white: #fff;\n --bs-gray: #6c757d;\n --bs-gray-dark: #343a40;\n --bs-gray-100: #f8f9fa;\n --bs-gray-200: #e9ecef;\n --bs-gray-300: #dee2e6;\n --bs-gray-400: #ced4da;\n --bs-gray-500: #adb5bd;\n --bs-gray-600: #6c757d;\n --bs-gray-700: #495057;\n --bs-gray-800: #343a40;\n --bs-gray-900: #212529;\n --bs-primary: #0d6efd;\n --bs-secondary: #6c757d;\n --bs-success: #198754;\n --bs-info: #0dcaf0;\n --bs-warning: #ffc107;\n --bs-danger: #dc3545;\n --bs-light: #f8f9fa;\n --bs-dark: #212529;\n --bs-primary-rgb: 13, 110, 253;\n --bs-secondary-rgb: 108, 117, 125;\n --bs-success-rgb: 25, 135, 84;\n --bs-info-rgb: 13, 202, 240;\n --bs-warning-rgb: 255, 193, 7;\n --bs-danger-rgb: 220, 53, 69;\n --bs-light-rgb: 248, 249, 250;\n --bs-dark-rgb: 33, 37, 41;\n --bs-primary-text-emphasis: #052c65;\n --bs-secondary-text-emphasis: #2b2f32;\n --bs-success-text-emphasis: #0a3622;\n --bs-info-text-emphasis: #055160;\n --bs-warning-text-emphasis: #664d03;\n --bs-danger-text-emphasis: #58151c;\n --bs-light-text-emphasis: #495057;\n --bs-dark-text-emphasis: #495057;\n --bs-primary-bg-subtle: #cfe2ff;\n --bs-secondary-bg-subtle: #e2e3e5;\n --bs-success-bg-subtle: #d1e7dd;\n --bs-info-bg-subtle: #cff4fc;\n --bs-warning-bg-subtle: #fff3cd;\n --bs-danger-bg-subtle: #f8d7da;\n --bs-light-bg-subtle: #fcfcfd;\n --bs-dark-bg-subtle: #ced4da;\n --bs-primary-border-subtle: #9ec5fe;\n --bs-secondary-border-subtle: #c4c8cb;\n --bs-success-border-subtle: #a3cfbb;\n --bs-info-border-subtle: #9eeaf9;\n --bs-warning-border-subtle: #ffe69c;\n --bs-danger-border-subtle: #f1aeb5;\n --bs-light-border-subtle: #e9ecef;\n --bs-dark-border-subtle: #adb5bd;\n --bs-white-rgb: 255, 255, 255;\n --bs-black-rgb: 0, 0, 0;\n --bs-font-sans-serif: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", \"Noto Sans\", \"Liberation Sans\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));\n --bs-body-font-family: var(--bs-font-sans-serif);\n --bs-body-font-size: 1rem;\n --bs-body-font-weight: 400;\n --bs-body-line-height: 1.5;\n --bs-body-color: #212529;\n --bs-body-color-rgb: 33, 37, 41;\n --bs-body-bg: #fff;\n --bs-body-bg-rgb: 255, 255, 255;\n --bs-emphasis-color: #000;\n --bs-emphasis-color-rgb: 0, 0, 0;\n --bs-secondary-color: rgba(33, 37, 41, 0.75);\n --bs-secondary-color-rgb: 33, 37, 41;\n --bs-secondary-bg: #e9ecef;\n --bs-secondary-bg-rgb: 233, 236, 239;\n --bs-tertiary-color: rgba(33, 37, 41, 0.5);\n --bs-tertiary-color-rgb: 33, 37, 41;\n --bs-tertiary-bg: #f8f9fa;\n --bs-tertiary-bg-rgb: 248, 249, 250;\n --bs-heading-color: inherit;\n --bs-link-color: #0d6efd;\n --bs-link-color-rgb: 13, 110, 253;\n --bs-link-decoration: underline;\n --bs-link-hover-color: #0a58ca;\n --bs-link-hover-color-rgb: 10, 88, 202;\n --bs-code-color: #d63384;\n --bs-highlight-color: #212529;\n --bs-highlight-bg: #fff3cd;\n --bs-border-width: 1px;\n --bs-border-style: solid;\n --bs-border-color: #dee2e6;\n --bs-border-color-translucent: rgba(0, 0, 0, 0.175);\n --bs-border-radius: 0.375rem;\n --bs-border-radius-sm: 0.25rem;\n --bs-border-radius-lg: 0.5rem;\n --bs-border-radius-xl: 1rem;\n --bs-border-radius-xxl: 2rem;\n --bs-border-radius-2xl: var(--bs-border-radius-xxl);\n --bs-border-radius-pill: 50rem;\n --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);\n --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);\n --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);\n --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);\n --bs-focus-ring-width: 0.25rem;\n --bs-focus-ring-opacity: 0.25;\n --bs-focus-ring-color: rgba(13, 110, 253, 0.25);\n --bs-form-valid-color: #198754;\n --bs-form-valid-border-color: #198754;\n --bs-form-invalid-color: #dc3545;\n --bs-form-invalid-border-color: #dc3545;\n}\n\n[data-bs-theme=dark] {\n color-scheme: dark;\n --bs-body-color: #dee2e6;\n --bs-body-color-rgb: 222, 226, 230;\n --bs-body-bg: #212529;\n --bs-body-bg-rgb: 33, 37, 41;\n --bs-emphasis-color: #fff;\n --bs-emphasis-color-rgb: 255, 255, 255;\n --bs-secondary-color: rgba(222, 226, 230, 0.75);\n --bs-secondary-color-rgb: 222, 226, 230;\n --bs-secondary-bg: #343a40;\n --bs-secondary-bg-rgb: 52, 58, 64;\n --bs-tertiary-color: rgba(222, 226, 230, 0.5);\n --bs-tertiary-color-rgb: 222, 226, 230;\n --bs-tertiary-bg: #2b3035;\n --bs-tertiary-bg-rgb: 43, 48, 53;\n --bs-primary-text-emphasis: #6ea8fe;\n --bs-secondary-text-emphasis: #a7acb1;\n --bs-success-text-emphasis: #75b798;\n --bs-info-text-emphasis: #6edff6;\n --bs-warning-text-emphasis: #ffda6a;\n --bs-danger-text-emphasis: #ea868f;\n --bs-light-text-emphasis: #f8f9fa;\n --bs-dark-text-emphasis: #dee2e6;\n --bs-primary-bg-subtle: #031633;\n --bs-secondary-bg-subtle: #161719;\n --bs-success-bg-subtle: #051b11;\n --bs-info-bg-subtle: #032830;\n --bs-warning-bg-subtle: #332701;\n --bs-danger-bg-subtle: #2c0b0e;\n --bs-light-bg-subtle: #343a40;\n --bs-dark-bg-subtle: #1a1d20;\n --bs-primary-border-subtle: #084298;\n --bs-secondary-border-subtle: #41464b;\n --bs-success-border-subtle: #0f5132;\n --bs-info-border-subtle: #087990;\n --bs-warning-border-subtle: #997404;\n --bs-danger-border-subtle: #842029;\n --bs-light-border-subtle: #495057;\n --bs-dark-border-subtle: #343a40;\n --bs-heading-color: inherit;\n --bs-link-color: #6ea8fe;\n --bs-link-hover-color: #8bb9fe;\n --bs-link-color-rgb: 110, 168, 254;\n --bs-link-hover-color-rgb: 139, 185, 254;\n --bs-code-color: #e685b5;\n --bs-highlight-color: #dee2e6;\n --bs-highlight-bg: #664d03;\n --bs-border-color: #495057;\n --bs-border-color-translucent: rgba(255, 255, 255, 0.15);\n --bs-form-valid-color: #75b798;\n --bs-form-valid-border-color: #75b798;\n --bs-form-invalid-color: #ea868f;\n --bs-form-invalid-border-color: #ea868f;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n :root {\n scroll-behavior: smooth;\n }\n}\n\nbody {\n margin: 0;\n font-family: var(--bs-body-font-family);\n font-size: var(--bs-body-font-size);\n font-weight: var(--bs-body-font-weight);\n line-height: var(--bs-body-line-height);\n color: var(--bs-body-color);\n text-align: var(--bs-body-text-align);\n background-color: var(--bs-body-bg);\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\nhr {\n margin: 1rem 0;\n color: inherit;\n border: 0;\n border-top: var(--bs-border-width) solid;\n opacity: 0.25;\n}\n\nh6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n font-weight: 500;\n line-height: 1.2;\n color: var(--bs-heading-color);\n}\n\nh1, .h1 {\n font-size: calc(1.375rem + 1.5vw);\n}\n@media (min-width: 1200px) {\n h1, .h1 {\n font-size: 2.5rem;\n }\n}\n\nh2, .h2 {\n font-size: calc(1.325rem + 0.9vw);\n}\n@media (min-width: 1200px) {\n h2, .h2 {\n font-size: 2rem;\n }\n}\n\nh3, .h3 {\n font-size: calc(1.3rem + 0.6vw);\n}\n@media (min-width: 1200px) {\n h3, .h3 {\n font-size: 1.75rem;\n }\n}\n\nh4, .h4 {\n font-size: calc(1.275rem + 0.3vw);\n}\n@media (min-width: 1200px) {\n h4, .h4 {\n font-size: 1.5rem;\n }\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title] {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n cursor: help;\n -webkit-text-decoration-skip-ink: none;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: 0.5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall, .small {\n font-size: 0.875em;\n}\n\nmark, .mark {\n padding: 0.1875em;\n color: var(--bs-highlight-color);\n background-color: var(--bs-highlight-bg);\n}\n\nsub,\nsup {\n position: relative;\n font-size: 0.75em;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\na {\n color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));\n text-decoration: underline;\n}\na:hover {\n --bs-link-color-rgb: var(--bs-link-hover-color-rgb);\n}\n\na:not([href]):not([class]), a:not([href]):not([class]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: var(--bs-font-monospace);\n font-size: 1em;\n}\n\npre {\n display: block;\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n font-size: 0.875em;\n}\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\ncode {\n font-size: 0.875em;\n color: var(--bs-code-color);\n word-wrap: break-word;\n}\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.1875rem 0.375rem;\n font-size: 0.875em;\n color: var(--bs-body-bg);\n background-color: var(--bs-body-color);\n border-radius: 0.25rem;\n}\nkbd kbd {\n padding: 0;\n font-size: 1em;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n color: var(--bs-secondary-color);\n text-align: left;\n}\n\nth {\n text-align: inherit;\n text-align: -webkit-match-parent;\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\nlabel {\n display: inline-block;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\n[role=button] {\n cursor: pointer;\n}\n\nselect {\n word-wrap: normal;\n}\nselect:disabled {\n opacity: 1;\n}\n\n[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {\n display: none !important;\n}\n\nbutton,\n[type=button],\n[type=reset],\n[type=submit] {\n -webkit-appearance: button;\n}\nbutton:not(:disabled),\n[type=button]:not(:disabled),\n[type=reset]:not(:disabled),\n[type=submit]:not(:disabled) {\n cursor: pointer;\n}\n\n::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ntextarea {\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n float: left;\n width: 100%;\n padding: 0;\n margin-bottom: 0.5rem;\n font-size: calc(1.275rem + 0.3vw);\n line-height: inherit;\n}\n@media (min-width: 1200px) {\n legend {\n font-size: 1.5rem;\n }\n}\nlegend + * {\n clear: left;\n}\n\n::-webkit-datetime-edit-fields-wrapper,\n::-webkit-datetime-edit-text,\n::-webkit-datetime-edit-minute,\n::-webkit-datetime-edit-hour-field,\n::-webkit-datetime-edit-day-field,\n::-webkit-datetime-edit-month-field,\n::-webkit-datetime-edit-year-field {\n padding: 0;\n}\n\n::-webkit-inner-spin-button {\n height: auto;\n}\n\n[type=search] {\n -webkit-appearance: textfield;\n outline-offset: -2px;\n}\n\n/* rtl:raw:\n[type=\"tel\"],\n[type=\"url\"],\n[type=\"email\"],\n[type=\"number\"] {\n direction: ltr;\n}\n*/\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-color-swatch-wrapper {\n padding: 0;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\n::file-selector-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\niframe {\n border: 0;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[hidden] {\n display: none !important;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-size: calc(1.625rem + 4.5vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-1 {\n font-size: 5rem;\n }\n}\n\n.display-2 {\n font-size: calc(1.575rem + 3.9vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-2 {\n font-size: 4.5rem;\n }\n}\n\n.display-3 {\n font-size: calc(1.525rem + 3.3vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-3 {\n font-size: 4rem;\n }\n}\n\n.display-4 {\n font-size: calc(1.475rem + 2.7vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-4 {\n font-size: 3.5rem;\n }\n}\n\n.display-5 {\n font-size: calc(1.425rem + 2.1vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-5 {\n font-size: 3rem;\n }\n}\n\n.display-6 {\n font-size: calc(1.375rem + 1.5vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-6 {\n font-size: 2.5rem;\n }\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 0.875em;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n.blockquote > :last-child {\n margin-bottom: 0;\n}\n\n.blockquote-footer {\n margin-top: -1rem;\n margin-bottom: 1rem;\n font-size: 0.875em;\n color: #6c757d;\n}\n.blockquote-footer::before {\n content: \"— \";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: var(--bs-body-bg);\n border: var(--bs-border-width) solid var(--bs-border-color);\n border-radius: var(--bs-border-radius);\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 0.875em;\n color: var(--bs-secondary-color);\n}\n\n.container,\n.container-fluid,\n.container-xxl,\n.container-xl,\n.container-lg,\n.container-md,\n.container-sm {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n width: 100%;\n padding-right: calc(var(--bs-gutter-x) * 0.5);\n padding-left: calc(var(--bs-gutter-x) * 0.5);\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container-sm, .container {\n max-width: 540px;\n }\n}\n@media (min-width: 768px) {\n .container-md, .container-sm, .container {\n max-width: 720px;\n }\n}\n@media (min-width: 992px) {\n .container-lg, .container-md, .container-sm, .container {\n max-width: 960px;\n }\n}\n@media (min-width: 1200px) {\n .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1140px;\n }\n}\n@media (min-width: 1400px) {\n .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1320px;\n }\n}\n:root {\n --bs-breakpoint-xs: 0;\n --bs-breakpoint-sm: 576px;\n --bs-breakpoint-md: 768px;\n --bs-breakpoint-lg: 992px;\n --bs-breakpoint-xl: 1200px;\n --bs-breakpoint-xxl: 1400px;\n}\n\n.row {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(-1 * var(--bs-gutter-y));\n margin-right: calc(-0.5 * var(--bs-gutter-x));\n margin-left: calc(-0.5 * var(--bs-gutter-x));\n}\n.row > * {\n flex-shrink: 0;\n width: 100%;\n max-width: 100%;\n padding-right: calc(var(--bs-gutter-x) * 0.5);\n padding-left: calc(var(--bs-gutter-x) * 0.5);\n margin-top: var(--bs-gutter-y);\n}\n\n.col {\n flex: 1 0 0%;\n}\n\n.row-cols-auto > * {\n flex: 0 0 auto;\n width: auto;\n}\n\n.row-cols-1 > * {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 auto;\n width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n}\n\n.col-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n}\n\n.col-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-3 {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.col-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.col-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n}\n\n.col-6 {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.col-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n}\n\n.col-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n}\n\n.col-9 {\n flex: 0 0 auto;\n width: 75%;\n}\n\n.col-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n}\n\n.col-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n}\n\n.col-12 {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.offset-1 {\n margin-left: 8.33333333%;\n}\n\n.offset-2 {\n margin-left: 16.66666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.33333333%;\n}\n\n.offset-5 {\n margin-left: 41.66666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.33333333%;\n}\n\n.offset-8 {\n margin-left: 66.66666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.33333333%;\n}\n\n.offset-11 {\n margin-left: 91.66666667%;\n}\n\n.g-0,\n.gx-0 {\n --bs-gutter-x: 0;\n}\n\n.g-0,\n.gy-0 {\n --bs-gutter-y: 0;\n}\n\n.g-1,\n.gx-1 {\n --bs-gutter-x: 0.25rem;\n}\n\n.g-1,\n.gy-1 {\n --bs-gutter-y: 0.25rem;\n}\n\n.g-2,\n.gx-2 {\n --bs-gutter-x: 0.5rem;\n}\n\n.g-2,\n.gy-2 {\n --bs-gutter-y: 0.5rem;\n}\n\n.g-3,\n.gx-3 {\n --bs-gutter-x: 1rem;\n}\n\n.g-3,\n.gy-3 {\n --bs-gutter-y: 1rem;\n}\n\n.g-4,\n.gx-4 {\n --bs-gutter-x: 1.5rem;\n}\n\n.g-4,\n.gy-4 {\n --bs-gutter-y: 1.5rem;\n}\n\n.g-5,\n.gx-5 {\n --bs-gutter-x: 3rem;\n}\n\n.g-5,\n.gy-5 {\n --bs-gutter-y: 3rem;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex: 1 0 0%;\n }\n .row-cols-sm-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-sm-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-sm-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-sm-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-sm-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-sm-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-sm-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-sm-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-sm-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-sm-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-sm-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-sm-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-sm-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-sm-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-sm-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-sm-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.33333333%;\n }\n .offset-sm-2 {\n margin-left: 16.66666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.33333333%;\n }\n .offset-sm-5 {\n margin-left: 41.66666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.33333333%;\n }\n .offset-sm-8 {\n margin-left: 66.66666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.33333333%;\n }\n .offset-sm-11 {\n margin-left: 91.66666667%;\n }\n .g-sm-0,\n .gx-sm-0 {\n --bs-gutter-x: 0;\n }\n .g-sm-0,\n .gy-sm-0 {\n --bs-gutter-y: 0;\n }\n .g-sm-1,\n .gx-sm-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-sm-1,\n .gy-sm-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-sm-2,\n .gx-sm-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-sm-2,\n .gy-sm-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-sm-3,\n .gx-sm-3 {\n --bs-gutter-x: 1rem;\n }\n .g-sm-3,\n .gy-sm-3 {\n --bs-gutter-y: 1rem;\n }\n .g-sm-4,\n .gx-sm-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-sm-4,\n .gy-sm-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-sm-5,\n .gx-sm-5 {\n --bs-gutter-x: 3rem;\n }\n .g-sm-5,\n .gy-sm-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 768px) {\n .col-md {\n flex: 1 0 0%;\n }\n .row-cols-md-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-md-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-md-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-md-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-md-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-md-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-md-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-md-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-md-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-md-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-md-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-md-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-md-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-md-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-md-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-md-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-md-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-md-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-md-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.33333333%;\n }\n .offset-md-2 {\n margin-left: 16.66666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.33333333%;\n }\n .offset-md-5 {\n margin-left: 41.66666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.33333333%;\n }\n .offset-md-8 {\n margin-left: 66.66666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.33333333%;\n }\n .offset-md-11 {\n margin-left: 91.66666667%;\n }\n .g-md-0,\n .gx-md-0 {\n --bs-gutter-x: 0;\n }\n .g-md-0,\n .gy-md-0 {\n --bs-gutter-y: 0;\n }\n .g-md-1,\n .gx-md-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-md-1,\n .gy-md-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-md-2,\n .gx-md-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-md-2,\n .gy-md-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-md-3,\n .gx-md-3 {\n --bs-gutter-x: 1rem;\n }\n .g-md-3,\n .gy-md-3 {\n --bs-gutter-y: 1rem;\n }\n .g-md-4,\n .gx-md-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-md-4,\n .gy-md-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-md-5,\n .gx-md-5 {\n --bs-gutter-x: 3rem;\n }\n .g-md-5,\n .gy-md-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 992px) {\n .col-lg {\n flex: 1 0 0%;\n }\n .row-cols-lg-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-lg-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-lg-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-lg-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-lg-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-lg-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-lg-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-lg-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-lg-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-lg-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-lg-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-lg-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-lg-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-lg-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-lg-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-lg-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.33333333%;\n }\n .offset-lg-2 {\n margin-left: 16.66666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.33333333%;\n }\n .offset-lg-5 {\n margin-left: 41.66666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.33333333%;\n }\n .offset-lg-8 {\n margin-left: 66.66666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.33333333%;\n }\n .offset-lg-11 {\n margin-left: 91.66666667%;\n }\n .g-lg-0,\n .gx-lg-0 {\n --bs-gutter-x: 0;\n }\n .g-lg-0,\n .gy-lg-0 {\n --bs-gutter-y: 0;\n }\n .g-lg-1,\n .gx-lg-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-lg-1,\n .gy-lg-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-lg-2,\n .gx-lg-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-lg-2,\n .gy-lg-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-lg-3,\n .gx-lg-3 {\n --bs-gutter-x: 1rem;\n }\n .g-lg-3,\n .gy-lg-3 {\n --bs-gutter-y: 1rem;\n }\n .g-lg-4,\n .gx-lg-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-lg-4,\n .gy-lg-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-lg-5,\n .gx-lg-5 {\n --bs-gutter-x: 3rem;\n }\n .g-lg-5,\n .gy-lg-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1200px) {\n .col-xl {\n flex: 1 0 0%;\n }\n .row-cols-xl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-xl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-xl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-xl-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-xl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-xl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-xl-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-xl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-xl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-xl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-xl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-xl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-xl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-xl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-xl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.33333333%;\n }\n .offset-xl-2 {\n margin-left: 16.66666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.33333333%;\n }\n .offset-xl-5 {\n margin-left: 41.66666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.33333333%;\n }\n .offset-xl-8 {\n margin-left: 66.66666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.33333333%;\n }\n .offset-xl-11 {\n margin-left: 91.66666667%;\n }\n .g-xl-0,\n .gx-xl-0 {\n --bs-gutter-x: 0;\n }\n .g-xl-0,\n .gy-xl-0 {\n --bs-gutter-y: 0;\n }\n .g-xl-1,\n .gx-xl-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-xl-1,\n .gy-xl-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-xl-2,\n .gx-xl-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-xl-2,\n .gy-xl-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-xl-3,\n .gx-xl-3 {\n --bs-gutter-x: 1rem;\n }\n .g-xl-3,\n .gy-xl-3 {\n --bs-gutter-y: 1rem;\n }\n .g-xl-4,\n .gx-xl-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-xl-4,\n .gy-xl-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-xl-5,\n .gx-xl-5 {\n --bs-gutter-x: 3rem;\n }\n .g-xl-5,\n .gy-xl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1400px) {\n .col-xxl {\n flex: 1 0 0%;\n }\n .row-cols-xxl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-xxl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-xxl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-xxl-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-xxl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-xxl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-xxl-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xxl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-xxl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-xxl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xxl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-xxl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-xxl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-xxl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-xxl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-xxl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-xxl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-xxl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-xxl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-xxl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-xxl-0 {\n margin-left: 0;\n }\n .offset-xxl-1 {\n margin-left: 8.33333333%;\n }\n .offset-xxl-2 {\n margin-left: 16.66666667%;\n }\n .offset-xxl-3 {\n margin-left: 25%;\n }\n .offset-xxl-4 {\n margin-left: 33.33333333%;\n }\n .offset-xxl-5 {\n margin-left: 41.66666667%;\n }\n .offset-xxl-6 {\n margin-left: 50%;\n }\n .offset-xxl-7 {\n margin-left: 58.33333333%;\n }\n .offset-xxl-8 {\n margin-left: 66.66666667%;\n }\n .offset-xxl-9 {\n margin-left: 75%;\n }\n .offset-xxl-10 {\n margin-left: 83.33333333%;\n }\n .offset-xxl-11 {\n margin-left: 91.66666667%;\n }\n .g-xxl-0,\n .gx-xxl-0 {\n --bs-gutter-x: 0;\n }\n .g-xxl-0,\n .gy-xxl-0 {\n --bs-gutter-y: 0;\n }\n .g-xxl-1,\n .gx-xxl-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-xxl-1,\n .gy-xxl-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-xxl-2,\n .gx-xxl-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-xxl-2,\n .gy-xxl-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-xxl-3,\n .gx-xxl-3 {\n --bs-gutter-x: 1rem;\n }\n .g-xxl-3,\n .gy-xxl-3 {\n --bs-gutter-y: 1rem;\n }\n .g-xxl-4,\n .gx-xxl-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-xxl-4,\n .gy-xxl-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-xxl-5,\n .gx-xxl-5 {\n --bs-gutter-x: 3rem;\n }\n .g-xxl-5,\n .gy-xxl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n.table {\n --bs-table-color-type: initial;\n --bs-table-bg-type: initial;\n --bs-table-color-state: initial;\n --bs-table-bg-state: initial;\n --bs-table-color: var(--bs-emphasis-color);\n --bs-table-bg: var(--bs-body-bg);\n --bs-table-border-color: var(--bs-border-color);\n --bs-table-accent-bg: transparent;\n --bs-table-striped-color: var(--bs-emphasis-color);\n --bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05);\n --bs-table-active-color: var(--bs-emphasis-color);\n --bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), 0.1);\n --bs-table-hover-color: var(--bs-emphasis-color);\n --bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.075);\n width: 100%;\n margin-bottom: 1rem;\n vertical-align: top;\n border-color: var(--bs-table-border-color);\n}\n.table > :not(caption) > * > * {\n padding: 0.5rem 0.5rem;\n color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));\n background-color: var(--bs-table-bg);\n border-bottom-width: var(--bs-border-width);\n box-shadow: inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg)));\n}\n.table > tbody {\n vertical-align: inherit;\n}\n.table > thead {\n vertical-align: bottom;\n}\n\n.table-group-divider {\n border-top: calc(var(--bs-border-width) * 2) solid currentcolor;\n}\n\n.caption-top {\n caption-side: top;\n}\n\n.table-sm > :not(caption) > * > * {\n padding: 0.25rem 0.25rem;\n}\n\n.table-bordered > :not(caption) > * {\n border-width: var(--bs-border-width) 0;\n}\n.table-bordered > :not(caption) > * > * {\n border-width: 0 var(--bs-border-width);\n}\n\n.table-borderless > :not(caption) > * > * {\n border-bottom-width: 0;\n}\n.table-borderless > :not(:first-child) {\n border-top-width: 0;\n}\n\n.table-striped > tbody > tr:nth-of-type(odd) > * {\n --bs-table-color-type: var(--bs-table-striped-color);\n --bs-table-bg-type: var(--bs-table-striped-bg);\n}\n\n.table-striped-columns > :not(caption) > tr > :nth-child(even) {\n --bs-table-color-type: var(--bs-table-striped-color);\n --bs-table-bg-type: var(--bs-table-striped-bg);\n}\n\n.table-active {\n --bs-table-color-state: var(--bs-table-active-color);\n --bs-table-bg-state: var(--bs-table-active-bg);\n}\n\n.table-hover > tbody > tr:hover > * {\n --bs-table-color-state: var(--bs-table-hover-color);\n --bs-table-bg-state: var(--bs-table-hover-bg);\n}\n\n.table-primary {\n --bs-table-color: #000;\n --bs-table-bg: #cfe2ff;\n --bs-table-border-color: #a6b5cc;\n --bs-table-striped-bg: #c5d7f2;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #bacbe6;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #bfd1ec;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-secondary {\n --bs-table-color: #000;\n --bs-table-bg: #e2e3e5;\n --bs-table-border-color: #b5b6b7;\n --bs-table-striped-bg: #d7d8da;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #cbccce;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #d1d2d4;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-success {\n --bs-table-color: #000;\n --bs-table-bg: #d1e7dd;\n --bs-table-border-color: #a7b9b1;\n --bs-table-striped-bg: #c7dbd2;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #bcd0c7;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #c1d6cc;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-info {\n --bs-table-color: #000;\n --bs-table-bg: #cff4fc;\n --bs-table-border-color: #a6c3ca;\n --bs-table-striped-bg: #c5e8ef;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #badce3;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #bfe2e9;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-warning {\n --bs-table-color: #000;\n --bs-table-bg: #fff3cd;\n --bs-table-border-color: #ccc2a4;\n --bs-table-striped-bg: #f2e7c3;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #e6dbb9;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #ece1be;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-danger {\n --bs-table-color: #000;\n --bs-table-bg: #f8d7da;\n --bs-table-border-color: #c6acae;\n --bs-table-striped-bg: #eccccf;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #dfc2c4;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #e5c7ca;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-light {\n --bs-table-color: #000;\n --bs-table-bg: #f8f9fa;\n --bs-table-border-color: #c6c7c8;\n --bs-table-striped-bg: #ecedee;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #dfe0e1;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #e5e6e7;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-dark {\n --bs-table-color: #fff;\n --bs-table-bg: #212529;\n --bs-table-border-color: #4d5154;\n --bs-table-striped-bg: #2c3034;\n --bs-table-striped-color: #fff;\n --bs-table-active-bg: #373b3e;\n --bs-table-active-color: #fff;\n --bs-table-hover-bg: #323539;\n --bs-table-hover-color: #fff;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-responsive {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 767.98px) {\n .table-responsive-md {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 1399.98px) {\n .table-responsive-xxl {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n.form-label {\n margin-bottom: 0.5rem;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + var(--bs-border-width));\n padding-bottom: calc(0.375rem + var(--bs-border-width));\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + var(--bs-border-width));\n padding-bottom: calc(0.5rem + var(--bs-border-width));\n font-size: 1.25rem;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + var(--bs-border-width));\n padding-bottom: calc(0.25rem + var(--bs-border-width));\n font-size: 0.875rem;\n}\n\n.form-text {\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: var(--bs-secondary-color);\n}\n\n.form-control {\n display: block;\n width: 100%;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: var(--bs-body-color);\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n background-color: var(--bs-body-bg);\n background-clip: padding-box;\n border: var(--bs-border-width) solid var(--bs-border-color);\n border-radius: var(--bs-border-radius);\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n.form-control[type=file] {\n overflow: hidden;\n}\n.form-control[type=file]:not(:disabled):not([readonly]) {\n cursor: pointer;\n}\n.form-control:focus {\n color: var(--bs-body-color);\n background-color: var(--bs-body-bg);\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-control::-webkit-date-and-time-value {\n min-width: 85px;\n height: 1.5em;\n margin: 0;\n}\n.form-control::-webkit-datetime-edit {\n display: block;\n padding: 0;\n}\n.form-control::-moz-placeholder {\n color: var(--bs-secondary-color);\n opacity: 1;\n}\n.form-control::placeholder {\n color: var(--bs-secondary-color);\n opacity: 1;\n}\n.form-control:disabled {\n background-color: var(--bs-secondary-bg);\n opacity: 1;\n}\n.form-control::-webkit-file-upload-button {\n padding: 0.375rem 0.75rem;\n margin: -0.375rem -0.75rem;\n -webkit-margin-end: 0.75rem;\n margin-inline-end: 0.75rem;\n color: var(--bs-body-color);\n background-color: var(--bs-tertiary-bg);\n pointer-events: none;\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n border-inline-end-width: var(--bs-border-width);\n border-radius: 0;\n -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n.form-control::file-selector-button {\n padding: 0.375rem 0.75rem;\n margin: -0.375rem -0.75rem;\n -webkit-margin-end: 0.75rem;\n margin-inline-end: 0.75rem;\n color: var(--bs-body-color);\n background-color: var(--bs-tertiary-bg);\n pointer-events: none;\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n border-inline-end-width: var(--bs-border-width);\n border-radius: 0;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-control::-webkit-file-upload-button {\n -webkit-transition: none;\n transition: none;\n }\n .form-control::file-selector-button {\n transition: none;\n }\n}\n.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button {\n background-color: var(--bs-secondary-bg);\n}\n.form-control:hover:not(:disabled):not([readonly])::file-selector-button {\n background-color: var(--bs-secondary-bg);\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding: 0.375rem 0;\n margin-bottom: 0;\n line-height: 1.5;\n color: var(--bs-body-color);\n background-color: transparent;\n border: solid transparent;\n border-width: var(--bs-border-width) 0;\n}\n.form-control-plaintext:focus {\n outline: 0;\n}\n.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm {\n min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n border-radius: var(--bs-border-radius-sm);\n}\n.form-control-sm::-webkit-file-upload-button {\n padding: 0.25rem 0.5rem;\n margin: -0.25rem -0.5rem;\n -webkit-margin-end: 0.5rem;\n margin-inline-end: 0.5rem;\n}\n.form-control-sm::file-selector-button {\n padding: 0.25rem 0.5rem;\n margin: -0.25rem -0.5rem;\n -webkit-margin-end: 0.5rem;\n margin-inline-end: 0.5rem;\n}\n\n.form-control-lg {\n min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n border-radius: var(--bs-border-radius-lg);\n}\n.form-control-lg::-webkit-file-upload-button {\n padding: 0.5rem 1rem;\n margin: -0.5rem -1rem;\n -webkit-margin-end: 1rem;\n margin-inline-end: 1rem;\n}\n.form-control-lg::file-selector-button {\n padding: 0.5rem 1rem;\n margin: -0.5rem -1rem;\n -webkit-margin-end: 1rem;\n margin-inline-end: 1rem;\n}\n\ntextarea.form-control {\n min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));\n}\ntextarea.form-control-sm {\n min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));\n}\ntextarea.form-control-lg {\n min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));\n}\n\n.form-control-color {\n width: 3rem;\n height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));\n padding: 0.375rem;\n}\n.form-control-color:not(:disabled):not([readonly]) {\n cursor: pointer;\n}\n.form-control-color::-moz-color-swatch {\n border: 0 !important;\n border-radius: var(--bs-border-radius);\n}\n.form-control-color::-webkit-color-swatch {\n border: 0 !important;\n border-radius: var(--bs-border-radius);\n}\n.form-control-color.form-control-sm {\n height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));\n}\n.form-control-color.form-control-lg {\n height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));\n}\n\n.form-select {\n --bs-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='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e\");\n display: block;\n width: 100%;\n padding: 0.375rem 2.25rem 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: var(--bs-body-color);\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n background-color: var(--bs-body-bg);\n background-image: var(--bs-form-select-bg-img), var(--bs-form-select-bg-icon, none);\n background-repeat: no-repeat;\n background-position: right 0.75rem center;\n background-size: 16px 12px;\n border: var(--bs-border-width) solid var(--bs-border-color);\n border-radius: var(--bs-border-radius);\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-select {\n transition: none;\n }\n}\n.form-select:focus {\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-select[multiple], .form-select[size]:not([size=\"1\"]) {\n padding-right: 0.75rem;\n background-image: none;\n}\n.form-select:disabled {\n background-color: var(--bs-secondary-bg);\n}\n.form-select:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 var(--bs-body-color);\n}\n\n.form-select-sm {\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n padding-left: 0.5rem;\n font-size: 0.875rem;\n border-radius: var(--bs-border-radius-sm);\n}\n\n.form-select-lg {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n padding-left: 1rem;\n font-size: 1.25rem;\n border-radius: var(--bs-border-radius-lg);\n}\n\n[data-bs-theme=dark] .form-select {\n --bs-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='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e\");\n}\n\n.form-check {\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5em;\n margin-bottom: 0.125rem;\n}\n.form-check .form-check-input {\n float: left;\n margin-left: -1.5em;\n}\n\n.form-check-reverse {\n padding-right: 1.5em;\n padding-left: 0;\n text-align: right;\n}\n.form-check-reverse .form-check-input {\n float: right;\n margin-right: -1.5em;\n margin-left: 0;\n}\n\n.form-check-input {\n --bs-form-check-bg: var(--bs-body-bg);\n flex-shrink: 0;\n width: 1em;\n height: 1em;\n margin-top: 0.25em;\n vertical-align: top;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n background-color: var(--bs-form-check-bg);\n background-image: var(--bs-form-check-bg-image);\n background-repeat: no-repeat;\n background-position: center;\n background-size: contain;\n border: var(--bs-border-width) solid var(--bs-border-color);\n -webkit-print-color-adjust: exact;\n color-adjust: exact;\n print-color-adjust: exact;\n}\n.form-check-input[type=checkbox] {\n border-radius: 0.25em;\n}\n.form-check-input[type=radio] {\n border-radius: 50%;\n}\n.form-check-input:active {\n filter: brightness(90%);\n}\n.form-check-input:focus {\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-check-input:checked {\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.form-check-input:checked[type=checkbox] {\n --bs-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='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e\");\n}\n.form-check-input:checked[type=radio] {\n --bs-form-check-bg-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e\");\n}\n.form-check-input[type=checkbox]:indeterminate {\n background-color: #0d6efd;\n border-color: #0d6efd;\n --bs-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='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e\");\n}\n.form-check-input:disabled {\n pointer-events: none;\n filter: none;\n opacity: 0.5;\n}\n.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label {\n cursor: default;\n opacity: 0.5;\n}\n\n.form-switch {\n padding-left: 2.5em;\n}\n.form-switch .form-check-input {\n --bs-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%280, 0, 0, 0.25%29'/%3e%3c/svg%3e\");\n width: 2em;\n margin-left: -2.5em;\n background-image: var(--bs-form-switch-bg);\n background-position: left center;\n border-radius: 2em;\n transition: background-position 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-switch .form-check-input {\n transition: none;\n }\n}\n.form-switch .form-check-input:focus {\n --bs-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='%2386b7fe'/%3e%3c/svg%3e\");\n}\n.form-switch .form-check-input:checked {\n background-position: right center;\n --bs-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='%23fff'/%3e%3c/svg%3e\");\n}\n.form-switch.form-check-reverse {\n padding-right: 2.5em;\n padding-left: 0;\n}\n.form-switch.form-check-reverse .form-check-input {\n margin-right: -2.5em;\n margin-left: 0;\n}\n\n.form-check-inline {\n display: inline-block;\n margin-right: 1rem;\n}\n\n.btn-check {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.btn-check[disabled] + .btn, .btn-check:disabled + .btn {\n pointer-events: none;\n filter: none;\n opacity: 0.65;\n}\n\n[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus) {\n --bs-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\");\n}\n\n.form-range {\n width: 100%;\n height: 1.5rem;\n padding: 0;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n background-color: transparent;\n}\n.form-range:focus {\n outline: 0;\n}\n.form-range:focus::-webkit-slider-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-range:focus::-moz-range-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-range::-moz-focus-outer {\n border: 0;\n}\n.form-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n -webkit-appearance: none;\n appearance: none;\n background-color: #0d6efd;\n border: 0;\n border-radius: 1rem;\n -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-range::-webkit-slider-thumb {\n -webkit-transition: none;\n transition: none;\n }\n}\n.form-range::-webkit-slider-thumb:active {\n background-color: #b6d4fe;\n}\n.form-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: var(--bs-secondary-bg);\n border-color: transparent;\n border-radius: 1rem;\n}\n.form-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n -moz-appearance: none;\n appearance: none;\n background-color: #0d6efd;\n border: 0;\n border-radius: 1rem;\n -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-range::-moz-range-thumb {\n -moz-transition: none;\n transition: none;\n }\n}\n.form-range::-moz-range-thumb:active {\n background-color: #b6d4fe;\n}\n.form-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: var(--bs-secondary-bg);\n border-color: transparent;\n border-radius: 1rem;\n}\n.form-range:disabled {\n pointer-events: none;\n}\n.form-range:disabled::-webkit-slider-thumb {\n background-color: var(--bs-secondary-color);\n}\n.form-range:disabled::-moz-range-thumb {\n background-color: var(--bs-secondary-color);\n}\n\n.form-floating {\n position: relative;\n}\n.form-floating > .form-control,\n.form-floating > .form-control-plaintext,\n.form-floating > .form-select {\n height: calc(3.5rem + calc(var(--bs-border-width) * 2));\n min-height: calc(3.5rem + calc(var(--bs-border-width) * 2));\n line-height: 1.25;\n}\n.form-floating > label {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 2;\n height: 100%;\n padding: 1rem 0.75rem;\n overflow: hidden;\n text-align: start;\n text-overflow: ellipsis;\n white-space: nowrap;\n pointer-events: none;\n border: var(--bs-border-width) solid transparent;\n transform-origin: 0 0;\n transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-floating > label {\n transition: none;\n }\n}\n.form-floating > .form-control,\n.form-floating > .form-control-plaintext {\n padding: 1rem 0.75rem;\n}\n.form-floating > .form-control::-moz-placeholder, .form-floating > .form-control-plaintext::-moz-placeholder {\n color: transparent;\n}\n.form-floating > .form-control::placeholder,\n.form-floating > .form-control-plaintext::placeholder {\n color: transparent;\n}\n.form-floating > .form-control:not(:-moz-placeholder-shown), .form-floating > .form-control-plaintext:not(:-moz-placeholder-shown) {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown),\n.form-floating > .form-control-plaintext:focus,\n.form-floating > .form-control-plaintext:not(:placeholder-shown) {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-control:-webkit-autofill,\n.form-floating > .form-control-plaintext:-webkit-autofill {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-select {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label {\n color: rgba(var(--bs-body-color-rgb), 0.65);\n transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n.form-floating > .form-control:focus ~ label,\n.form-floating > .form-control:not(:placeholder-shown) ~ label,\n.form-floating > .form-control-plaintext ~ label,\n.form-floating > .form-select ~ label {\n color: rgba(var(--bs-body-color-rgb), 0.65);\n transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label::after {\n position: absolute;\n inset: 1rem 0.375rem;\n z-index: -1;\n height: 1.5em;\n content: \"\";\n background-color: var(--bs-body-bg);\n border-radius: var(--bs-border-radius);\n}\n.form-floating > .form-control:focus ~ label::after,\n.form-floating > .form-control:not(:placeholder-shown) ~ label::after,\n.form-floating > .form-control-plaintext ~ label::after,\n.form-floating > .form-select ~ label::after {\n position: absolute;\n inset: 1rem 0.375rem;\n z-index: -1;\n height: 1.5em;\n content: \"\";\n background-color: var(--bs-body-bg);\n border-radius: var(--bs-border-radius);\n}\n.form-floating > .form-control:-webkit-autofill ~ label {\n color: rgba(var(--bs-body-color-rgb), 0.65);\n transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n.form-floating > .form-control-plaintext ~ label {\n border-width: var(--bs-border-width) 0;\n}\n.form-floating > :disabled ~ label,\n.form-floating > .form-control:disabled ~ label {\n color: #6c757d;\n}\n.form-floating > :disabled ~ label::after,\n.form-floating > .form-control:disabled ~ label::after {\n background-color: var(--bs-secondary-bg);\n}\n\n.input-group {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n width: 100%;\n}\n.input-group > .form-control,\n.input-group > .form-select,\n.input-group > .form-floating {\n position: relative;\n flex: 1 1 auto;\n width: 1%;\n min-width: 0;\n}\n.input-group > .form-control:focus,\n.input-group > .form-select:focus,\n.input-group > .form-floating:focus-within {\n z-index: 5;\n}\n.input-group .btn {\n position: relative;\n z-index: 2;\n}\n.input-group .btn:focus {\n z-index: 5;\n}\n\n.input-group-text {\n display: flex;\n align-items: center;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: var(--bs-body-color);\n text-align: center;\n white-space: nowrap;\n background-color: var(--bs-tertiary-bg);\n border: var(--bs-border-width) solid var(--bs-border-color);\n border-radius: var(--bs-border-radius);\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .form-select,\n.input-group-lg > .input-group-text,\n.input-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n border-radius: var(--bs-border-radius-lg);\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .form-select,\n.input-group-sm > .input-group-text,\n.input-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n border-radius: var(--bs-border-radius-sm);\n}\n\n.input-group-lg > .form-select,\n.input-group-sm > .form-select {\n padding-right: 3rem;\n}\n\n.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),\n.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3),\n.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control,\n.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),\n.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4),\n.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-control,\n.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-select {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {\n margin-left: calc(var(--bs-border-width) * -1);\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.input-group > .form-floating:not(:first-child) > .form-control,\n.input-group > .form-floating:not(:first-child) > .form-select {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: var(--bs-form-valid-color);\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: 0.1rem;\n font-size: 0.875rem;\n color: #fff;\n background-color: var(--bs-success);\n border-radius: var(--bs-border-radius);\n}\n\n.was-validated :valid ~ .valid-feedback,\n.was-validated :valid ~ .valid-tooltip,\n.is-valid ~ .valid-feedback,\n.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid {\n border-color: var(--bs-form-valid-border-color);\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus {\n border-color: var(--bs-form-valid-border-color);\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);\n}\n\n.was-validated textarea.form-control:valid, textarea.form-control.is-valid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .form-select:valid, .form-select.is-valid {\n border-color: var(--bs-form-valid-border-color);\n}\n.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\"] {\n --bs-form-select-bg-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n padding-right: 4.125rem;\n background-position: right 0.75rem center, center right 2.25rem;\n background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-select:valid:focus, .form-select.is-valid:focus {\n border-color: var(--bs-form-valid-border-color);\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);\n}\n\n.was-validated .form-control-color:valid, .form-control-color.is-valid {\n width: calc(3rem + calc(1.5em + 0.75rem));\n}\n\n.was-validated .form-check-input:valid, .form-check-input.is-valid {\n border-color: var(--bs-form-valid-border-color);\n}\n.was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked {\n background-color: var(--bs-form-valid-color);\n}\n.was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus {\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);\n}\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: var(--bs-form-valid-color);\n}\n\n.form-check-inline .form-check-input ~ .valid-feedback {\n margin-left: 0.5em;\n}\n\n.was-validated .input-group > .form-control:not(:focus):valid, .input-group > .form-control:not(:focus).is-valid,\n.was-validated .input-group > .form-select:not(:focus):valid,\n.input-group > .form-select:not(:focus).is-valid,\n.was-validated .input-group > .form-floating:not(:focus-within):valid,\n.input-group > .form-floating:not(:focus-within).is-valid {\n z-index: 3;\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: var(--bs-form-invalid-color);\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: 0.1rem;\n font-size: 0.875rem;\n color: #fff;\n background-color: var(--bs-danger);\n border-radius: var(--bs-border-radius);\n}\n\n.was-validated :invalid ~ .invalid-feedback,\n.was-validated :invalid ~ .invalid-tooltip,\n.is-invalid ~ .invalid-feedback,\n.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid {\n border-color: var(--bs-form-invalid-border-color);\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {\n border-color: var(--bs-form-invalid-border-color);\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);\n}\n\n.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .form-select:invalid, .form-select.is-invalid {\n border-color: var(--bs-form-invalid-border-color);\n}\n.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]), .form-select.is-invalid:not([multiple])[size=\"1\"] {\n --bs-form-select-bg-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n padding-right: 4.125rem;\n background-position: right 0.75rem center, center right 2.25rem;\n background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-select:invalid:focus, .form-select.is-invalid:focus {\n border-color: var(--bs-form-invalid-border-color);\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);\n}\n\n.was-validated .form-control-color:invalid, .form-control-color.is-invalid {\n width: calc(3rem + calc(1.5em + 0.75rem));\n}\n\n.was-validated .form-check-input:invalid, .form-check-input.is-invalid {\n border-color: var(--bs-form-invalid-border-color);\n}\n.was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked {\n background-color: var(--bs-form-invalid-color);\n}\n.was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus {\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);\n}\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: var(--bs-form-invalid-color);\n}\n\n.form-check-inline .form-check-input ~ .invalid-feedback {\n margin-left: 0.5em;\n}\n\n.was-validated .input-group > .form-control:not(:focus):invalid, .input-group > .form-control:not(:focus).is-invalid,\n.was-validated .input-group > .form-select:not(:focus):invalid,\n.input-group > .form-select:not(:focus).is-invalid,\n.was-validated .input-group > .form-floating:not(:focus-within):invalid,\n.input-group > .form-floating:not(:focus-within).is-invalid {\n z-index: 4;\n}\n\n.btn {\n --bs-btn-padding-x: 0.75rem;\n --bs-btn-padding-y: 0.375rem;\n --bs-btn-font-family: ;\n --bs-btn-font-size: 1rem;\n --bs-btn-font-weight: 400;\n --bs-btn-line-height: 1.5;\n --bs-btn-color: var(--bs-body-color);\n --bs-btn-bg: transparent;\n --bs-btn-border-width: var(--bs-border-width);\n --bs-btn-border-color: transparent;\n --bs-btn-border-radius: var(--bs-border-radius);\n --bs-btn-hover-border-color: transparent;\n --bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n --bs-btn-disabled-opacity: 0.65;\n --bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);\n display: inline-block;\n padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x);\n font-family: var(--bs-btn-font-family);\n font-size: var(--bs-btn-font-size);\n font-weight: var(--bs-btn-font-weight);\n line-height: var(--bs-btn-line-height);\n color: var(--bs-btn-color);\n text-align: center;\n text-decoration: none;\n vertical-align: middle;\n cursor: pointer;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n border: var(--bs-btn-border-width) solid var(--bs-btn-border-color);\n border-radius: var(--bs-btn-border-radius);\n background-color: var(--bs-btn-bg);\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n.btn:hover {\n color: var(--bs-btn-hover-color);\n background-color: var(--bs-btn-hover-bg);\n border-color: var(--bs-btn-hover-border-color);\n}\n.btn-check + .btn:hover {\n color: var(--bs-btn-color);\n background-color: var(--bs-btn-bg);\n border-color: var(--bs-btn-border-color);\n}\n.btn:focus-visible {\n color: var(--bs-btn-hover-color);\n background-color: var(--bs-btn-hover-bg);\n border-color: var(--bs-btn-hover-border-color);\n outline: 0;\n box-shadow: var(--bs-btn-focus-box-shadow);\n}\n.btn-check:focus-visible + .btn {\n border-color: var(--bs-btn-hover-border-color);\n outline: 0;\n box-shadow: var(--bs-btn-focus-box-shadow);\n}\n.btn-check:checked + .btn, :not(.btn-check) + .btn:active, .btn:first-child:active, .btn.active, .btn.show {\n color: var(--bs-btn-active-color);\n background-color: var(--bs-btn-active-bg);\n border-color: var(--bs-btn-active-border-color);\n}\n.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 {\n box-shadow: var(--bs-btn-focus-box-shadow);\n}\n.btn-check:checked:focus-visible + .btn {\n box-shadow: var(--bs-btn-focus-box-shadow);\n}\n.btn:disabled, .btn.disabled, fieldset:disabled .btn {\n color: var(--bs-btn-disabled-color);\n pointer-events: none;\n background-color: var(--bs-btn-disabled-bg);\n border-color: var(--bs-btn-disabled-border-color);\n opacity: var(--bs-btn-disabled-opacity);\n}\n\n.btn-primary {\n --bs-btn-color: #fff;\n --bs-btn-bg: #0d6efd;\n --bs-btn-border-color: #0d6efd;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #0b5ed7;\n --bs-btn-hover-border-color: #0a58ca;\n --bs-btn-focus-shadow-rgb: 49, 132, 253;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #0a58ca;\n --bs-btn-active-border-color: #0a53be;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #fff;\n --bs-btn-disabled-bg: #0d6efd;\n --bs-btn-disabled-border-color: #0d6efd;\n}\n\n.btn-secondary {\n --bs-btn-color: #fff;\n --bs-btn-bg: #6c757d;\n --bs-btn-border-color: #6c757d;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #5c636a;\n --bs-btn-hover-border-color: #565e64;\n --bs-btn-focus-shadow-rgb: 130, 138, 145;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #565e64;\n --bs-btn-active-border-color: #51585e;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #fff;\n --bs-btn-disabled-bg: #6c757d;\n --bs-btn-disabled-border-color: #6c757d;\n}\n\n.btn-success {\n --bs-btn-color: #fff;\n --bs-btn-bg: #198754;\n --bs-btn-border-color: #198754;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #157347;\n --bs-btn-hover-border-color: #146c43;\n --bs-btn-focus-shadow-rgb: 60, 153, 110;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #146c43;\n --bs-btn-active-border-color: #13653f;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #fff;\n --bs-btn-disabled-bg: #198754;\n --bs-btn-disabled-border-color: #198754;\n}\n\n.btn-info {\n --bs-btn-color: #000;\n --bs-btn-bg: #0dcaf0;\n --bs-btn-border-color: #0dcaf0;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #31d2f2;\n --bs-btn-hover-border-color: #25cff2;\n --bs-btn-focus-shadow-rgb: 11, 172, 204;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #3dd5f3;\n --bs-btn-active-border-color: #25cff2;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #000;\n --bs-btn-disabled-bg: #0dcaf0;\n --bs-btn-disabled-border-color: #0dcaf0;\n}\n\n.btn-warning {\n --bs-btn-color: #000;\n --bs-btn-bg: #ffc107;\n --bs-btn-border-color: #ffc107;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #ffca2c;\n --bs-btn-hover-border-color: #ffc720;\n --bs-btn-focus-shadow-rgb: 217, 164, 6;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #ffcd39;\n --bs-btn-active-border-color: #ffc720;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #000;\n --bs-btn-disabled-bg: #ffc107;\n --bs-btn-disabled-border-color: #ffc107;\n}\n\n.btn-danger {\n --bs-btn-color: #fff;\n --bs-btn-bg: #dc3545;\n --bs-btn-border-color: #dc3545;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #bb2d3b;\n --bs-btn-hover-border-color: #b02a37;\n --bs-btn-focus-shadow-rgb: 225, 83, 97;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #b02a37;\n --bs-btn-active-border-color: #a52834;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #fff;\n --bs-btn-disabled-bg: #dc3545;\n --bs-btn-disabled-border-color: #dc3545;\n}\n\n.btn-light {\n --bs-btn-color: #000;\n --bs-btn-bg: #f8f9fa;\n --bs-btn-border-color: #f8f9fa;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #d3d4d5;\n --bs-btn-hover-border-color: #c6c7c8;\n --bs-btn-focus-shadow-rgb: 211, 212, 213;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #c6c7c8;\n --bs-btn-active-border-color: #babbbc;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #000;\n --bs-btn-disabled-bg: #f8f9fa;\n --bs-btn-disabled-border-color: #f8f9fa;\n}\n\n.btn-dark {\n --bs-btn-color: #fff;\n --bs-btn-bg: #212529;\n --bs-btn-border-color: #212529;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #424649;\n --bs-btn-hover-border-color: #373b3e;\n --bs-btn-focus-shadow-rgb: 66, 70, 73;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #4d5154;\n --bs-btn-active-border-color: #373b3e;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #fff;\n --bs-btn-disabled-bg: #212529;\n --bs-btn-disabled-border-color: #212529;\n}\n\n.btn-outline-primary {\n --bs-btn-color: #0d6efd;\n --bs-btn-border-color: #0d6efd;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #0d6efd;\n --bs-btn-hover-border-color: #0d6efd;\n --bs-btn-focus-shadow-rgb: 13, 110, 253;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #0d6efd;\n --bs-btn-active-border-color: #0d6efd;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #0d6efd;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #0d6efd;\n --bs-gradient: none;\n}\n\n.btn-outline-secondary {\n --bs-btn-color: #6c757d;\n --bs-btn-border-color: #6c757d;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #6c757d;\n --bs-btn-hover-border-color: #6c757d;\n --bs-btn-focus-shadow-rgb: 108, 117, 125;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #6c757d;\n --bs-btn-active-border-color: #6c757d;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #6c757d;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #6c757d;\n --bs-gradient: none;\n}\n\n.btn-outline-success {\n --bs-btn-color: #198754;\n --bs-btn-border-color: #198754;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #198754;\n --bs-btn-hover-border-color: #198754;\n --bs-btn-focus-shadow-rgb: 25, 135, 84;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #198754;\n --bs-btn-active-border-color: #198754;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #198754;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #198754;\n --bs-gradient: none;\n}\n\n.btn-outline-info {\n --bs-btn-color: #0dcaf0;\n --bs-btn-border-color: #0dcaf0;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #0dcaf0;\n --bs-btn-hover-border-color: #0dcaf0;\n --bs-btn-focus-shadow-rgb: 13, 202, 240;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #0dcaf0;\n --bs-btn-active-border-color: #0dcaf0;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #0dcaf0;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #0dcaf0;\n --bs-gradient: none;\n}\n\n.btn-outline-warning {\n --bs-btn-color: #ffc107;\n --bs-btn-border-color: #ffc107;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #ffc107;\n --bs-btn-hover-border-color: #ffc107;\n --bs-btn-focus-shadow-rgb: 255, 193, 7;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #ffc107;\n --bs-btn-active-border-color: #ffc107;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #ffc107;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #ffc107;\n --bs-gradient: none;\n}\n\n.btn-outline-danger {\n --bs-btn-color: #dc3545;\n --bs-btn-border-color: #dc3545;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #dc3545;\n --bs-btn-hover-border-color: #dc3545;\n --bs-btn-focus-shadow-rgb: 220, 53, 69;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #dc3545;\n --bs-btn-active-border-color: #dc3545;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #dc3545;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #dc3545;\n --bs-gradient: none;\n}\n\n.btn-outline-light {\n --bs-btn-color: #f8f9fa;\n --bs-btn-border-color: #f8f9fa;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #f8f9fa;\n --bs-btn-hover-border-color: #f8f9fa;\n --bs-btn-focus-shadow-rgb: 248, 249, 250;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #f8f9fa;\n --bs-btn-active-border-color: #f8f9fa;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #f8f9fa;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #f8f9fa;\n --bs-gradient: none;\n}\n\n.btn-outline-dark {\n --bs-btn-color: #212529;\n --bs-btn-border-color: #212529;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #212529;\n --bs-btn-hover-border-color: #212529;\n --bs-btn-focus-shadow-rgb: 33, 37, 41;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #212529;\n --bs-btn-active-border-color: #212529;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #212529;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #212529;\n --bs-gradient: none;\n}\n\n.btn-link {\n --bs-btn-font-weight: 400;\n --bs-btn-color: var(--bs-link-color);\n --bs-btn-bg: transparent;\n --bs-btn-border-color: transparent;\n --bs-btn-hover-color: var(--bs-link-hover-color);\n --bs-btn-hover-border-color: transparent;\n --bs-btn-active-color: var(--bs-link-hover-color);\n --bs-btn-active-border-color: transparent;\n --bs-btn-disabled-color: #6c757d;\n --bs-btn-disabled-border-color: transparent;\n --bs-btn-box-shadow: 0 0 0 #000;\n --bs-btn-focus-shadow-rgb: 49, 132, 253;\n text-decoration: underline;\n}\n.btn-link:focus-visible {\n color: var(--bs-btn-color);\n}\n.btn-link:hover {\n color: var(--bs-btn-hover-color);\n}\n\n.btn-lg, .btn-group-lg > .btn {\n --bs-btn-padding-y: 0.5rem;\n --bs-btn-padding-x: 1rem;\n --bs-btn-font-size: 1.25rem;\n --bs-btn-border-radius: var(--bs-border-radius-lg);\n}\n\n.btn-sm, .btn-group-sm > .btn {\n --bs-btn-padding-y: 0.25rem;\n --bs-btn-padding-x: 0.5rem;\n --bs-btn-font-size: 0.875rem;\n --bs-btn-border-radius: var(--bs-border-radius-sm);\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n@media (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n.collapsing.collapse-horizontal {\n width: 0;\n height: auto;\n transition: width 0.35s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .collapsing.collapse-horizontal {\n transition: none;\n }\n}\n\n.dropup,\n.dropend,\n.dropdown,\n.dropstart,\n.dropup-center,\n.dropdown-center {\n position: relative;\n}\n\n.dropdown-toggle {\n white-space: nowrap;\n}\n.dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n --bs-dropdown-zindex: 1000;\n --bs-dropdown-min-width: 10rem;\n --bs-dropdown-padding-x: 0;\n --bs-dropdown-padding-y: 0.5rem;\n --bs-dropdown-spacer: 0.125rem;\n --bs-dropdown-font-size: 1rem;\n --bs-dropdown-color: var(--bs-body-color);\n --bs-dropdown-bg: var(--bs-body-bg);\n --bs-dropdown-border-color: var(--bs-border-color-translucent);\n --bs-dropdown-border-radius: var(--bs-border-radius);\n --bs-dropdown-border-width: var(--bs-border-width);\n --bs-dropdown-inner-border-radius: calc(var(--bs-border-radius) - var(--bs-border-width));\n --bs-dropdown-divider-bg: var(--bs-border-color-translucent);\n --bs-dropdown-divider-margin-y: 0.5rem;\n --bs-dropdown-box-shadow: var(--bs-box-shadow);\n --bs-dropdown-link-color: var(--bs-body-color);\n --bs-dropdown-link-hover-color: var(--bs-body-color);\n --bs-dropdown-link-hover-bg: var(--bs-tertiary-bg);\n --bs-dropdown-link-active-color: #fff;\n --bs-dropdown-link-active-bg: #0d6efd;\n --bs-dropdown-link-disabled-color: var(--bs-tertiary-color);\n --bs-dropdown-item-padding-x: 1rem;\n --bs-dropdown-item-padding-y: 0.25rem;\n --bs-dropdown-header-color: #6c757d;\n --bs-dropdown-header-padding-x: 1rem;\n --bs-dropdown-header-padding-y: 0.5rem;\n position: absolute;\n z-index: var(--bs-dropdown-zindex);\n display: none;\n min-width: var(--bs-dropdown-min-width);\n padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);\n margin: 0;\n font-size: var(--bs-dropdown-font-size);\n color: var(--bs-dropdown-color);\n text-align: left;\n list-style: none;\n background-color: var(--bs-dropdown-bg);\n background-clip: padding-box;\n border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);\n border-radius: var(--bs-dropdown-border-radius);\n}\n.dropdown-menu[data-bs-popper] {\n top: 100%;\n left: 0;\n margin-top: var(--bs-dropdown-spacer);\n}\n\n.dropdown-menu-start {\n --bs-position: start;\n}\n.dropdown-menu-start[data-bs-popper] {\n right: auto;\n left: 0;\n}\n\n.dropdown-menu-end {\n --bs-position: end;\n}\n.dropdown-menu-end[data-bs-popper] {\n right: 0;\n left: auto;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-start {\n --bs-position: start;\n }\n .dropdown-menu-sm-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n .dropdown-menu-sm-end {\n --bs-position: end;\n }\n .dropdown-menu-sm-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 768px) {\n .dropdown-menu-md-start {\n --bs-position: start;\n }\n .dropdown-menu-md-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n .dropdown-menu-md-end {\n --bs-position: end;\n }\n .dropdown-menu-md-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 992px) {\n .dropdown-menu-lg-start {\n --bs-position: start;\n }\n .dropdown-menu-lg-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n .dropdown-menu-lg-end {\n --bs-position: end;\n }\n .dropdown-menu-lg-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 1200px) {\n .dropdown-menu-xl-start {\n --bs-position: start;\n }\n .dropdown-menu-xl-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n .dropdown-menu-xl-end {\n --bs-position: end;\n }\n .dropdown-menu-xl-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 1400px) {\n .dropdown-menu-xxl-start {\n --bs-position: start;\n }\n .dropdown-menu-xxl-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n .dropdown-menu-xxl-end {\n --bs-position: end;\n }\n .dropdown-menu-xxl-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n.dropup .dropdown-menu[data-bs-popper] {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: var(--bs-dropdown-spacer);\n}\n.dropup .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropend .dropdown-menu[data-bs-popper] {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: var(--bs-dropdown-spacer);\n}\n.dropend .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n.dropend .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n.dropend .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropstart .dropdown-menu[data-bs-popper] {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: var(--bs-dropdown-spacer);\n}\n.dropstart .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n.dropstart .dropdown-toggle::after {\n display: none;\n}\n.dropstart .dropdown-toggle::before {\n display: inline-block;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n.dropstart .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n.dropstart .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-divider {\n height: 0;\n margin: var(--bs-dropdown-divider-margin-y) 0;\n overflow: hidden;\n border-top: 1px solid var(--bs-dropdown-divider-bg);\n opacity: 1;\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);\n clear: both;\n font-weight: 400;\n color: var(--bs-dropdown-link-color);\n text-align: inherit;\n text-decoration: none;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n border-radius: var(--bs-dropdown-item-border-radius, 0);\n}\n.dropdown-item:hover, .dropdown-item:focus {\n color: var(--bs-dropdown-link-hover-color);\n background-color: var(--bs-dropdown-link-hover-bg);\n}\n.dropdown-item.active, .dropdown-item:active {\n color: var(--bs-dropdown-link-active-color);\n text-decoration: none;\n background-color: var(--bs-dropdown-link-active-bg);\n}\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: var(--bs-dropdown-link-disabled-color);\n pointer-events: none;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);\n margin-bottom: 0;\n font-size: 0.875rem;\n color: var(--bs-dropdown-header-color);\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);\n color: var(--bs-dropdown-link-color);\n}\n\n.dropdown-menu-dark {\n --bs-dropdown-color: #dee2e6;\n --bs-dropdown-bg: #343a40;\n --bs-dropdown-border-color: var(--bs-border-color-translucent);\n --bs-dropdown-box-shadow: ;\n --bs-dropdown-link-color: #dee2e6;\n --bs-dropdown-link-hover-color: #fff;\n --bs-dropdown-divider-bg: var(--bs-border-color-translucent);\n --bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.15);\n --bs-dropdown-link-active-color: #fff;\n --bs-dropdown-link-active-bg: #0d6efd;\n --bs-dropdown-link-disabled-color: #adb5bd;\n --bs-dropdown-header-color: #adb5bd;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-flex;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n flex: 1 1 auto;\n}\n.btn-group > .btn-check:checked + .btn,\n.btn-group > .btn-check:focus + .btn,\n.btn-group > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn-check:checked + .btn,\n.btn-group-vertical > .btn-check:focus + .btn,\n.btn-group-vertical > .btn:hover,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-toolbar {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n}\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group {\n border-radius: var(--bs-border-radius);\n}\n.btn-group > :not(.btn-check:first-child) + .btn,\n.btn-group > .btn-group:not(:first-child) {\n margin-left: calc(var(--bs-border-width) * -1);\n}\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn.dropdown-toggle-split:first-child,\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn:nth-child(n+3),\n.btn-group > :not(.btn-check) + .btn,\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n.dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropend .dropdown-toggle-split::after {\n margin-left: 0;\n}\n.dropstart .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group {\n width: 100%;\n}\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) {\n margin-top: calc(var(--bs-border-width) * -1);\n}\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn ~ .btn,\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav {\n --bs-nav-link-padding-x: 1rem;\n --bs-nav-link-padding-y: 0.5rem;\n --bs-nav-link-font-weight: ;\n --bs-nav-link-color: var(--bs-link-color);\n --bs-nav-link-hover-color: var(--bs-link-hover-color);\n --bs-nav-link-disabled-color: var(--bs-secondary-color);\n display: flex;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);\n font-size: var(--bs-nav-link-font-size);\n font-weight: var(--bs-nav-link-font-weight);\n color: var(--bs-nav-link-color);\n text-decoration: none;\n background: none;\n border: 0;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .nav-link {\n transition: none;\n }\n}\n.nav-link:hover, .nav-link:focus {\n color: var(--bs-nav-link-hover-color);\n}\n.nav-link:focus-visible {\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.nav-link.disabled, .nav-link:disabled {\n color: var(--bs-nav-link-disabled-color);\n pointer-events: none;\n cursor: default;\n}\n\n.nav-tabs {\n --bs-nav-tabs-border-width: var(--bs-border-width);\n --bs-nav-tabs-border-color: var(--bs-border-color);\n --bs-nav-tabs-border-radius: var(--bs-border-radius);\n --bs-nav-tabs-link-hover-border-color: var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);\n --bs-nav-tabs-link-active-color: var(--bs-emphasis-color);\n --bs-nav-tabs-link-active-bg: var(--bs-body-bg);\n --bs-nav-tabs-link-active-border-color: var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);\n border-bottom: var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color);\n}\n.nav-tabs .nav-link {\n margin-bottom: calc(-1 * var(--bs-nav-tabs-border-width));\n border: var(--bs-nav-tabs-border-width) solid transparent;\n border-top-left-radius: var(--bs-nav-tabs-border-radius);\n border-top-right-radius: var(--bs-nav-tabs-border-radius);\n}\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n isolation: isolate;\n border-color: var(--bs-nav-tabs-link-hover-border-color);\n}\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: var(--bs-nav-tabs-link-active-color);\n background-color: var(--bs-nav-tabs-link-active-bg);\n border-color: var(--bs-nav-tabs-link-active-border-color);\n}\n.nav-tabs .dropdown-menu {\n margin-top: calc(-1 * var(--bs-nav-tabs-border-width));\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills {\n --bs-nav-pills-border-radius: var(--bs-border-radius);\n --bs-nav-pills-link-active-color: #fff;\n --bs-nav-pills-link-active-bg: #0d6efd;\n}\n.nav-pills .nav-link {\n border-radius: var(--bs-nav-pills-border-radius);\n}\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: var(--bs-nav-pills-link-active-color);\n background-color: var(--bs-nav-pills-link-active-bg);\n}\n\n.nav-underline {\n --bs-nav-underline-gap: 1rem;\n --bs-nav-underline-border-width: 0.125rem;\n --bs-nav-underline-link-active-color: var(--bs-emphasis-color);\n gap: var(--bs-nav-underline-gap);\n}\n.nav-underline .nav-link {\n padding-right: 0;\n padding-left: 0;\n border-bottom: var(--bs-nav-underline-border-width) solid transparent;\n}\n.nav-underline .nav-link:hover, .nav-underline .nav-link:focus {\n border-bottom-color: currentcolor;\n}\n.nav-underline .nav-link.active,\n.nav-underline .show > .nav-link {\n font-weight: 700;\n color: var(--bs-nav-underline-link-active-color);\n border-bottom-color: currentcolor;\n}\n\n.nav-fill > .nav-link,\n.nav-fill .nav-item {\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified > .nav-link,\n.nav-justified .nav-item {\n flex-basis: 0;\n flex-grow: 1;\n text-align: center;\n}\n\n.nav-fill .nav-item .nav-link,\n.nav-justified .nav-item .nav-link {\n width: 100%;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n --bs-navbar-padding-x: 0;\n --bs-navbar-padding-y: 0.5rem;\n --bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), 0.65);\n --bs-navbar-hover-color: rgba(var(--bs-emphasis-color-rgb), 0.8);\n --bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), 0.3);\n --bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1);\n --bs-navbar-brand-padding-y: 0.3125rem;\n --bs-navbar-brand-margin-end: 1rem;\n --bs-navbar-brand-font-size: 1.25rem;\n --bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1);\n --bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1);\n --bs-navbar-nav-link-padding-x: 0.5rem;\n --bs-navbar-toggler-padding-y: 0.25rem;\n --bs-navbar-toggler-padding-x: 0.75rem;\n --bs-navbar-toggler-font-size: 1.25rem;\n --bs-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%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n --bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15);\n --bs-navbar-toggler-border-radius: var(--bs-border-radius);\n --bs-navbar-toggler-focus-width: 0.25rem;\n --bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n padding: var(--bs-navbar-padding-y) var(--bs-navbar-padding-x);\n}\n.navbar > .container,\n.navbar > .container-fluid,\n.navbar > .container-sm,\n.navbar > .container-md,\n.navbar > .container-lg,\n.navbar > .container-xl,\n.navbar > .container-xxl {\n display: flex;\n flex-wrap: inherit;\n align-items: center;\n justify-content: space-between;\n}\n.navbar-brand {\n padding-top: var(--bs-navbar-brand-padding-y);\n padding-bottom: var(--bs-navbar-brand-padding-y);\n margin-right: var(--bs-navbar-brand-margin-end);\n font-size: var(--bs-navbar-brand-font-size);\n color: var(--bs-navbar-brand-color);\n text-decoration: none;\n white-space: nowrap;\n}\n.navbar-brand:hover, .navbar-brand:focus {\n color: var(--bs-navbar-brand-hover-color);\n}\n\n.navbar-nav {\n --bs-nav-link-padding-x: 0;\n --bs-nav-link-padding-y: 0.5rem;\n --bs-nav-link-font-weight: ;\n --bs-nav-link-color: var(--bs-navbar-color);\n --bs-nav-link-hover-color: var(--bs-navbar-hover-color);\n --bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n.navbar-nav .nav-link.active, .navbar-nav .nav-link.show {\n color: var(--bs-navbar-active-color);\n}\n.navbar-nav .dropdown-menu {\n position: static;\n}\n\n.navbar-text {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n color: var(--bs-navbar-color);\n}\n.navbar-text a,\n.navbar-text a:hover,\n.navbar-text a:focus {\n color: var(--bs-navbar-active-color);\n}\n\n.navbar-collapse {\n flex-basis: 100%;\n flex-grow: 1;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);\n font-size: var(--bs-navbar-toggler-font-size);\n line-height: 1;\n color: var(--bs-navbar-color);\n background-color: transparent;\n border: var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);\n border-radius: var(--bs-navbar-toggler-border-radius);\n transition: var(--bs-navbar-toggler-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n .navbar-toggler {\n transition: none;\n }\n}\n.navbar-toggler:hover {\n text-decoration: none;\n}\n.navbar-toggler:focus {\n text-decoration: none;\n outline: 0;\n box-shadow: 0 0 0 var(--bs-navbar-toggler-focus-width);\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n background-image: var(--bs-navbar-toggler-icon-bg);\n background-repeat: no-repeat;\n background-position: center;\n background-size: 100%;\n}\n\n.navbar-nav-scroll {\n max-height: var(--bs-scroll-height, 75vh);\n overflow-y: auto;\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n }\n .navbar-expand-sm .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-sm .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n .navbar-expand-sm .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n }\n .navbar-expand-sm .offcanvas .offcanvas-header {\n display: none;\n }\n .navbar-expand-sm .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 768px) {\n .navbar-expand-md {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n }\n .navbar-expand-md .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-md .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n .navbar-expand-md .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n }\n .navbar-expand-md .offcanvas .offcanvas-header {\n display: none;\n }\n .navbar-expand-md .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 992px) {\n .navbar-expand-lg {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n }\n .navbar-expand-lg .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-lg .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n .navbar-expand-lg .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n }\n .navbar-expand-lg .offcanvas .offcanvas-header {\n display: none;\n }\n .navbar-expand-lg .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n }\n .navbar-expand-xl .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-xl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n .navbar-expand-xl .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n }\n .navbar-expand-xl .offcanvas .offcanvas-header {\n display: none;\n }\n .navbar-expand-xl .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 1400px) {\n .navbar-expand-xxl {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xxl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xxl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xxl .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n }\n .navbar-expand-xxl .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-xxl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xxl .navbar-toggler {\n display: none;\n }\n .navbar-expand-xxl .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n }\n .navbar-expand-xxl .offcanvas .offcanvas-header {\n display: none;\n }\n .navbar-expand-xxl .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n.navbar-expand {\n flex-wrap: nowrap;\n justify-content: flex-start;\n}\n.navbar-expand .navbar-nav {\n flex-direction: row;\n}\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n.navbar-expand .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n}\n.navbar-expand .navbar-nav-scroll {\n overflow: visible;\n}\n.navbar-expand .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n}\n.navbar-expand .navbar-toggler {\n display: none;\n}\n.navbar-expand .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n}\n.navbar-expand .offcanvas .offcanvas-header {\n display: none;\n}\n.navbar-expand .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n}\n\n.navbar-dark,\n.navbar[data-bs-theme=dark] {\n --bs-navbar-color: rgba(255, 255, 255, 0.55);\n --bs-navbar-hover-color: rgba(255, 255, 255, 0.75);\n --bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);\n --bs-navbar-active-color: #fff;\n --bs-navbar-brand-color: #fff;\n --bs-navbar-brand-hover-color: #fff;\n --bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1);\n --bs-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.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n[data-bs-theme=dark] .navbar-toggler-icon {\n --bs-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.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.card {\n --bs-card-spacer-y: 1rem;\n --bs-card-spacer-x: 1rem;\n --bs-card-title-spacer-y: 0.5rem;\n --bs-card-title-color: ;\n --bs-card-subtitle-color: ;\n --bs-card-border-width: var(--bs-border-width);\n --bs-card-border-color: var(--bs-border-color-translucent);\n --bs-card-border-radius: var(--bs-border-radius);\n --bs-card-box-shadow: ;\n --bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));\n --bs-card-cap-padding-y: 0.5rem;\n --bs-card-cap-padding-x: 1rem;\n --bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03);\n --bs-card-cap-color: ;\n --bs-card-height: ;\n --bs-card-color: ;\n --bs-card-bg: var(--bs-body-bg);\n --bs-card-img-overlay-padding: 1rem;\n --bs-card-group-margin: 0.75rem;\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n height: var(--bs-card-height);\n color: var(--bs-body-color);\n word-wrap: break-word;\n background-color: var(--bs-card-bg);\n background-clip: border-box;\n border: var(--bs-card-border-width) solid var(--bs-card-border-color);\n border-radius: var(--bs-card-border-radius);\n}\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n.card > .list-group {\n border-top: inherit;\n border-bottom: inherit;\n}\n.card > .list-group:first-child {\n border-top-width: 0;\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);\n}\n.card > .list-group:last-child {\n border-bottom-width: 0;\n border-bottom-right-radius: var(--bs-card-inner-border-radius);\n border-bottom-left-radius: var(--bs-card-inner-border-radius);\n}\n.card > .card-header + .list-group,\n.card > .list-group + .card-footer {\n border-top: 0;\n}\n\n.card-body {\n flex: 1 1 auto;\n padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x);\n color: var(--bs-card-color);\n}\n\n.card-title {\n margin-bottom: var(--bs-card-title-spacer-y);\n color: var(--bs-card-title-color);\n}\n\n.card-subtitle {\n margin-top: calc(-0.5 * var(--bs-card-title-spacer-y));\n margin-bottom: 0;\n color: var(--bs-card-subtitle-color);\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link + .card-link {\n margin-left: var(--bs-card-spacer-x);\n}\n\n.card-header {\n padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);\n margin-bottom: 0;\n color: var(--bs-card-cap-color);\n background-color: var(--bs-card-cap-bg);\n border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color);\n}\n.card-header:first-child {\n border-radius: var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0;\n}\n\n.card-footer {\n padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);\n color: var(--bs-card-cap-color);\n background-color: var(--bs-card-cap-bg);\n border-top: var(--bs-card-border-width) solid var(--bs-card-border-color);\n}\n.card-footer:last-child {\n border-radius: 0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius);\n}\n\n.card-header-tabs {\n margin-right: calc(-0.5 * var(--bs-card-cap-padding-x));\n margin-bottom: calc(-1 * var(--bs-card-cap-padding-y));\n margin-left: calc(-0.5 * var(--bs-card-cap-padding-x));\n border-bottom: 0;\n}\n.card-header-tabs .nav-link.active {\n background-color: var(--bs-card-bg);\n border-bottom-color: var(--bs-card-bg);\n}\n\n.card-header-pills {\n margin-right: calc(-0.5 * var(--bs-card-cap-padding-x));\n margin-left: calc(-0.5 * var(--bs-card-cap-padding-x));\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: var(--bs-card-img-overlay-padding);\n border-radius: var(--bs-card-inner-border-radius);\n}\n\n.card-img,\n.card-img-top,\n.card-img-bottom {\n width: 100%;\n}\n\n.card-img,\n.card-img-top {\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);\n}\n\n.card-img,\n.card-img-bottom {\n border-bottom-right-radius: var(--bs-card-inner-border-radius);\n border-bottom-left-radius: var(--bs-card-inner-border-radius);\n}\n\n.card-group > .card {\n margin-bottom: var(--bs-card-group-margin);\n}\n@media (min-width: 576px) {\n .card-group {\n display: flex;\n flex-flow: row wrap;\n }\n .card-group > .card {\n flex: 1 0 0%;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-top,\n .card-group > .card:not(:last-child) .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-bottom,\n .card-group > .card:not(:last-child) .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-top,\n .card-group > .card:not(:first-child) .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-bottom,\n .card-group > .card:not(:first-child) .card-footer {\n border-bottom-left-radius: 0;\n }\n}\n\n.accordion {\n --bs-accordion-color: var(--bs-body-color);\n --bs-accordion-bg: var(--bs-body-bg);\n --bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;\n --bs-accordion-border-color: var(--bs-border-color);\n --bs-accordion-border-width: var(--bs-border-width);\n --bs-accordion-border-radius: var(--bs-border-radius);\n --bs-accordion-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));\n --bs-accordion-btn-padding-x: 1.25rem;\n --bs-accordion-btn-padding-y: 1rem;\n --bs-accordion-btn-color: var(--bs-body-color);\n --bs-accordion-btn-bg: var(--bs-accordion-bg);\n --bs-accordion-btn-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e\");\n --bs-accordion-btn-icon-width: 1.25rem;\n --bs-accordion-btn-icon-transform: rotate(-180deg);\n --bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;\n --bs-accordion-btn-active-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e\");\n --bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n --bs-accordion-body-padding-x: 1.25rem;\n --bs-accordion-body-padding-y: 1rem;\n --bs-accordion-active-color: var(--bs-primary-text-emphasis);\n --bs-accordion-active-bg: var(--bs-primary-bg-subtle);\n}\n\n.accordion-button {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n padding: var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);\n font-size: 1rem;\n color: var(--bs-accordion-btn-color);\n text-align: left;\n background-color: var(--bs-accordion-btn-bg);\n border: 0;\n border-radius: 0;\n overflow-anchor: none;\n transition: var(--bs-accordion-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n .accordion-button {\n transition: none;\n }\n}\n.accordion-button:not(.collapsed) {\n color: var(--bs-accordion-active-color);\n background-color: var(--bs-accordion-active-bg);\n box-shadow: inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color);\n}\n.accordion-button:not(.collapsed)::after {\n background-image: var(--bs-accordion-btn-active-icon);\n transform: var(--bs-accordion-btn-icon-transform);\n}\n.accordion-button::after {\n flex-shrink: 0;\n width: var(--bs-accordion-btn-icon-width);\n height: var(--bs-accordion-btn-icon-width);\n margin-left: auto;\n content: \"\";\n background-image: var(--bs-accordion-btn-icon);\n background-repeat: no-repeat;\n background-size: var(--bs-accordion-btn-icon-width);\n transition: var(--bs-accordion-btn-icon-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n .accordion-button::after {\n transition: none;\n }\n}\n.accordion-button:hover {\n z-index: 2;\n}\n.accordion-button:focus {\n z-index: 3;\n outline: 0;\n box-shadow: var(--bs-accordion-btn-focus-box-shadow);\n}\n\n.accordion-header {\n margin-bottom: 0;\n}\n\n.accordion-item {\n color: var(--bs-accordion-color);\n background-color: var(--bs-accordion-bg);\n border: var(--bs-accordion-border-width) solid var(--bs-accordion-border-color);\n}\n.accordion-item:first-of-type {\n border-top-left-radius: var(--bs-accordion-border-radius);\n border-top-right-radius: var(--bs-accordion-border-radius);\n}\n.accordion-item:first-of-type > .accordion-header .accordion-button {\n border-top-left-radius: var(--bs-accordion-inner-border-radius);\n border-top-right-radius: var(--bs-accordion-inner-border-radius);\n}\n.accordion-item:not(:first-of-type) {\n border-top: 0;\n}\n.accordion-item:last-of-type {\n border-bottom-right-radius: var(--bs-accordion-border-radius);\n border-bottom-left-radius: var(--bs-accordion-border-radius);\n}\n.accordion-item:last-of-type > .accordion-header .accordion-button.collapsed {\n border-bottom-right-radius: var(--bs-accordion-inner-border-radius);\n border-bottom-left-radius: var(--bs-accordion-inner-border-radius);\n}\n.accordion-item:last-of-type > .accordion-collapse {\n border-bottom-right-radius: var(--bs-accordion-border-radius);\n border-bottom-left-radius: var(--bs-accordion-border-radius);\n}\n\n.accordion-body {\n padding: var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x);\n}\n\n.accordion-flush > .accordion-item {\n border-right: 0;\n border-left: 0;\n border-radius: 0;\n}\n.accordion-flush > .accordion-item:first-child {\n border-top: 0;\n}\n.accordion-flush > .accordion-item:last-child {\n border-bottom: 0;\n}\n.accordion-flush > .accordion-item > .accordion-header .accordion-button, .accordion-flush > .accordion-item > .accordion-header .accordion-button.collapsed {\n border-radius: 0;\n}\n.accordion-flush > .accordion-item > .accordion-collapse {\n border-radius: 0;\n}\n\n[data-bs-theme=dark] .accordion-button::after {\n --bs-accordion-btn-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n --bs-accordion-btn-active-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n}\n\n.breadcrumb {\n --bs-breadcrumb-padding-x: 0;\n --bs-breadcrumb-padding-y: 0;\n --bs-breadcrumb-margin-bottom: 1rem;\n --bs-breadcrumb-bg: ;\n --bs-breadcrumb-border-radius: ;\n --bs-breadcrumb-divider-color: var(--bs-secondary-color);\n --bs-breadcrumb-item-padding-x: 0.5rem;\n --bs-breadcrumb-item-active-color: var(--bs-secondary-color);\n display: flex;\n flex-wrap: wrap;\n padding: var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);\n margin-bottom: var(--bs-breadcrumb-margin-bottom);\n font-size: var(--bs-breadcrumb-font-size);\n list-style: none;\n background-color: var(--bs-breadcrumb-bg);\n border-radius: var(--bs-breadcrumb-border-radius);\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: var(--bs-breadcrumb-item-padding-x);\n}\n.breadcrumb-item + .breadcrumb-item::before {\n float: left;\n padding-right: var(--bs-breadcrumb-item-padding-x);\n color: var(--bs-breadcrumb-divider-color);\n content: var(--bs-breadcrumb-divider, \"/\") /* rtl: var(--bs-breadcrumb-divider, \"/\") */;\n}\n.breadcrumb-item.active {\n color: var(--bs-breadcrumb-item-active-color);\n}\n\n.pagination {\n --bs-pagination-padding-x: 0.75rem;\n --bs-pagination-padding-y: 0.375rem;\n --bs-pagination-font-size: 1rem;\n --bs-pagination-color: var(--bs-link-color);\n --bs-pagination-bg: var(--bs-body-bg);\n --bs-pagination-border-width: var(--bs-border-width);\n --bs-pagination-border-color: var(--bs-border-color);\n --bs-pagination-border-radius: var(--bs-border-radius);\n --bs-pagination-hover-color: var(--bs-link-hover-color);\n --bs-pagination-hover-bg: var(--bs-tertiary-bg);\n --bs-pagination-hover-border-color: var(--bs-border-color);\n --bs-pagination-focus-color: var(--bs-link-hover-color);\n --bs-pagination-focus-bg: var(--bs-secondary-bg);\n --bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n --bs-pagination-active-color: #fff;\n --bs-pagination-active-bg: #0d6efd;\n --bs-pagination-active-border-color: #0d6efd;\n --bs-pagination-disabled-color: var(--bs-secondary-color);\n --bs-pagination-disabled-bg: var(--bs-secondary-bg);\n --bs-pagination-disabled-border-color: var(--bs-border-color);\n display: flex;\n padding-left: 0;\n list-style: none;\n}\n\n.page-link {\n position: relative;\n display: block;\n padding: var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);\n font-size: var(--bs-pagination-font-size);\n color: var(--bs-pagination-color);\n text-decoration: none;\n background-color: var(--bs-pagination-bg);\n border: var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .page-link {\n transition: none;\n }\n}\n.page-link:hover {\n z-index: 2;\n color: var(--bs-pagination-hover-color);\n background-color: var(--bs-pagination-hover-bg);\n border-color: var(--bs-pagination-hover-border-color);\n}\n.page-link:focus {\n z-index: 3;\n color: var(--bs-pagination-focus-color);\n background-color: var(--bs-pagination-focus-bg);\n outline: 0;\n box-shadow: var(--bs-pagination-focus-box-shadow);\n}\n.page-link.active, .active > .page-link {\n z-index: 3;\n color: var(--bs-pagination-active-color);\n background-color: var(--bs-pagination-active-bg);\n border-color: var(--bs-pagination-active-border-color);\n}\n.page-link.disabled, .disabled > .page-link {\n color: var(--bs-pagination-disabled-color);\n pointer-events: none;\n background-color: var(--bs-pagination-disabled-bg);\n border-color: var(--bs-pagination-disabled-border-color);\n}\n\n.page-item:not(:first-child) .page-link {\n margin-left: calc(var(--bs-border-width) * -1);\n}\n.page-item:first-child .page-link {\n border-top-left-radius: var(--bs-pagination-border-radius);\n border-bottom-left-radius: var(--bs-pagination-border-radius);\n}\n.page-item:last-child .page-link {\n border-top-right-radius: var(--bs-pagination-border-radius);\n border-bottom-right-radius: var(--bs-pagination-border-radius);\n}\n\n.pagination-lg {\n --bs-pagination-padding-x: 1.5rem;\n --bs-pagination-padding-y: 0.75rem;\n --bs-pagination-font-size: 1.25rem;\n --bs-pagination-border-radius: var(--bs-border-radius-lg);\n}\n\n.pagination-sm {\n --bs-pagination-padding-x: 0.5rem;\n --bs-pagination-padding-y: 0.25rem;\n --bs-pagination-font-size: 0.875rem;\n --bs-pagination-border-radius: var(--bs-border-radius-sm);\n}\n\n.badge {\n --bs-badge-padding-x: 0.65em;\n --bs-badge-padding-y: 0.35em;\n --bs-badge-font-size: 0.75em;\n --bs-badge-font-weight: 700;\n --bs-badge-color: #fff;\n --bs-badge-border-radius: var(--bs-border-radius);\n display: inline-block;\n padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x);\n font-size: var(--bs-badge-font-size);\n font-weight: var(--bs-badge-font-weight);\n line-height: 1;\n color: var(--bs-badge-color);\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: var(--bs-badge-border-radius);\n}\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.alert {\n --bs-alert-bg: transparent;\n --bs-alert-padding-x: 1rem;\n --bs-alert-padding-y: 1rem;\n --bs-alert-margin-bottom: 1rem;\n --bs-alert-color: inherit;\n --bs-alert-border-color: transparent;\n --bs-alert-border: var(--bs-border-width) solid var(--bs-alert-border-color);\n --bs-alert-border-radius: var(--bs-border-radius);\n --bs-alert-link-color: inherit;\n position: relative;\n padding: var(--bs-alert-padding-y) var(--bs-alert-padding-x);\n margin-bottom: var(--bs-alert-margin-bottom);\n color: var(--bs-alert-color);\n background-color: var(--bs-alert-bg);\n border: var(--bs-alert-border);\n border-radius: var(--bs-alert-border-radius);\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n color: var(--bs-alert-link-color);\n}\n\n.alert-dismissible {\n padding-right: 3rem;\n}\n.alert-dismissible .btn-close {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n padding: 1.25rem 1rem;\n}\n\n.alert-primary {\n --bs-alert-color: var(--bs-primary-text-emphasis);\n --bs-alert-bg: var(--bs-primary-bg-subtle);\n --bs-alert-border-color: var(--bs-primary-border-subtle);\n --bs-alert-link-color: var(--bs-primary-text-emphasis);\n}\n\n.alert-secondary {\n --bs-alert-color: var(--bs-secondary-text-emphasis);\n --bs-alert-bg: var(--bs-secondary-bg-subtle);\n --bs-alert-border-color: var(--bs-secondary-border-subtle);\n --bs-alert-link-color: var(--bs-secondary-text-emphasis);\n}\n\n.alert-success {\n --bs-alert-color: var(--bs-success-text-emphasis);\n --bs-alert-bg: var(--bs-success-bg-subtle);\n --bs-alert-border-color: var(--bs-success-border-subtle);\n --bs-alert-link-color: var(--bs-success-text-emphasis);\n}\n\n.alert-info {\n --bs-alert-color: var(--bs-info-text-emphasis);\n --bs-alert-bg: var(--bs-info-bg-subtle);\n --bs-alert-border-color: var(--bs-info-border-subtle);\n --bs-alert-link-color: var(--bs-info-text-emphasis);\n}\n\n.alert-warning {\n --bs-alert-color: var(--bs-warning-text-emphasis);\n --bs-alert-bg: var(--bs-warning-bg-subtle);\n --bs-alert-border-color: var(--bs-warning-border-subtle);\n --bs-alert-link-color: var(--bs-warning-text-emphasis);\n}\n\n.alert-danger {\n --bs-alert-color: var(--bs-danger-text-emphasis);\n --bs-alert-bg: var(--bs-danger-bg-subtle);\n --bs-alert-border-color: var(--bs-danger-border-subtle);\n --bs-alert-link-color: var(--bs-danger-text-emphasis);\n}\n\n.alert-light {\n --bs-alert-color: var(--bs-light-text-emphasis);\n --bs-alert-bg: var(--bs-light-bg-subtle);\n --bs-alert-border-color: var(--bs-light-border-subtle);\n --bs-alert-link-color: var(--bs-light-text-emphasis);\n}\n\n.alert-dark {\n --bs-alert-color: var(--bs-dark-text-emphasis);\n --bs-alert-bg: var(--bs-dark-bg-subtle);\n --bs-alert-border-color: var(--bs-dark-border-subtle);\n --bs-alert-link-color: var(--bs-dark-text-emphasis);\n}\n\n@keyframes progress-bar-stripes {\n 0% {\n background-position-x: 1rem;\n }\n}\n.progress,\n.progress-stacked {\n --bs-progress-height: 1rem;\n --bs-progress-font-size: 0.75rem;\n --bs-progress-bg: var(--bs-secondary-bg);\n --bs-progress-border-radius: var(--bs-border-radius);\n --bs-progress-box-shadow: var(--bs-box-shadow-inset);\n --bs-progress-bar-color: #fff;\n --bs-progress-bar-bg: #0d6efd;\n --bs-progress-bar-transition: width 0.6s ease;\n display: flex;\n height: var(--bs-progress-height);\n overflow: hidden;\n font-size: var(--bs-progress-font-size);\n background-color: var(--bs-progress-bg);\n border-radius: var(--bs-progress-border-radius);\n}\n\n.progress-bar {\n display: flex;\n flex-direction: column;\n justify-content: center;\n overflow: hidden;\n color: var(--bs-progress-bar-color);\n text-align: center;\n white-space: nowrap;\n background-color: var(--bs-progress-bar-bg);\n transition: var(--bs-progress-bar-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: var(--bs-progress-height) var(--bs-progress-height);\n}\n\n.progress-stacked > .progress {\n overflow: visible;\n}\n\n.progress-stacked > .progress > .progress-bar {\n width: 100%;\n}\n\n.progress-bar-animated {\n animation: 1s linear infinite progress-bar-stripes;\n}\n@media (prefers-reduced-motion: reduce) {\n .progress-bar-animated {\n animation: none;\n }\n}\n\n.list-group {\n --bs-list-group-color: var(--bs-body-color);\n --bs-list-group-bg: var(--bs-body-bg);\n --bs-list-group-border-color: var(--bs-border-color);\n --bs-list-group-border-width: var(--bs-border-width);\n --bs-list-group-border-radius: var(--bs-border-radius);\n --bs-list-group-item-padding-x: 1rem;\n --bs-list-group-item-padding-y: 0.5rem;\n --bs-list-group-action-color: var(--bs-secondary-color);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-tertiary-bg);\n --bs-list-group-action-active-color: var(--bs-body-color);\n --bs-list-group-action-active-bg: var(--bs-secondary-bg);\n --bs-list-group-disabled-color: var(--bs-secondary-color);\n --bs-list-group-disabled-bg: var(--bs-body-bg);\n --bs-list-group-active-color: #fff;\n --bs-list-group-active-bg: #0d6efd;\n --bs-list-group-active-border-color: #0d6efd;\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n border-radius: var(--bs-list-group-border-radius);\n}\n\n.list-group-numbered {\n list-style-type: none;\n counter-reset: section;\n}\n.list-group-numbered > .list-group-item::before {\n content: counters(section, \".\") \". \";\n counter-increment: section;\n}\n\n.list-group-item-action {\n width: 100%;\n color: var(--bs-list-group-action-color);\n text-align: inherit;\n}\n.list-group-item-action:hover, .list-group-item-action:focus {\n z-index: 1;\n color: var(--bs-list-group-action-hover-color);\n text-decoration: none;\n background-color: var(--bs-list-group-action-hover-bg);\n}\n.list-group-item-action:active {\n color: var(--bs-list-group-action-active-color);\n background-color: var(--bs-list-group-action-active-bg);\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);\n color: var(--bs-list-group-color);\n text-decoration: none;\n background-color: var(--bs-list-group-bg);\n border: var(--bs-list-group-border-width) solid var(--bs-list-group-border-color);\n}\n.list-group-item:first-child {\n border-top-left-radius: inherit;\n border-top-right-radius: inherit;\n}\n.list-group-item:last-child {\n border-bottom-right-radius: inherit;\n border-bottom-left-radius: inherit;\n}\n.list-group-item.disabled, .list-group-item:disabled {\n color: var(--bs-list-group-disabled-color);\n pointer-events: none;\n background-color: var(--bs-list-group-disabled-bg);\n}\n.list-group-item.active {\n z-index: 2;\n color: var(--bs-list-group-active-color);\n background-color: var(--bs-list-group-active-bg);\n border-color: var(--bs-list-group-active-border-color);\n}\n.list-group-item + .list-group-item {\n border-top-width: 0;\n}\n.list-group-item + .list-group-item.active {\n margin-top: calc(-1 * var(--bs-list-group-border-width));\n border-top-width: var(--bs-list-group-border-width);\n}\n\n.list-group-horizontal {\n flex-direction: row;\n}\n.list-group-horizontal > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n}\n.list-group-horizontal > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n}\n.list-group-horizontal > .list-group-item.active {\n margin-top: 0;\n}\n.list-group-horizontal > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n}\n.list-group-horizontal > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n}\n\n@media (min-width: 576px) {\n .list-group-horizontal-sm {\n flex-direction: row;\n }\n .list-group-horizontal-sm > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n }\n}\n@media (min-width: 768px) {\n .list-group-horizontal-md {\n flex-direction: row;\n }\n .list-group-horizontal-md > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n }\n}\n@media (min-width: 992px) {\n .list-group-horizontal-lg {\n flex-direction: row;\n }\n .list-group-horizontal-lg > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n }\n}\n@media (min-width: 1200px) {\n .list-group-horizontal-xl {\n flex-direction: row;\n }\n .list-group-horizontal-xl > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n }\n}\n@media (min-width: 1400px) {\n .list-group-horizontal-xxl {\n flex-direction: row;\n }\n .list-group-horizontal-xxl > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xxl > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xxl > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xxl > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n }\n .list-group-horizontal-xxl > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n }\n}\n.list-group-flush {\n border-radius: 0;\n}\n.list-group-flush > .list-group-item {\n border-width: 0 0 var(--bs-list-group-border-width);\n}\n.list-group-flush > .list-group-item:last-child {\n border-bottom-width: 0;\n}\n\n.list-group-item-primary {\n --bs-list-group-color: var(--bs-primary-text-emphasis);\n --bs-list-group-bg: var(--bs-primary-bg-subtle);\n --bs-list-group-border-color: var(--bs-primary-border-subtle);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-primary-border-subtle);\n --bs-list-group-action-active-color: var(--bs-emphasis-color);\n --bs-list-group-action-active-bg: var(--bs-primary-border-subtle);\n --bs-list-group-active-color: var(--bs-primary-bg-subtle);\n --bs-list-group-active-bg: var(--bs-primary-text-emphasis);\n --bs-list-group-active-border-color: var(--bs-primary-text-emphasis);\n}\n\n.list-group-item-secondary {\n --bs-list-group-color: var(--bs-secondary-text-emphasis);\n --bs-list-group-bg: var(--bs-secondary-bg-subtle);\n --bs-list-group-border-color: var(--bs-secondary-border-subtle);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-secondary-border-subtle);\n --bs-list-group-action-active-color: var(--bs-emphasis-color);\n --bs-list-group-action-active-bg: var(--bs-secondary-border-subtle);\n --bs-list-group-active-color: var(--bs-secondary-bg-subtle);\n --bs-list-group-active-bg: var(--bs-secondary-text-emphasis);\n --bs-list-group-active-border-color: var(--bs-secondary-text-emphasis);\n}\n\n.list-group-item-success {\n --bs-list-group-color: var(--bs-success-text-emphasis);\n --bs-list-group-bg: var(--bs-success-bg-subtle);\n --bs-list-group-border-color: var(--bs-success-border-subtle);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-success-border-subtle);\n --bs-list-group-action-active-color: var(--bs-emphasis-color);\n --bs-list-group-action-active-bg: var(--bs-success-border-subtle);\n --bs-list-group-active-color: var(--bs-success-bg-subtle);\n --bs-list-group-active-bg: var(--bs-success-text-emphasis);\n --bs-list-group-active-border-color: var(--bs-success-text-emphasis);\n}\n\n.list-group-item-info {\n --bs-list-group-color: var(--bs-info-text-emphasis);\n --bs-list-group-bg: var(--bs-info-bg-subtle);\n --bs-list-group-border-color: var(--bs-info-border-subtle);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-info-border-subtle);\n --bs-list-group-action-active-color: var(--bs-emphasis-color);\n --bs-list-group-action-active-bg: var(--bs-info-border-subtle);\n --bs-list-group-active-color: var(--bs-info-bg-subtle);\n --bs-list-group-active-bg: var(--bs-info-text-emphasis);\n --bs-list-group-active-border-color: var(--bs-info-text-emphasis);\n}\n\n.list-group-item-warning {\n --bs-list-group-color: var(--bs-warning-text-emphasis);\n --bs-list-group-bg: var(--bs-warning-bg-subtle);\n --bs-list-group-border-color: var(--bs-warning-border-subtle);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-warning-border-subtle);\n --bs-list-group-action-active-color: var(--bs-emphasis-color);\n --bs-list-group-action-active-bg: var(--bs-warning-border-subtle);\n --bs-list-group-active-color: var(--bs-warning-bg-subtle);\n --bs-list-group-active-bg: var(--bs-warning-text-emphasis);\n --bs-list-group-active-border-color: var(--bs-warning-text-emphasis);\n}\n\n.list-group-item-danger {\n --bs-list-group-color: var(--bs-danger-text-emphasis);\n --bs-list-group-bg: var(--bs-danger-bg-subtle);\n --bs-list-group-border-color: var(--bs-danger-border-subtle);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-danger-border-subtle);\n --bs-list-group-action-active-color: var(--bs-emphasis-color);\n --bs-list-group-action-active-bg: var(--bs-danger-border-subtle);\n --bs-list-group-active-color: var(--bs-danger-bg-subtle);\n --bs-list-group-active-bg: var(--bs-danger-text-emphasis);\n --bs-list-group-active-border-color: var(--bs-danger-text-emphasis);\n}\n\n.list-group-item-light {\n --bs-list-group-color: var(--bs-light-text-emphasis);\n --bs-list-group-bg: var(--bs-light-bg-subtle);\n --bs-list-group-border-color: var(--bs-light-border-subtle);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-light-border-subtle);\n --bs-list-group-action-active-color: var(--bs-emphasis-color);\n --bs-list-group-action-active-bg: var(--bs-light-border-subtle);\n --bs-list-group-active-color: var(--bs-light-bg-subtle);\n --bs-list-group-active-bg: var(--bs-light-text-emphasis);\n --bs-list-group-active-border-color: var(--bs-light-text-emphasis);\n}\n\n.list-group-item-dark {\n --bs-list-group-color: var(--bs-dark-text-emphasis);\n --bs-list-group-bg: var(--bs-dark-bg-subtle);\n --bs-list-group-border-color: var(--bs-dark-border-subtle);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-dark-border-subtle);\n --bs-list-group-action-active-color: var(--bs-emphasis-color);\n --bs-list-group-action-active-bg: var(--bs-dark-border-subtle);\n --bs-list-group-active-color: var(--bs-dark-bg-subtle);\n --bs-list-group-active-bg: var(--bs-dark-text-emphasis);\n --bs-list-group-active-border-color: var(--bs-dark-text-emphasis);\n}\n\n.btn-close {\n --bs-btn-close-color: #000;\n --bs-btn-close-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%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.414z'/%3e%3c/svg%3e\");\n --bs-btn-close-opacity: 0.5;\n --bs-btn-close-hover-opacity: 0.75;\n --bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n --bs-btn-close-focus-opacity: 1;\n --bs-btn-close-disabled-opacity: 0.25;\n --bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);\n box-sizing: content-box;\n width: 1em;\n height: 1em;\n padding: 0.25em 0.25em;\n color: var(--bs-btn-close-color);\n background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat;\n border: 0;\n border-radius: 0.375rem;\n opacity: var(--bs-btn-close-opacity);\n}\n.btn-close:hover {\n color: var(--bs-btn-close-color);\n text-decoration: none;\n opacity: var(--bs-btn-close-hover-opacity);\n}\n.btn-close:focus {\n outline: 0;\n box-shadow: var(--bs-btn-close-focus-shadow);\n opacity: var(--bs-btn-close-focus-opacity);\n}\n.btn-close:disabled, .btn-close.disabled {\n pointer-events: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n opacity: var(--bs-btn-close-disabled-opacity);\n}\n\n.btn-close-white {\n filter: var(--bs-btn-close-white-filter);\n}\n\n[data-bs-theme=dark] .btn-close {\n filter: var(--bs-btn-close-white-filter);\n}\n\n.toast {\n --bs-toast-zindex: 1090;\n --bs-toast-padding-x: 0.75rem;\n --bs-toast-padding-y: 0.5rem;\n --bs-toast-spacing: 1.5rem;\n --bs-toast-max-width: 350px;\n --bs-toast-font-size: 0.875rem;\n --bs-toast-color: ;\n --bs-toast-bg: rgba(var(--bs-body-bg-rgb), 0.85);\n --bs-toast-border-width: var(--bs-border-width);\n --bs-toast-border-color: var(--bs-border-color-translucent);\n --bs-toast-border-radius: var(--bs-border-radius);\n --bs-toast-box-shadow: var(--bs-box-shadow);\n --bs-toast-header-color: var(--bs-secondary-color);\n --bs-toast-header-bg: rgba(var(--bs-body-bg-rgb), 0.85);\n --bs-toast-header-border-color: var(--bs-border-color-translucent);\n width: var(--bs-toast-max-width);\n max-width: 100%;\n font-size: var(--bs-toast-font-size);\n color: var(--bs-toast-color);\n pointer-events: auto;\n background-color: var(--bs-toast-bg);\n background-clip: padding-box;\n border: var(--bs-toast-border-width) solid var(--bs-toast-border-color);\n box-shadow: var(--bs-toast-box-shadow);\n border-radius: var(--bs-toast-border-radius);\n}\n.toast.showing {\n opacity: 0;\n}\n.toast:not(.show) {\n display: none;\n}\n\n.toast-container {\n --bs-toast-zindex: 1090;\n position: absolute;\n z-index: var(--bs-toast-zindex);\n width: -webkit-max-content;\n width: -moz-max-content;\n width: max-content;\n max-width: 100%;\n pointer-events: none;\n}\n.toast-container > :not(:last-child) {\n margin-bottom: var(--bs-toast-spacing);\n}\n\n.toast-header {\n display: flex;\n align-items: center;\n padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x);\n color: var(--bs-toast-header-color);\n background-color: var(--bs-toast-header-bg);\n background-clip: padding-box;\n border-bottom: var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);\n border-top-left-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));\n border-top-right-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));\n}\n.toast-header .btn-close {\n margin-right: calc(-0.5 * var(--bs-toast-padding-x));\n margin-left: var(--bs-toast-padding-x);\n}\n\n.toast-body {\n padding: var(--bs-toast-padding-x);\n word-wrap: break-word;\n}\n\n.modal {\n --bs-modal-zindex: 1055;\n --bs-modal-width: 500px;\n --bs-modal-padding: 1rem;\n --bs-modal-margin: 0.5rem;\n --bs-modal-color: ;\n --bs-modal-bg: var(--bs-body-bg);\n --bs-modal-border-color: var(--bs-border-color-translucent);\n --bs-modal-border-width: var(--bs-border-width);\n --bs-modal-border-radius: var(--bs-border-radius-lg);\n --bs-modal-box-shadow: var(--bs-box-shadow-sm);\n --bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));\n --bs-modal-header-padding-x: 1rem;\n --bs-modal-header-padding-y: 1rem;\n --bs-modal-header-padding: 1rem 1rem;\n --bs-modal-header-border-color: var(--bs-border-color);\n --bs-modal-header-border-width: var(--bs-border-width);\n --bs-modal-title-line-height: 1.5;\n --bs-modal-footer-gap: 0.5rem;\n --bs-modal-footer-bg: ;\n --bs-modal-footer-border-color: var(--bs-border-color);\n --bs-modal-footer-border-width: var(--bs-border-width);\n position: fixed;\n top: 0;\n left: 0;\n z-index: var(--bs-modal-zindex);\n display: none;\n width: 100%;\n height: 100%;\n overflow-x: hidden;\n overflow-y: auto;\n outline: 0;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: var(--bs-modal-margin);\n pointer-events: none;\n}\n.modal.fade .modal-dialog {\n transition: transform 0.3s ease-out;\n transform: translate(0, -50px);\n}\n@media (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n.modal.show .modal-dialog {\n transform: none;\n}\n.modal.modal-static .modal-dialog {\n transform: scale(1.02);\n}\n\n.modal-dialog-scrollable {\n height: calc(100% - var(--bs-modal-margin) * 2);\n}\n.modal-dialog-scrollable .modal-content {\n max-height: 100%;\n overflow: hidden;\n}\n.modal-dialog-scrollable .modal-body {\n overflow-y: auto;\n}\n\n.modal-dialog-centered {\n display: flex;\n align-items: center;\n min-height: calc(100% - var(--bs-modal-margin) * 2);\n}\n\n.modal-content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n color: var(--bs-modal-color);\n pointer-events: auto;\n background-color: var(--bs-modal-bg);\n background-clip: padding-box;\n border: var(--bs-modal-border-width) solid var(--bs-modal-border-color);\n border-radius: var(--bs-modal-border-radius);\n outline: 0;\n}\n\n.modal-backdrop {\n --bs-backdrop-zindex: 1050;\n --bs-backdrop-bg: #000;\n --bs-backdrop-opacity: 0.5;\n position: fixed;\n top: 0;\n left: 0;\n z-index: var(--bs-backdrop-zindex);\n width: 100vw;\n height: 100vh;\n background-color: var(--bs-backdrop-bg);\n}\n.modal-backdrop.fade {\n opacity: 0;\n}\n.modal-backdrop.show {\n opacity: var(--bs-backdrop-opacity);\n}\n\n.modal-header {\n display: flex;\n flex-shrink: 0;\n align-items: center;\n padding: var(--bs-modal-header-padding);\n border-bottom: var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);\n border-top-left-radius: var(--bs-modal-inner-border-radius);\n border-top-right-radius: var(--bs-modal-inner-border-radius);\n}\n.modal-header .btn-close {\n padding: calc(var(--bs-modal-header-padding-y) * 0.5) calc(var(--bs-modal-header-padding-x) * 0.5);\n margin: calc(-0.5 * var(--bs-modal-header-padding-y)) calc(-0.5 * var(--bs-modal-header-padding-x)) calc(-0.5 * var(--bs-modal-header-padding-y)) auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: var(--bs-modal-title-line-height);\n}\n\n.modal-body {\n position: relative;\n flex: 1 1 auto;\n padding: var(--bs-modal-padding);\n}\n\n.modal-footer {\n display: flex;\n flex-shrink: 0;\n flex-wrap: wrap;\n align-items: center;\n justify-content: flex-end;\n padding: calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * 0.5);\n background-color: var(--bs-modal-footer-bg);\n border-top: var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);\n border-bottom-right-radius: var(--bs-modal-inner-border-radius);\n border-bottom-left-radius: var(--bs-modal-inner-border-radius);\n}\n.modal-footer > * {\n margin: calc(var(--bs-modal-footer-gap) * 0.5);\n}\n\n@media (min-width: 576px) {\n .modal {\n --bs-modal-margin: 1.75rem;\n --bs-modal-box-shadow: var(--bs-box-shadow);\n }\n .modal-dialog {\n max-width: var(--bs-modal-width);\n margin-right: auto;\n margin-left: auto;\n }\n .modal-sm {\n --bs-modal-width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg,\n .modal-xl {\n --bs-modal-width: 800px;\n }\n}\n@media (min-width: 1200px) {\n .modal-xl {\n --bs-modal-width: 1140px;\n }\n}\n.modal-fullscreen {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n}\n.modal-fullscreen .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n}\n.modal-fullscreen .modal-header,\n.modal-fullscreen .modal-footer {\n border-radius: 0;\n}\n.modal-fullscreen .modal-body {\n overflow-y: auto;\n}\n\n@media (max-width: 575.98px) {\n .modal-fullscreen-sm-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-sm-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-sm-down .modal-header,\n .modal-fullscreen-sm-down .modal-footer {\n border-radius: 0;\n }\n .modal-fullscreen-sm-down .modal-body {\n overflow-y: auto;\n }\n}\n@media (max-width: 767.98px) {\n .modal-fullscreen-md-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-md-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-md-down .modal-header,\n .modal-fullscreen-md-down .modal-footer {\n border-radius: 0;\n }\n .modal-fullscreen-md-down .modal-body {\n overflow-y: auto;\n }\n}\n@media (max-width: 991.98px) {\n .modal-fullscreen-lg-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-lg-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-lg-down .modal-header,\n .modal-fullscreen-lg-down .modal-footer {\n border-radius: 0;\n }\n .modal-fullscreen-lg-down .modal-body {\n overflow-y: auto;\n }\n}\n@media (max-width: 1199.98px) {\n .modal-fullscreen-xl-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-xl-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-xl-down .modal-header,\n .modal-fullscreen-xl-down .modal-footer {\n border-radius: 0;\n }\n .modal-fullscreen-xl-down .modal-body {\n overflow-y: auto;\n }\n}\n@media (max-width: 1399.98px) {\n .modal-fullscreen-xxl-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-xxl-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-xxl-down .modal-header,\n .modal-fullscreen-xxl-down .modal-footer {\n border-radius: 0;\n }\n .modal-fullscreen-xxl-down .modal-body {\n overflow-y: auto;\n }\n}\n.tooltip {\n --bs-tooltip-zindex: 1080;\n --bs-tooltip-max-width: 200px;\n --bs-tooltip-padding-x: 0.5rem;\n --bs-tooltip-padding-y: 0.25rem;\n --bs-tooltip-margin: ;\n --bs-tooltip-font-size: 0.875rem;\n --bs-tooltip-color: var(--bs-body-bg);\n --bs-tooltip-bg: var(--bs-emphasis-color);\n --bs-tooltip-border-radius: var(--bs-border-radius);\n --bs-tooltip-opacity: 0.9;\n --bs-tooltip-arrow-width: 0.8rem;\n --bs-tooltip-arrow-height: 0.4rem;\n z-index: var(--bs-tooltip-zindex);\n display: block;\n margin: var(--bs-tooltip-margin);\n font-family: var(--bs-font-sans-serif);\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n white-space: normal;\n word-spacing: normal;\n line-break: auto;\n font-size: var(--bs-tooltip-font-size);\n word-wrap: break-word;\n opacity: 0;\n}\n.tooltip.show {\n opacity: var(--bs-tooltip-opacity);\n}\n.tooltip .tooltip-arrow {\n display: block;\n width: var(--bs-tooltip-arrow-width);\n height: var(--bs-tooltip-arrow-height);\n}\n.tooltip .tooltip-arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow {\n bottom: calc(-1 * var(--bs-tooltip-arrow-height));\n}\n.bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before {\n top: -1px;\n border-width: var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0;\n border-top-color: var(--bs-tooltip-bg);\n}\n\n/* rtl:begin:ignore */\n.bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow {\n left: calc(-1 * var(--bs-tooltip-arrow-height));\n width: var(--bs-tooltip-arrow-height);\n height: var(--bs-tooltip-arrow-width);\n}\n.bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before {\n right: -1px;\n border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0;\n border-right-color: var(--bs-tooltip-bg);\n}\n\n/* rtl:end:ignore */\n.bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow {\n top: calc(-1 * var(--bs-tooltip-arrow-height));\n}\n.bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before {\n bottom: -1px;\n border-width: 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height);\n border-bottom-color: var(--bs-tooltip-bg);\n}\n\n/* rtl:begin:ignore */\n.bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow {\n right: calc(-1 * var(--bs-tooltip-arrow-height));\n width: var(--bs-tooltip-arrow-height);\n height: var(--bs-tooltip-arrow-width);\n}\n.bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before {\n left: -1px;\n border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height);\n border-left-color: var(--bs-tooltip-bg);\n}\n\n/* rtl:end:ignore */\n.tooltip-inner {\n max-width: var(--bs-tooltip-max-width);\n padding: var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);\n color: var(--bs-tooltip-color);\n text-align: center;\n background-color: var(--bs-tooltip-bg);\n border-radius: var(--bs-tooltip-border-radius);\n}\n\n.popover {\n --bs-popover-zindex: 1070;\n --bs-popover-max-width: 276px;\n --bs-popover-font-size: 0.875rem;\n --bs-popover-bg: var(--bs-body-bg);\n --bs-popover-border-width: var(--bs-border-width);\n --bs-popover-border-color: var(--bs-border-color-translucent);\n --bs-popover-border-radius: var(--bs-border-radius-lg);\n --bs-popover-inner-border-radius: calc(var(--bs-border-radius-lg) - var(--bs-border-width));\n --bs-popover-box-shadow: var(--bs-box-shadow);\n --bs-popover-header-padding-x: 1rem;\n --bs-popover-header-padding-y: 0.5rem;\n --bs-popover-header-font-size: 1rem;\n --bs-popover-header-color: inherit;\n --bs-popover-header-bg: var(--bs-secondary-bg);\n --bs-popover-body-padding-x: 1rem;\n --bs-popover-body-padding-y: 1rem;\n --bs-popover-body-color: var(--bs-body-color);\n --bs-popover-arrow-width: 1rem;\n --bs-popover-arrow-height: 0.5rem;\n --bs-popover-arrow-border: var(--bs-popover-border-color);\n z-index: var(--bs-popover-zindex);\n display: block;\n max-width: var(--bs-popover-max-width);\n font-family: var(--bs-font-sans-serif);\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n white-space: normal;\n word-spacing: normal;\n line-break: auto;\n font-size: var(--bs-popover-font-size);\n word-wrap: break-word;\n background-color: var(--bs-popover-bg);\n background-clip: padding-box;\n border: var(--bs-popover-border-width) solid var(--bs-popover-border-color);\n border-radius: var(--bs-popover-border-radius);\n}\n.popover .popover-arrow {\n display: block;\n width: var(--bs-popover-arrow-width);\n height: var(--bs-popover-arrow-height);\n}\n.popover .popover-arrow::before, .popover .popover-arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n border-width: 0;\n}\n\n.bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow {\n bottom: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n}\n.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 {\n border-width: var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0;\n}\n.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before {\n bottom: 0;\n border-top-color: var(--bs-popover-arrow-border);\n}\n.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after {\n bottom: var(--bs-popover-border-width);\n border-top-color: var(--bs-popover-bg);\n}\n\n/* rtl:begin:ignore */\n.bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow {\n left: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n width: var(--bs-popover-arrow-height);\n height: var(--bs-popover-arrow-width);\n}\n.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 {\n border-width: calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0;\n}\n.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before {\n left: 0;\n border-right-color: var(--bs-popover-arrow-border);\n}\n.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after {\n left: var(--bs-popover-border-width);\n border-right-color: var(--bs-popover-bg);\n}\n\n/* rtl:end:ignore */\n.bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow {\n top: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n}\n.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 {\n border-width: 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height);\n}\n.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before {\n top: 0;\n border-bottom-color: var(--bs-popover-arrow-border);\n}\n.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after {\n top: var(--bs-popover-border-width);\n border-bottom-color: var(--bs-popover-bg);\n}\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[data-popper-placement^=bottom] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: var(--bs-popover-arrow-width);\n margin-left: calc(-0.5 * var(--bs-popover-arrow-width));\n content: \"\";\n border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-header-bg);\n}\n\n/* rtl:begin:ignore */\n.bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow {\n right: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n width: var(--bs-popover-arrow-height);\n height: var(--bs-popover-arrow-width);\n}\n.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 {\n border-width: calc(var(--bs-popover-arrow-width) * 0.5) 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height);\n}\n.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before {\n right: 0;\n border-left-color: var(--bs-popover-arrow-border);\n}\n.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after {\n right: var(--bs-popover-border-width);\n border-left-color: var(--bs-popover-bg);\n}\n\n/* rtl:end:ignore */\n.popover-header {\n padding: var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);\n margin-bottom: 0;\n font-size: var(--bs-popover-header-font-size);\n color: var(--bs-popover-header-color);\n background-color: var(--bs-popover-header-bg);\n border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-border-color);\n border-top-left-radius: var(--bs-popover-inner-border-radius);\n border-top-right-radius: var(--bs-popover-inner-border-radius);\n}\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);\n color: var(--bs-popover-body-color);\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n.carousel-inner::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n -webkit-backface-visibility: hidden;\n backface-visibility: hidden;\n transition: transform 0.6s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n.carousel-item-next:not(.carousel-item-start),\n.active.carousel-item-end {\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-end),\n.active.carousel-item-start {\n transform: translateX(-100%);\n}\n\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-property: opacity;\n transform: none;\n}\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-start,\n.carousel-fade .carousel-item-prev.carousel-item-end {\n z-index: 1;\n opacity: 1;\n}\n.carousel-fade .active.carousel-item-start,\n.carousel-fade .active.carousel-item-end {\n z-index: 0;\n opacity: 0;\n transition: opacity 0s 0.6s;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-fade .active.carousel-item-start,\n .carousel-fade .active.carousel-item-end {\n transition: none;\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 15%;\n padding: 0;\n color: #fff;\n text-align: center;\n background: none;\n border: 0;\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-control-prev,\n .carousel-control-next {\n transition: none;\n }\n}\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: 0.9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n background-repeat: no-repeat;\n background-position: 50%;\n background-size: 100% 100%;\n}\n\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e\") /*rtl:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\")*/;\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\") /*rtl:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e\")*/;\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 2;\n display: flex;\n justify-content: center;\n padding: 0;\n margin-right: 15%;\n margin-bottom: 1rem;\n margin-left: 15%;\n}\n.carousel-indicators [data-bs-target] {\n box-sizing: content-box;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n padding: 0;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #fff;\n background-clip: padding-box;\n border: 0;\n border-top: 10px solid transparent;\n border-bottom: 10px solid transparent;\n opacity: 0.5;\n transition: opacity 0.6s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-indicators [data-bs-target] {\n transition: none;\n }\n}\n.carousel-indicators .active {\n opacity: 1;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 1.25rem;\n left: 15%;\n padding-top: 1.25rem;\n padding-bottom: 1.25rem;\n color: #fff;\n text-align: center;\n}\n\n.carousel-dark .carousel-control-prev-icon,\n.carousel-dark .carousel-control-next-icon {\n filter: invert(1) grayscale(100);\n}\n.carousel-dark .carousel-indicators [data-bs-target] {\n background-color: #000;\n}\n.carousel-dark .carousel-caption {\n color: #000;\n}\n\n[data-bs-theme=dark] .carousel .carousel-control-prev-icon,\n[data-bs-theme=dark] .carousel .carousel-control-next-icon, [data-bs-theme=dark].carousel .carousel-control-prev-icon,\n[data-bs-theme=dark].carousel .carousel-control-next-icon {\n filter: invert(1) grayscale(100);\n}\n[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target], [data-bs-theme=dark].carousel .carousel-indicators [data-bs-target] {\n background-color: #000;\n}\n[data-bs-theme=dark] .carousel .carousel-caption, [data-bs-theme=dark].carousel .carousel-caption {\n color: #000;\n}\n\n.spinner-grow,\n.spinner-border {\n display: inline-block;\n width: var(--bs-spinner-width);\n height: var(--bs-spinner-height);\n vertical-align: var(--bs-spinner-vertical-align);\n border-radius: 50%;\n animation: var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name);\n}\n\n@keyframes spinner-border {\n to {\n transform: rotate(360deg) /* rtl:ignore */;\n }\n}\n.spinner-border {\n --bs-spinner-width: 2rem;\n --bs-spinner-height: 2rem;\n --bs-spinner-vertical-align: -0.125em;\n --bs-spinner-border-width: 0.25em;\n --bs-spinner-animation-speed: 0.75s;\n --bs-spinner-animation-name: spinner-border;\n border: var(--bs-spinner-border-width) solid currentcolor;\n border-right-color: transparent;\n}\n\n.spinner-border-sm {\n --bs-spinner-width: 1rem;\n --bs-spinner-height: 1rem;\n --bs-spinner-border-width: 0.2em;\n}\n\n@keyframes spinner-grow {\n 0% {\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n transform: none;\n }\n}\n.spinner-grow {\n --bs-spinner-width: 2rem;\n --bs-spinner-height: 2rem;\n --bs-spinner-vertical-align: -0.125em;\n --bs-spinner-animation-speed: 0.75s;\n --bs-spinner-animation-name: spinner-grow;\n background-color: currentcolor;\n opacity: 0;\n}\n\n.spinner-grow-sm {\n --bs-spinner-width: 1rem;\n --bs-spinner-height: 1rem;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .spinner-border,\n .spinner-grow {\n --bs-spinner-animation-speed: 1.5s;\n }\n}\n.offcanvas, .offcanvas-xxl, .offcanvas-xl, .offcanvas-lg, .offcanvas-md, .offcanvas-sm {\n --bs-offcanvas-zindex: 1045;\n --bs-offcanvas-width: 400px;\n --bs-offcanvas-height: 30vh;\n --bs-offcanvas-padding-x: 1rem;\n --bs-offcanvas-padding-y: 1rem;\n --bs-offcanvas-color: var(--bs-body-color);\n --bs-offcanvas-bg: var(--bs-body-bg);\n --bs-offcanvas-border-width: var(--bs-border-width);\n --bs-offcanvas-border-color: var(--bs-border-color-translucent);\n --bs-offcanvas-box-shadow: var(--bs-box-shadow-sm);\n --bs-offcanvas-transition: transform 0.3s ease-in-out;\n --bs-offcanvas-title-line-height: 1.5;\n}\n\n@media (max-width: 575.98px) {\n .offcanvas-sm {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n }\n}\n@media (max-width: 575.98px) and (prefers-reduced-motion: reduce) {\n .offcanvas-sm {\n transition: none;\n }\n}\n@media (max-width: 575.98px) {\n .offcanvas-sm.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n }\n .offcanvas-sm.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n }\n .offcanvas-sm.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n }\n .offcanvas-sm.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n }\n .offcanvas-sm.showing, .offcanvas-sm.show:not(.hiding) {\n transform: none;\n }\n .offcanvas-sm.showing, .offcanvas-sm.hiding, .offcanvas-sm.show {\n visibility: visible;\n }\n}\n@media (min-width: 576px) {\n .offcanvas-sm {\n --bs-offcanvas-height: auto;\n --bs-offcanvas-border-width: 0;\n background-color: transparent !important;\n }\n .offcanvas-sm .offcanvas-header {\n display: none;\n }\n .offcanvas-sm .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n background-color: transparent !important;\n }\n}\n\n@media (max-width: 767.98px) {\n .offcanvas-md {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n }\n}\n@media (max-width: 767.98px) and (prefers-reduced-motion: reduce) {\n .offcanvas-md {\n transition: none;\n }\n}\n@media (max-width: 767.98px) {\n .offcanvas-md.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n }\n .offcanvas-md.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n }\n .offcanvas-md.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n }\n .offcanvas-md.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n }\n .offcanvas-md.showing, .offcanvas-md.show:not(.hiding) {\n transform: none;\n }\n .offcanvas-md.showing, .offcanvas-md.hiding, .offcanvas-md.show {\n visibility: visible;\n }\n}\n@media (min-width: 768px) {\n .offcanvas-md {\n --bs-offcanvas-height: auto;\n --bs-offcanvas-border-width: 0;\n background-color: transparent !important;\n }\n .offcanvas-md .offcanvas-header {\n display: none;\n }\n .offcanvas-md .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n background-color: transparent !important;\n }\n}\n\n@media (max-width: 991.98px) {\n .offcanvas-lg {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n }\n}\n@media (max-width: 991.98px) and (prefers-reduced-motion: reduce) {\n .offcanvas-lg {\n transition: none;\n }\n}\n@media (max-width: 991.98px) {\n .offcanvas-lg.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n }\n .offcanvas-lg.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n }\n .offcanvas-lg.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n }\n .offcanvas-lg.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n }\n .offcanvas-lg.showing, .offcanvas-lg.show:not(.hiding) {\n transform: none;\n }\n .offcanvas-lg.showing, .offcanvas-lg.hiding, .offcanvas-lg.show {\n visibility: visible;\n }\n}\n@media (min-width: 992px) {\n .offcanvas-lg {\n --bs-offcanvas-height: auto;\n --bs-offcanvas-border-width: 0;\n background-color: transparent !important;\n }\n .offcanvas-lg .offcanvas-header {\n display: none;\n }\n .offcanvas-lg .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n background-color: transparent !important;\n }\n}\n\n@media (max-width: 1199.98px) {\n .offcanvas-xl {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n }\n}\n@media (max-width: 1199.98px) and (prefers-reduced-motion: reduce) {\n .offcanvas-xl {\n transition: none;\n }\n}\n@media (max-width: 1199.98px) {\n .offcanvas-xl.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n }\n .offcanvas-xl.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n }\n .offcanvas-xl.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n }\n .offcanvas-xl.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n }\n .offcanvas-xl.showing, .offcanvas-xl.show:not(.hiding) {\n transform: none;\n }\n .offcanvas-xl.showing, .offcanvas-xl.hiding, .offcanvas-xl.show {\n visibility: visible;\n }\n}\n@media (min-width: 1200px) {\n .offcanvas-xl {\n --bs-offcanvas-height: auto;\n --bs-offcanvas-border-width: 0;\n background-color: transparent !important;\n }\n .offcanvas-xl .offcanvas-header {\n display: none;\n }\n .offcanvas-xl .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n background-color: transparent !important;\n }\n}\n\n@media (max-width: 1399.98px) {\n .offcanvas-xxl {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n }\n}\n@media (max-width: 1399.98px) and (prefers-reduced-motion: reduce) {\n .offcanvas-xxl {\n transition: none;\n }\n}\n@media (max-width: 1399.98px) {\n .offcanvas-xxl.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n }\n .offcanvas-xxl.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n }\n .offcanvas-xxl.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n }\n .offcanvas-xxl.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n }\n .offcanvas-xxl.showing, .offcanvas-xxl.show:not(.hiding) {\n transform: none;\n }\n .offcanvas-xxl.showing, .offcanvas-xxl.hiding, .offcanvas-xxl.show {\n visibility: visible;\n }\n}\n@media (min-width: 1400px) {\n .offcanvas-xxl {\n --bs-offcanvas-height: auto;\n --bs-offcanvas-border-width: 0;\n background-color: transparent !important;\n }\n .offcanvas-xxl .offcanvas-header {\n display: none;\n }\n .offcanvas-xxl .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n background-color: transparent !important;\n }\n}\n\n.offcanvas {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n .offcanvas {\n transition: none;\n }\n}\n.offcanvas.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n}\n.offcanvas.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n}\n.offcanvas.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n}\n.offcanvas.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n}\n.offcanvas.showing, .offcanvas.show:not(.hiding) {\n transform: none;\n}\n.offcanvas.showing, .offcanvas.hiding, .offcanvas.show {\n visibility: visible;\n}\n\n.offcanvas-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1040;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n.offcanvas-backdrop.fade {\n opacity: 0;\n}\n.offcanvas-backdrop.show {\n opacity: 0.5;\n}\n\n.offcanvas-header {\n display: flex;\n align-items: center;\n padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);\n}\n.offcanvas-header .btn-close {\n padding: calc(var(--bs-offcanvas-padding-y) * 0.5) calc(var(--bs-offcanvas-padding-x) * 0.5);\n margin: calc(-0.5 * var(--bs-offcanvas-padding-y)) calc(-0.5 * var(--bs-offcanvas-padding-x)) calc(-0.5 * var(--bs-offcanvas-padding-y)) auto;\n}\n\n.offcanvas-title {\n margin-bottom: 0;\n line-height: var(--bs-offcanvas-title-line-height);\n}\n\n.offcanvas-body {\n flex-grow: 1;\n padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);\n overflow-y: auto;\n}\n\n.placeholder {\n display: inline-block;\n min-height: 1em;\n vertical-align: middle;\n cursor: wait;\n background-color: currentcolor;\n opacity: 0.5;\n}\n.placeholder.btn::before {\n display: inline-block;\n content: \"\";\n}\n\n.placeholder-xs {\n min-height: 0.6em;\n}\n\n.placeholder-sm {\n min-height: 0.8em;\n}\n\n.placeholder-lg {\n min-height: 1.2em;\n}\n\n.placeholder-glow .placeholder {\n animation: placeholder-glow 2s ease-in-out infinite;\n}\n\n@keyframes placeholder-glow {\n 50% {\n opacity: 0.2;\n }\n}\n.placeholder-wave {\n -webkit-mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);\n mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);\n -webkit-mask-size: 200% 100%;\n mask-size: 200% 100%;\n animation: placeholder-wave 2s linear infinite;\n}\n\n@keyframes placeholder-wave {\n 100% {\n -webkit-mask-position: -200% 0%;\n mask-position: -200% 0%;\n }\n}\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.text-bg-primary {\n color: #fff !important;\n background-color: RGBA(var(--bs-primary-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-secondary {\n color: #fff !important;\n background-color: RGBA(var(--bs-secondary-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-success {\n color: #fff !important;\n background-color: RGBA(var(--bs-success-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-info {\n color: #000 !important;\n background-color: RGBA(var(--bs-info-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-warning {\n color: #000 !important;\n background-color: RGBA(var(--bs-warning-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-danger {\n color: #fff !important;\n background-color: RGBA(var(--bs-danger-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-light {\n color: #000 !important;\n background-color: RGBA(var(--bs-light-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-dark {\n color: #fff !important;\n background-color: RGBA(var(--bs-dark-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.link-primary {\n color: RGBA(var(--bs-primary-rgb), var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-primary:hover, .link-primary:focus {\n color: RGBA(10, 88, 202, var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(10, 88, 202, var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(10, 88, 202, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-secondary {\n color: RGBA(var(--bs-secondary-rgb), var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-secondary:hover, .link-secondary:focus {\n color: RGBA(86, 94, 100, var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(86, 94, 100, var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(86, 94, 100, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-success {\n color: RGBA(var(--bs-success-rgb), var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-success:hover, .link-success:focus {\n color: RGBA(20, 108, 67, var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(20, 108, 67, var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(20, 108, 67, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-info {\n color: RGBA(var(--bs-info-rgb), var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-info:hover, .link-info:focus {\n color: RGBA(61, 213, 243, var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(61, 213, 243, var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(61, 213, 243, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-warning {\n color: RGBA(var(--bs-warning-rgb), var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-warning:hover, .link-warning:focus {\n color: RGBA(255, 205, 57, var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(255, 205, 57, var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(255, 205, 57, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-danger {\n color: RGBA(var(--bs-danger-rgb), var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-danger:hover, .link-danger:focus {\n color: RGBA(176, 42, 55, var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(176, 42, 55, var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(176, 42, 55, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-light {\n color: RGBA(var(--bs-light-rgb), var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-light:hover, .link-light:focus {\n color: RGBA(249, 250, 251, var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(249, 250, 251, var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(249, 250, 251, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-dark {\n color: RGBA(var(--bs-dark-rgb), var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-dark:hover, .link-dark:focus {\n color: RGBA(26, 30, 33, var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(26, 30, 33, var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(26, 30, 33, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-body-emphasis {\n color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 1)) !important;\n -webkit-text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-body-emphasis:hover, .link-body-emphasis:focus {\n color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 0.75)) !important;\n -webkit-text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important;\n text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important;\n}\n\n.focus-ring:focus {\n outline: 0;\n box-shadow: var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color);\n}\n\n.icon-link {\n display: inline-flex;\n gap: 0.375rem;\n align-items: center;\n -webkit-text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5));\n text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5));\n text-underline-offset: 0.25em;\n -webkit-backface-visibility: hidden;\n backface-visibility: hidden;\n}\n.icon-link > .bi {\n flex-shrink: 0;\n width: 1em;\n height: 1em;\n fill: currentcolor;\n transition: 0.2s ease-in-out transform;\n}\n@media (prefers-reduced-motion: reduce) {\n .icon-link > .bi {\n transition: none;\n }\n}\n\n.icon-link-hover:hover > .bi, .icon-link-hover:focus-visible > .bi {\n transform: var(--bs-icon-link-transform, translate3d(0.25em, 0, 0));\n}\n\n.ratio {\n position: relative;\n width: 100%;\n}\n.ratio::before {\n display: block;\n padding-top: var(--bs-aspect-ratio);\n content: \"\";\n}\n.ratio > * {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n}\n\n.ratio-1x1 {\n --bs-aspect-ratio: 100%;\n}\n\n.ratio-4x3 {\n --bs-aspect-ratio: 75%;\n}\n\n.ratio-16x9 {\n --bs-aspect-ratio: 56.25%;\n}\n\n.ratio-21x9 {\n --bs-aspect-ratio: 42.8571428571%;\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n.sticky-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n}\n\n.sticky-bottom {\n position: -webkit-sticky;\n position: sticky;\n bottom: 0;\n z-index: 1020;\n}\n\n@media (min-width: 576px) {\n .sticky-sm-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n .sticky-sm-bottom {\n position: -webkit-sticky;\n position: sticky;\n bottom: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 768px) {\n .sticky-md-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n .sticky-md-bottom {\n position: -webkit-sticky;\n position: sticky;\n bottom: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 992px) {\n .sticky-lg-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n .sticky-lg-bottom {\n position: -webkit-sticky;\n position: sticky;\n bottom: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 1200px) {\n .sticky-xl-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n .sticky-xl-bottom {\n position: -webkit-sticky;\n position: sticky;\n bottom: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 1400px) {\n .sticky-xxl-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n .sticky-xxl-bottom {\n position: -webkit-sticky;\n position: sticky;\n bottom: 0;\n z-index: 1020;\n }\n}\n.hstack {\n display: flex;\n flex-direction: row;\n align-items: center;\n align-self: stretch;\n}\n\n.vstack {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n align-self: stretch;\n}\n\n.visually-hidden,\n.visually-hidden-focusable:not(:focus):not(:focus-within) {\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important;\n overflow: hidden !important;\n clip: rect(0, 0, 0, 0) !important;\n white-space: nowrap !important;\n border: 0 !important;\n}\n.visually-hidden:not(caption),\n.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) {\n position: absolute !important;\n}\n\n.stretched-link::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n content: \"\";\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.vr {\n display: inline-block;\n align-self: stretch;\n width: var(--bs-border-width);\n min-height: 1em;\n background-color: currentcolor;\n opacity: 0.25;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.float-start {\n float: left !important;\n}\n\n.float-end {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n.object-fit-contain {\n -o-object-fit: contain !important;\n object-fit: contain !important;\n}\n\n.object-fit-cover {\n -o-object-fit: cover !important;\n object-fit: cover !important;\n}\n\n.object-fit-fill {\n -o-object-fit: fill !important;\n object-fit: fill !important;\n}\n\n.object-fit-scale {\n -o-object-fit: scale-down !important;\n object-fit: scale-down !important;\n}\n\n.object-fit-none {\n -o-object-fit: none !important;\n object-fit: none !important;\n}\n\n.opacity-0 {\n opacity: 0 !important;\n}\n\n.opacity-25 {\n opacity: 0.25 !important;\n}\n\n.opacity-50 {\n opacity: 0.5 !important;\n}\n\n.opacity-75 {\n opacity: 0.75 !important;\n}\n\n.opacity-100 {\n opacity: 1 !important;\n}\n\n.overflow-auto {\n overflow: auto !important;\n}\n\n.overflow-hidden {\n overflow: hidden !important;\n}\n\n.overflow-visible {\n overflow: visible !important;\n}\n\n.overflow-scroll {\n overflow: scroll !important;\n}\n\n.overflow-x-auto {\n overflow-x: auto !important;\n}\n\n.overflow-x-hidden {\n overflow-x: hidden !important;\n}\n\n.overflow-x-visible {\n overflow-x: visible !important;\n}\n\n.overflow-x-scroll {\n overflow-x: scroll !important;\n}\n\n.overflow-y-auto {\n overflow-y: auto !important;\n}\n\n.overflow-y-hidden {\n overflow-y: hidden !important;\n}\n\n.overflow-y-visible {\n overflow-y: visible !important;\n}\n\n.overflow-y-scroll {\n overflow-y: scroll !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-grid {\n display: grid !important;\n}\n\n.d-inline-grid {\n display: inline-grid !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n.d-none {\n display: none !important;\n}\n\n.shadow {\n box-shadow: var(--bs-box-shadow) !important;\n}\n\n.shadow-sm {\n box-shadow: var(--bs-box-shadow-sm) !important;\n}\n\n.shadow-lg {\n box-shadow: var(--bs-box-shadow-lg) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.focus-ring-primary {\n --bs-focus-ring-color: rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-secondary {\n --bs-focus-ring-color: rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-success {\n --bs-focus-ring-color: rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-info {\n --bs-focus-ring-color: rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-warning {\n --bs-focus-ring-color: rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-danger {\n --bs-focus-ring-color: rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-light {\n --bs-focus-ring-color: rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-dark {\n --bs-focus-ring-color: rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity));\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: -webkit-sticky !important;\n position: sticky !important;\n}\n\n.top-0 {\n top: 0 !important;\n}\n\n.top-50 {\n top: 50% !important;\n}\n\n.top-100 {\n top: 100% !important;\n}\n\n.bottom-0 {\n bottom: 0 !important;\n}\n\n.bottom-50 {\n bottom: 50% !important;\n}\n\n.bottom-100 {\n bottom: 100% !important;\n}\n\n.start-0 {\n left: 0 !important;\n}\n\n.start-50 {\n left: 50% !important;\n}\n\n.start-100 {\n left: 100% !important;\n}\n\n.end-0 {\n right: 0 !important;\n}\n\n.end-50 {\n right: 50% !important;\n}\n\n.end-100 {\n right: 100% !important;\n}\n\n.translate-middle {\n transform: translate(-50%, -50%) !important;\n}\n\n.translate-middle-x {\n transform: translateX(-50%) !important;\n}\n\n.translate-middle-y {\n transform: translateY(-50%) !important;\n}\n\n.border {\n border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top {\n border-top: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-end {\n border-right: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-end-0 {\n border-right: 0 !important;\n}\n\n.border-bottom {\n border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-start {\n border-left: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-start-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-primary-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-secondary {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-secondary-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-success {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-success-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-info {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-info-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-warning {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-warning-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-danger {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-light {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-dark {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-dark-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-black {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-black-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-white {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-white-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-primary-subtle {\n border-color: var(--bs-primary-border-subtle) !important;\n}\n\n.border-secondary-subtle {\n border-color: var(--bs-secondary-border-subtle) !important;\n}\n\n.border-success-subtle {\n border-color: var(--bs-success-border-subtle) !important;\n}\n\n.border-info-subtle {\n border-color: var(--bs-info-border-subtle) !important;\n}\n\n.border-warning-subtle {\n border-color: var(--bs-warning-border-subtle) !important;\n}\n\n.border-danger-subtle {\n border-color: var(--bs-danger-border-subtle) !important;\n}\n\n.border-light-subtle {\n border-color: var(--bs-light-border-subtle) !important;\n}\n\n.border-dark-subtle {\n border-color: var(--bs-dark-border-subtle) !important;\n}\n\n.border-1 {\n border-width: 1px !important;\n}\n\n.border-2 {\n border-width: 2px !important;\n}\n\n.border-3 {\n border-width: 3px !important;\n}\n\n.border-4 {\n border-width: 4px !important;\n}\n\n.border-5 {\n border-width: 5px !important;\n}\n\n.border-opacity-10 {\n --bs-border-opacity: 0.1;\n}\n\n.border-opacity-25 {\n --bs-border-opacity: 0.25;\n}\n\n.border-opacity-50 {\n --bs-border-opacity: 0.5;\n}\n\n.border-opacity-75 {\n --bs-border-opacity: 0.75;\n}\n\n.border-opacity-100 {\n --bs-border-opacity: 1;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.vw-100 {\n width: 100vw !important;\n}\n\n.min-vw-100 {\n min-width: 100vw !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.vh-100 {\n height: 100vh !important;\n}\n\n.min-vh-100 {\n min-height: 100vh !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.justify-content-evenly {\n justify-content: space-evenly !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n.order-first {\n order: -1 !important;\n}\n\n.order-0 {\n order: 0 !important;\n}\n\n.order-1 {\n order: 1 !important;\n}\n\n.order-2 {\n order: 2 !important;\n}\n\n.order-3 {\n order: 3 !important;\n}\n\n.order-4 {\n order: 4 !important;\n}\n\n.order-5 {\n order: 5 !important;\n}\n\n.order-last {\n order: 6 !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mx-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n}\n\n.mx-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n}\n\n.mx-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n}\n\n.mx-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n}\n\n.mx-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n}\n\n.mx-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n}\n\n.mx-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n}\n\n.my-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n}\n\n.my-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n}\n\n.my-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n}\n\n.my-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n}\n\n.my-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n}\n\n.my-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n}\n\n.my-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n}\n\n.mt-0 {\n margin-top: 0 !important;\n}\n\n.mt-1 {\n margin-top: 0.25rem !important;\n}\n\n.mt-2 {\n margin-top: 0.5rem !important;\n}\n\n.mt-3 {\n margin-top: 1rem !important;\n}\n\n.mt-4 {\n margin-top: 1.5rem !important;\n}\n\n.mt-5 {\n margin-top: 3rem !important;\n}\n\n.mt-auto {\n margin-top: auto !important;\n}\n\n.me-0 {\n margin-right: 0 !important;\n}\n\n.me-1 {\n margin-right: 0.25rem !important;\n}\n\n.me-2 {\n margin-right: 0.5rem !important;\n}\n\n.me-3 {\n margin-right: 1rem !important;\n}\n\n.me-4 {\n margin-right: 1.5rem !important;\n}\n\n.me-5 {\n margin-right: 3rem !important;\n}\n\n.me-auto {\n margin-right: auto !important;\n}\n\n.mb-0 {\n margin-bottom: 0 !important;\n}\n\n.mb-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.mb-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.mb-3 {\n margin-bottom: 1rem !important;\n}\n\n.mb-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.mb-5 {\n margin-bottom: 3rem !important;\n}\n\n.mb-auto {\n margin-bottom: auto !important;\n}\n\n.ms-0 {\n margin-left: 0 !important;\n}\n\n.ms-1 {\n margin-left: 0.25rem !important;\n}\n\n.ms-2 {\n margin-left: 0.5rem !important;\n}\n\n.ms-3 {\n margin-left: 1rem !important;\n}\n\n.ms-4 {\n margin-left: 1.5rem !important;\n}\n\n.ms-5 {\n margin-left: 3rem !important;\n}\n\n.ms-auto {\n margin-left: auto !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.px-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n}\n\n.px-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n}\n\n.px-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n}\n\n.px-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n}\n\n.px-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n}\n\n.px-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n}\n\n.py-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n}\n\n.py-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n}\n\n.py-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n}\n\n.py-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n}\n\n.py-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n}\n\n.py-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n}\n\n.pt-0 {\n padding-top: 0 !important;\n}\n\n.pt-1 {\n padding-top: 0.25rem !important;\n}\n\n.pt-2 {\n padding-top: 0.5rem !important;\n}\n\n.pt-3 {\n padding-top: 1rem !important;\n}\n\n.pt-4 {\n padding-top: 1.5rem !important;\n}\n\n.pt-5 {\n padding-top: 3rem !important;\n}\n\n.pe-0 {\n padding-right: 0 !important;\n}\n\n.pe-1 {\n padding-right: 0.25rem !important;\n}\n\n.pe-2 {\n padding-right: 0.5rem !important;\n}\n\n.pe-3 {\n padding-right: 1rem !important;\n}\n\n.pe-4 {\n padding-right: 1.5rem !important;\n}\n\n.pe-5 {\n padding-right: 3rem !important;\n}\n\n.pb-0 {\n padding-bottom: 0 !important;\n}\n\n.pb-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pb-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pb-3 {\n padding-bottom: 1rem !important;\n}\n\n.pb-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pb-5 {\n padding-bottom: 3rem !important;\n}\n\n.ps-0 {\n padding-left: 0 !important;\n}\n\n.ps-1 {\n padding-left: 0.25rem !important;\n}\n\n.ps-2 {\n padding-left: 0.5rem !important;\n}\n\n.ps-3 {\n padding-left: 1rem !important;\n}\n\n.ps-4 {\n padding-left: 1.5rem !important;\n}\n\n.ps-5 {\n padding-left: 3rem !important;\n}\n\n.gap-0 {\n gap: 0 !important;\n}\n\n.gap-1 {\n gap: 0.25rem !important;\n}\n\n.gap-2 {\n gap: 0.5rem !important;\n}\n\n.gap-3 {\n gap: 1rem !important;\n}\n\n.gap-4 {\n gap: 1.5rem !important;\n}\n\n.gap-5 {\n gap: 3rem !important;\n}\n\n.row-gap-0 {\n row-gap: 0 !important;\n}\n\n.row-gap-1 {\n row-gap: 0.25rem !important;\n}\n\n.row-gap-2 {\n row-gap: 0.5rem !important;\n}\n\n.row-gap-3 {\n row-gap: 1rem !important;\n}\n\n.row-gap-4 {\n row-gap: 1.5rem !important;\n}\n\n.row-gap-5 {\n row-gap: 3rem !important;\n}\n\n.column-gap-0 {\n -moz-column-gap: 0 !important;\n column-gap: 0 !important;\n}\n\n.column-gap-1 {\n -moz-column-gap: 0.25rem !important;\n column-gap: 0.25rem !important;\n}\n\n.column-gap-2 {\n -moz-column-gap: 0.5rem !important;\n column-gap: 0.5rem !important;\n}\n\n.column-gap-3 {\n -moz-column-gap: 1rem !important;\n column-gap: 1rem !important;\n}\n\n.column-gap-4 {\n -moz-column-gap: 1.5rem !important;\n column-gap: 1.5rem !important;\n}\n\n.column-gap-5 {\n -moz-column-gap: 3rem !important;\n column-gap: 3rem !important;\n}\n\n.font-monospace {\n font-family: var(--bs-font-monospace) !important;\n}\n\n.fs-1 {\n font-size: calc(1.375rem + 1.5vw) !important;\n}\n\n.fs-2 {\n font-size: calc(1.325rem + 0.9vw) !important;\n}\n\n.fs-3 {\n font-size: calc(1.3rem + 0.6vw) !important;\n}\n\n.fs-4 {\n font-size: calc(1.275rem + 0.3vw) !important;\n}\n\n.fs-5 {\n font-size: 1.25rem !important;\n}\n\n.fs-6 {\n font-size: 1rem !important;\n}\n\n.fst-italic {\n font-style: italic !important;\n}\n\n.fst-normal {\n font-style: normal !important;\n}\n\n.fw-lighter {\n font-weight: lighter !important;\n}\n\n.fw-light {\n font-weight: 300 !important;\n}\n\n.fw-normal {\n font-weight: 400 !important;\n}\n\n.fw-medium {\n font-weight: 500 !important;\n}\n\n.fw-semibold {\n font-weight: 600 !important;\n}\n\n.fw-bold {\n font-weight: 700 !important;\n}\n\n.fw-bolder {\n font-weight: bolder !important;\n}\n\n.lh-1 {\n line-height: 1 !important;\n}\n\n.lh-sm {\n line-height: 1.25 !important;\n}\n\n.lh-base {\n line-height: 1.5 !important;\n}\n\n.lh-lg {\n line-height: 2 !important;\n}\n\n.text-start {\n text-align: left !important;\n}\n\n.text-end {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n.text-decoration-none {\n text-decoration: none !important;\n}\n\n.text-decoration-underline {\n text-decoration: underline !important;\n}\n\n.text-decoration-line-through {\n text-decoration: line-through !important;\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.text-wrap {\n white-space: normal !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n/* rtl:begin:remove */\n.text-break {\n word-wrap: break-word !important;\n word-break: break-word !important;\n}\n\n/* rtl:end:remove */\n.text-primary {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-secondary {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-success {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-info {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-warning {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-danger {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-light {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-dark {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-black {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-white {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-body {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-muted {\n --bs-text-opacity: 1;\n color: var(--bs-secondary-color) !important;\n}\n\n.text-black-50 {\n --bs-text-opacity: 1;\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n --bs-text-opacity: 1;\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-body-secondary {\n --bs-text-opacity: 1;\n color: var(--bs-secondary-color) !important;\n}\n\n.text-body-tertiary {\n --bs-text-opacity: 1;\n color: var(--bs-tertiary-color) !important;\n}\n\n.text-body-emphasis {\n --bs-text-opacity: 1;\n color: var(--bs-emphasis-color) !important;\n}\n\n.text-reset {\n --bs-text-opacity: 1;\n color: inherit !important;\n}\n\n.text-opacity-25 {\n --bs-text-opacity: 0.25;\n}\n\n.text-opacity-50 {\n --bs-text-opacity: 0.5;\n}\n\n.text-opacity-75 {\n --bs-text-opacity: 0.75;\n}\n\n.text-opacity-100 {\n --bs-text-opacity: 1;\n}\n\n.text-primary-emphasis {\n color: var(--bs-primary-text-emphasis) !important;\n}\n\n.text-secondary-emphasis {\n color: var(--bs-secondary-text-emphasis) !important;\n}\n\n.text-success-emphasis {\n color: var(--bs-success-text-emphasis) !important;\n}\n\n.text-info-emphasis {\n color: var(--bs-info-text-emphasis) !important;\n}\n\n.text-warning-emphasis {\n color: var(--bs-warning-text-emphasis) !important;\n}\n\n.text-danger-emphasis {\n color: var(--bs-danger-text-emphasis) !important;\n}\n\n.text-light-emphasis {\n color: var(--bs-light-text-emphasis) !important;\n}\n\n.text-dark-emphasis {\n color: var(--bs-dark-text-emphasis) !important;\n}\n\n.link-opacity-10 {\n --bs-link-opacity: 0.1;\n}\n\n.link-opacity-10-hover:hover {\n --bs-link-opacity: 0.1;\n}\n\n.link-opacity-25 {\n --bs-link-opacity: 0.25;\n}\n\n.link-opacity-25-hover:hover {\n --bs-link-opacity: 0.25;\n}\n\n.link-opacity-50 {\n --bs-link-opacity: 0.5;\n}\n\n.link-opacity-50-hover:hover {\n --bs-link-opacity: 0.5;\n}\n\n.link-opacity-75 {\n --bs-link-opacity: 0.75;\n}\n\n.link-opacity-75-hover:hover {\n --bs-link-opacity: 0.75;\n}\n\n.link-opacity-100 {\n --bs-link-opacity: 1;\n}\n\n.link-opacity-100-hover:hover {\n --bs-link-opacity: 1;\n}\n\n.link-offset-1 {\n text-underline-offset: 0.125em !important;\n}\n\n.link-offset-1-hover:hover {\n text-underline-offset: 0.125em !important;\n}\n\n.link-offset-2 {\n text-underline-offset: 0.25em !important;\n}\n\n.link-offset-2-hover:hover {\n text-underline-offset: 0.25em !important;\n}\n\n.link-offset-3 {\n text-underline-offset: 0.375em !important;\n}\n\n.link-offset-3-hover:hover {\n text-underline-offset: 0.375em !important;\n}\n\n.link-underline-primary {\n --bs-link-underline-opacity: 1;\n -webkit-text-decoration-color: rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important;\n text-decoration-color: rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-secondary {\n --bs-link-underline-opacity: 1;\n -webkit-text-decoration-color: rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important;\n text-decoration-color: rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-success {\n --bs-link-underline-opacity: 1;\n -webkit-text-decoration-color: rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important;\n text-decoration-color: rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-info {\n --bs-link-underline-opacity: 1;\n -webkit-text-decoration-color: rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important;\n text-decoration-color: rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-warning {\n --bs-link-underline-opacity: 1;\n -webkit-text-decoration-color: rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important;\n text-decoration-color: rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-danger {\n --bs-link-underline-opacity: 1;\n -webkit-text-decoration-color: rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important;\n text-decoration-color: rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-light {\n --bs-link-underline-opacity: 1;\n -webkit-text-decoration-color: rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important;\n text-decoration-color: rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-dark {\n --bs-link-underline-opacity: 1;\n -webkit-text-decoration-color: rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important;\n text-decoration-color: rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline {\n --bs-link-underline-opacity: 1;\n -webkit-text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important;\n text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-underline-opacity-0 {\n --bs-link-underline-opacity: 0;\n}\n\n.link-underline-opacity-0-hover:hover {\n --bs-link-underline-opacity: 0;\n}\n\n.link-underline-opacity-10 {\n --bs-link-underline-opacity: 0.1;\n}\n\n.link-underline-opacity-10-hover:hover {\n --bs-link-underline-opacity: 0.1;\n}\n\n.link-underline-opacity-25 {\n --bs-link-underline-opacity: 0.25;\n}\n\n.link-underline-opacity-25-hover:hover {\n --bs-link-underline-opacity: 0.25;\n}\n\n.link-underline-opacity-50 {\n --bs-link-underline-opacity: 0.5;\n}\n\n.link-underline-opacity-50-hover:hover {\n --bs-link-underline-opacity: 0.5;\n}\n\n.link-underline-opacity-75 {\n --bs-link-underline-opacity: 0.75;\n}\n\n.link-underline-opacity-75-hover:hover {\n --bs-link-underline-opacity: 0.75;\n}\n\n.link-underline-opacity-100 {\n --bs-link-underline-opacity: 1;\n}\n\n.link-underline-opacity-100-hover:hover {\n --bs-link-underline-opacity: 1;\n}\n\n.bg-primary {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-secondary {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-success {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-info {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-warning {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-danger {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-light {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-dark {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-black {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-white {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-body {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-transparent {\n --bs-bg-opacity: 1;\n background-color: transparent !important;\n}\n\n.bg-body-secondary {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-secondary-bg-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-body-tertiary {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-tertiary-bg-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-opacity-10 {\n --bs-bg-opacity: 0.1;\n}\n\n.bg-opacity-25 {\n --bs-bg-opacity: 0.25;\n}\n\n.bg-opacity-50 {\n --bs-bg-opacity: 0.5;\n}\n\n.bg-opacity-75 {\n --bs-bg-opacity: 0.75;\n}\n\n.bg-opacity-100 {\n --bs-bg-opacity: 1;\n}\n\n.bg-primary-subtle {\n background-color: var(--bs-primary-bg-subtle) !important;\n}\n\n.bg-secondary-subtle {\n background-color: var(--bs-secondary-bg-subtle) !important;\n}\n\n.bg-success-subtle {\n background-color: var(--bs-success-bg-subtle) !important;\n}\n\n.bg-info-subtle {\n background-color: var(--bs-info-bg-subtle) !important;\n}\n\n.bg-warning-subtle {\n background-color: var(--bs-warning-bg-subtle) !important;\n}\n\n.bg-danger-subtle {\n background-color: var(--bs-danger-bg-subtle) !important;\n}\n\n.bg-light-subtle {\n background-color: var(--bs-light-bg-subtle) !important;\n}\n\n.bg-dark-subtle {\n background-color: var(--bs-dark-bg-subtle) !important;\n}\n\n.bg-gradient {\n background-image: var(--bs-gradient) !important;\n}\n\n.user-select-all {\n -webkit-user-select: all !important;\n -moz-user-select: all !important;\n user-select: all !important;\n}\n\n.user-select-auto {\n -webkit-user-select: auto !important;\n -moz-user-select: auto !important;\n user-select: auto !important;\n}\n\n.user-select-none {\n -webkit-user-select: none !important;\n -moz-user-select: none !important;\n user-select: none !important;\n}\n\n.pe-none {\n pointer-events: none !important;\n}\n\n.pe-auto {\n pointer-events: auto !important;\n}\n\n.rounded {\n border-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.rounded-1 {\n border-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-2 {\n border-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-3 {\n border-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-4 {\n border-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-5 {\n border-radius: var(--bs-border-radius-xxl) !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: var(--bs-border-radius-pill) !important;\n}\n\n.rounded-top {\n border-top-left-radius: var(--bs-border-radius) !important;\n border-top-right-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-top-0 {\n border-top-left-radius: 0 !important;\n border-top-right-radius: 0 !important;\n}\n\n.rounded-top-1 {\n border-top-left-radius: var(--bs-border-radius-sm) !important;\n border-top-right-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-top-2 {\n border-top-left-radius: var(--bs-border-radius) !important;\n border-top-right-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-top-3 {\n border-top-left-radius: var(--bs-border-radius-lg) !important;\n border-top-right-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-top-4 {\n border-top-left-radius: var(--bs-border-radius-xl) !important;\n border-top-right-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-top-5 {\n border-top-left-radius: var(--bs-border-radius-xxl) !important;\n border-top-right-radius: var(--bs-border-radius-xxl) !important;\n}\n\n.rounded-top-circle {\n border-top-left-radius: 50% !important;\n border-top-right-radius: 50% !important;\n}\n\n.rounded-top-pill {\n border-top-left-radius: var(--bs-border-radius-pill) !important;\n border-top-right-radius: var(--bs-border-radius-pill) !important;\n}\n\n.rounded-end {\n border-top-right-radius: var(--bs-border-radius) !important;\n border-bottom-right-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-end-0 {\n border-top-right-radius: 0 !important;\n border-bottom-right-radius: 0 !important;\n}\n\n.rounded-end-1 {\n border-top-right-radius: var(--bs-border-radius-sm) !important;\n border-bottom-right-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-end-2 {\n border-top-right-radius: var(--bs-border-radius) !important;\n border-bottom-right-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-end-3 {\n border-top-right-radius: var(--bs-border-radius-lg) !important;\n border-bottom-right-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-end-4 {\n border-top-right-radius: var(--bs-border-radius-xl) !important;\n border-bottom-right-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-end-5 {\n border-top-right-radius: var(--bs-border-radius-xxl) !important;\n border-bottom-right-radius: var(--bs-border-radius-xxl) !important;\n}\n\n.rounded-end-circle {\n border-top-right-radius: 50% !important;\n border-bottom-right-radius: 50% !important;\n}\n\n.rounded-end-pill {\n border-top-right-radius: var(--bs-border-radius-pill) !important;\n border-bottom-right-radius: var(--bs-border-radius-pill) !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: var(--bs-border-radius) !important;\n border-bottom-left-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-bottom-0 {\n border-bottom-right-radius: 0 !important;\n border-bottom-left-radius: 0 !important;\n}\n\n.rounded-bottom-1 {\n border-bottom-right-radius: var(--bs-border-radius-sm) !important;\n border-bottom-left-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-bottom-2 {\n border-bottom-right-radius: var(--bs-border-radius) !important;\n border-bottom-left-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-bottom-3 {\n border-bottom-right-radius: var(--bs-border-radius-lg) !important;\n border-bottom-left-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-bottom-4 {\n border-bottom-right-radius: var(--bs-border-radius-xl) !important;\n border-bottom-left-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-bottom-5 {\n border-bottom-right-radius: var(--bs-border-radius-xxl) !important;\n border-bottom-left-radius: var(--bs-border-radius-xxl) !important;\n}\n\n.rounded-bottom-circle {\n border-bottom-right-radius: 50% !important;\n border-bottom-left-radius: 50% !important;\n}\n\n.rounded-bottom-pill {\n border-bottom-right-radius: var(--bs-border-radius-pill) !important;\n border-bottom-left-radius: var(--bs-border-radius-pill) !important;\n}\n\n.rounded-start {\n border-bottom-left-radius: var(--bs-border-radius) !important;\n border-top-left-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-start-0 {\n border-bottom-left-radius: 0 !important;\n border-top-left-radius: 0 !important;\n}\n\n.rounded-start-1 {\n border-bottom-left-radius: var(--bs-border-radius-sm) !important;\n border-top-left-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-start-2 {\n border-bottom-left-radius: var(--bs-border-radius) !important;\n border-top-left-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-start-3 {\n border-bottom-left-radius: var(--bs-border-radius-lg) !important;\n border-top-left-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-start-4 {\n border-bottom-left-radius: var(--bs-border-radius-xl) !important;\n border-top-left-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-start-5 {\n border-bottom-left-radius: var(--bs-border-radius-xxl) !important;\n border-top-left-radius: var(--bs-border-radius-xxl) !important;\n}\n\n.rounded-start-circle {\n border-bottom-left-radius: 50% !important;\n border-top-left-radius: 50% !important;\n}\n\n.rounded-start-pill {\n border-bottom-left-radius: var(--bs-border-radius-pill) !important;\n border-top-left-radius: var(--bs-border-radius-pill) !important;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n.z-n1 {\n z-index: -1 !important;\n}\n\n.z-0 {\n z-index: 0 !important;\n}\n\n.z-1 {\n z-index: 1 !important;\n}\n\n.z-2 {\n z-index: 2 !important;\n}\n\n.z-3 {\n z-index: 3 !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-start {\n float: left !important;\n }\n .float-sm-end {\n float: right !important;\n }\n .float-sm-none {\n float: none !important;\n }\n .object-fit-sm-contain {\n -o-object-fit: contain !important;\n object-fit: contain !important;\n }\n .object-fit-sm-cover {\n -o-object-fit: cover !important;\n object-fit: cover !important;\n }\n .object-fit-sm-fill {\n -o-object-fit: fill !important;\n object-fit: fill !important;\n }\n .object-fit-sm-scale {\n -o-object-fit: scale-down !important;\n object-fit: scale-down !important;\n }\n .object-fit-sm-none {\n -o-object-fit: none !important;\n object-fit: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-grid {\n display: grid !important;\n }\n .d-sm-inline-grid {\n display: inline-grid !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n .d-sm-none {\n display: none !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .justify-content-sm-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n .order-sm-first {\n order: -1 !important;\n }\n .order-sm-0 {\n order: 0 !important;\n }\n .order-sm-1 {\n order: 1 !important;\n }\n .order-sm-2 {\n order: 2 !important;\n }\n .order-sm-3 {\n order: 3 !important;\n }\n .order-sm-4 {\n order: 4 !important;\n }\n .order-sm-5 {\n order: 5 !important;\n }\n .order-sm-last {\n order: 6 !important;\n }\n .m-sm-0 {\n margin: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mx-sm-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-sm-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-sm-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-sm-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-sm-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-sm-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-sm-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-sm-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-sm-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-sm-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-sm-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-sm-0 {\n margin-top: 0 !important;\n }\n .mt-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mt-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mt-sm-3 {\n margin-top: 1rem !important;\n }\n .mt-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mt-sm-5 {\n margin-top: 3rem !important;\n }\n .mt-sm-auto {\n margin-top: auto !important;\n }\n .me-sm-0 {\n margin-right: 0 !important;\n }\n .me-sm-1 {\n margin-right: 0.25rem !important;\n }\n .me-sm-2 {\n margin-right: 0.5rem !important;\n }\n .me-sm-3 {\n margin-right: 1rem !important;\n }\n .me-sm-4 {\n margin-right: 1.5rem !important;\n }\n .me-sm-5 {\n margin-right: 3rem !important;\n }\n .me-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-0 {\n margin-bottom: 0 !important;\n }\n .mb-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-sm-3 {\n margin-bottom: 1rem !important;\n }\n .mb-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-sm-5 {\n margin-bottom: 3rem !important;\n }\n .mb-sm-auto {\n margin-bottom: auto !important;\n }\n .ms-sm-0 {\n margin-left: 0 !important;\n }\n .ms-sm-1 {\n margin-left: 0.25rem !important;\n }\n .ms-sm-2 {\n margin-left: 0.5rem !important;\n }\n .ms-sm-3 {\n margin-left: 1rem !important;\n }\n .ms-sm-4 {\n margin-left: 1.5rem !important;\n }\n .ms-sm-5 {\n margin-left: 3rem !important;\n }\n .ms-sm-auto {\n margin-left: auto !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .px-sm-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-sm-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-sm-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-sm-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-sm-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-sm-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-sm-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-sm-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-sm-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-sm-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-sm-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-sm-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-sm-0 {\n padding-top: 0 !important;\n }\n .pt-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pt-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pt-sm-3 {\n padding-top: 1rem !important;\n }\n .pt-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pt-sm-5 {\n padding-top: 3rem !important;\n }\n .pe-sm-0 {\n padding-right: 0 !important;\n }\n .pe-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pe-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pe-sm-3 {\n padding-right: 1rem !important;\n }\n .pe-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pe-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-0 {\n padding-bottom: 0 !important;\n }\n .pb-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pb-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-sm-5 {\n padding-bottom: 3rem !important;\n }\n .ps-sm-0 {\n padding-left: 0 !important;\n }\n .ps-sm-1 {\n padding-left: 0.25rem !important;\n }\n .ps-sm-2 {\n padding-left: 0.5rem !important;\n }\n .ps-sm-3 {\n padding-left: 1rem !important;\n }\n .ps-sm-4 {\n padding-left: 1.5rem !important;\n }\n .ps-sm-5 {\n padding-left: 3rem !important;\n }\n .gap-sm-0 {\n gap: 0 !important;\n }\n .gap-sm-1 {\n gap: 0.25rem !important;\n }\n .gap-sm-2 {\n gap: 0.5rem !important;\n }\n .gap-sm-3 {\n gap: 1rem !important;\n }\n .gap-sm-4 {\n gap: 1.5rem !important;\n }\n .gap-sm-5 {\n gap: 3rem !important;\n }\n .row-gap-sm-0 {\n row-gap: 0 !important;\n }\n .row-gap-sm-1 {\n row-gap: 0.25rem !important;\n }\n .row-gap-sm-2 {\n row-gap: 0.5rem !important;\n }\n .row-gap-sm-3 {\n row-gap: 1rem !important;\n }\n .row-gap-sm-4 {\n row-gap: 1.5rem !important;\n }\n .row-gap-sm-5 {\n row-gap: 3rem !important;\n }\n .column-gap-sm-0 {\n -moz-column-gap: 0 !important;\n column-gap: 0 !important;\n }\n .column-gap-sm-1 {\n -moz-column-gap: 0.25rem !important;\n column-gap: 0.25rem !important;\n }\n .column-gap-sm-2 {\n -moz-column-gap: 0.5rem !important;\n column-gap: 0.5rem !important;\n }\n .column-gap-sm-3 {\n -moz-column-gap: 1rem !important;\n column-gap: 1rem !important;\n }\n .column-gap-sm-4 {\n -moz-column-gap: 1.5rem !important;\n column-gap: 1.5rem !important;\n }\n .column-gap-sm-5 {\n -moz-column-gap: 3rem !important;\n column-gap: 3rem !important;\n }\n .text-sm-start {\n text-align: left !important;\n }\n .text-sm-end {\n text-align: right !important;\n }\n .text-sm-center {\n text-align: center !important;\n }\n}\n@media (min-width: 768px) {\n .float-md-start {\n float: left !important;\n }\n .float-md-end {\n float: right !important;\n }\n .float-md-none {\n float: none !important;\n }\n .object-fit-md-contain {\n -o-object-fit: contain !important;\n object-fit: contain !important;\n }\n .object-fit-md-cover {\n -o-object-fit: cover !important;\n object-fit: cover !important;\n }\n .object-fit-md-fill {\n -o-object-fit: fill !important;\n object-fit: fill !important;\n }\n .object-fit-md-scale {\n -o-object-fit: scale-down !important;\n object-fit: scale-down !important;\n }\n .object-fit-md-none {\n -o-object-fit: none !important;\n object-fit: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-grid {\n display: grid !important;\n }\n .d-md-inline-grid {\n display: inline-grid !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n .d-md-none {\n display: none !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .justify-content-md-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n .order-md-first {\n order: -1 !important;\n }\n .order-md-0 {\n order: 0 !important;\n }\n .order-md-1 {\n order: 1 !important;\n }\n .order-md-2 {\n order: 2 !important;\n }\n .order-md-3 {\n order: 3 !important;\n }\n .order-md-4 {\n order: 4 !important;\n }\n .order-md-5 {\n order: 5 !important;\n }\n .order-md-last {\n order: 6 !important;\n }\n .m-md-0 {\n margin: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mx-md-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-md-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-md-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-md-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-md-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-md-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-md-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-md-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-md-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-md-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-md-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-md-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-md-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-md-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-md-0 {\n margin-top: 0 !important;\n }\n .mt-md-1 {\n margin-top: 0.25rem !important;\n }\n .mt-md-2 {\n margin-top: 0.5rem !important;\n }\n .mt-md-3 {\n margin-top: 1rem !important;\n }\n .mt-md-4 {\n margin-top: 1.5rem !important;\n }\n .mt-md-5 {\n margin-top: 3rem !important;\n }\n .mt-md-auto {\n margin-top: auto !important;\n }\n .me-md-0 {\n margin-right: 0 !important;\n }\n .me-md-1 {\n margin-right: 0.25rem !important;\n }\n .me-md-2 {\n margin-right: 0.5rem !important;\n }\n .me-md-3 {\n margin-right: 1rem !important;\n }\n .me-md-4 {\n margin-right: 1.5rem !important;\n }\n .me-md-5 {\n margin-right: 3rem !important;\n }\n .me-md-auto {\n margin-right: auto !important;\n }\n .mb-md-0 {\n margin-bottom: 0 !important;\n }\n .mb-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-md-3 {\n margin-bottom: 1rem !important;\n }\n .mb-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-md-5 {\n margin-bottom: 3rem !important;\n }\n .mb-md-auto {\n margin-bottom: auto !important;\n }\n .ms-md-0 {\n margin-left: 0 !important;\n }\n .ms-md-1 {\n margin-left: 0.25rem !important;\n }\n .ms-md-2 {\n margin-left: 0.5rem !important;\n }\n .ms-md-3 {\n margin-left: 1rem !important;\n }\n .ms-md-4 {\n margin-left: 1.5rem !important;\n }\n .ms-md-5 {\n margin-left: 3rem !important;\n }\n .ms-md-auto {\n margin-left: auto !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .px-md-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-md-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-md-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-md-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-md-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-md-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-md-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-md-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-md-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-md-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-md-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-md-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-md-0 {\n padding-top: 0 !important;\n }\n .pt-md-1 {\n padding-top: 0.25rem !important;\n }\n .pt-md-2 {\n padding-top: 0.5rem !important;\n }\n .pt-md-3 {\n padding-top: 1rem !important;\n }\n .pt-md-4 {\n padding-top: 1.5rem !important;\n }\n .pt-md-5 {\n padding-top: 3rem !important;\n }\n .pe-md-0 {\n padding-right: 0 !important;\n }\n .pe-md-1 {\n padding-right: 0.25rem !important;\n }\n .pe-md-2 {\n padding-right: 0.5rem !important;\n }\n .pe-md-3 {\n padding-right: 1rem !important;\n }\n .pe-md-4 {\n padding-right: 1.5rem !important;\n }\n .pe-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-0 {\n padding-bottom: 0 !important;\n }\n .pb-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-md-3 {\n padding-bottom: 1rem !important;\n }\n .pb-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-md-5 {\n padding-bottom: 3rem !important;\n }\n .ps-md-0 {\n padding-left: 0 !important;\n }\n .ps-md-1 {\n padding-left: 0.25rem !important;\n }\n .ps-md-2 {\n padding-left: 0.5rem !important;\n }\n .ps-md-3 {\n padding-left: 1rem !important;\n }\n .ps-md-4 {\n padding-left: 1.5rem !important;\n }\n .ps-md-5 {\n padding-left: 3rem !important;\n }\n .gap-md-0 {\n gap: 0 !important;\n }\n .gap-md-1 {\n gap: 0.25rem !important;\n }\n .gap-md-2 {\n gap: 0.5rem !important;\n }\n .gap-md-3 {\n gap: 1rem !important;\n }\n .gap-md-4 {\n gap: 1.5rem !important;\n }\n .gap-md-5 {\n gap: 3rem !important;\n }\n .row-gap-md-0 {\n row-gap: 0 !important;\n }\n .row-gap-md-1 {\n row-gap: 0.25rem !important;\n }\n .row-gap-md-2 {\n row-gap: 0.5rem !important;\n }\n .row-gap-md-3 {\n row-gap: 1rem !important;\n }\n .row-gap-md-4 {\n row-gap: 1.5rem !important;\n }\n .row-gap-md-5 {\n row-gap: 3rem !important;\n }\n .column-gap-md-0 {\n -moz-column-gap: 0 !important;\n column-gap: 0 !important;\n }\n .column-gap-md-1 {\n -moz-column-gap: 0.25rem !important;\n column-gap: 0.25rem !important;\n }\n .column-gap-md-2 {\n -moz-column-gap: 0.5rem !important;\n column-gap: 0.5rem !important;\n }\n .column-gap-md-3 {\n -moz-column-gap: 1rem !important;\n column-gap: 1rem !important;\n }\n .column-gap-md-4 {\n -moz-column-gap: 1.5rem !important;\n column-gap: 1.5rem !important;\n }\n .column-gap-md-5 {\n -moz-column-gap: 3rem !important;\n column-gap: 3rem !important;\n }\n .text-md-start {\n text-align: left !important;\n }\n .text-md-end {\n text-align: right !important;\n }\n .text-md-center {\n text-align: center !important;\n }\n}\n@media (min-width: 992px) {\n .float-lg-start {\n float: left !important;\n }\n .float-lg-end {\n float: right !important;\n }\n .float-lg-none {\n float: none !important;\n }\n .object-fit-lg-contain {\n -o-object-fit: contain !important;\n object-fit: contain !important;\n }\n .object-fit-lg-cover {\n -o-object-fit: cover !important;\n object-fit: cover !important;\n }\n .object-fit-lg-fill {\n -o-object-fit: fill !important;\n object-fit: fill !important;\n }\n .object-fit-lg-scale {\n -o-object-fit: scale-down !important;\n object-fit: scale-down !important;\n }\n .object-fit-lg-none {\n -o-object-fit: none !important;\n object-fit: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-grid {\n display: grid !important;\n }\n .d-lg-inline-grid {\n display: inline-grid !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n .d-lg-none {\n display: none !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .justify-content-lg-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n .order-lg-first {\n order: -1 !important;\n }\n .order-lg-0 {\n order: 0 !important;\n }\n .order-lg-1 {\n order: 1 !important;\n }\n .order-lg-2 {\n order: 2 !important;\n }\n .order-lg-3 {\n order: 3 !important;\n }\n .order-lg-4 {\n order: 4 !important;\n }\n .order-lg-5 {\n order: 5 !important;\n }\n .order-lg-last {\n order: 6 !important;\n }\n .m-lg-0 {\n margin: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mx-lg-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-lg-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-lg-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-lg-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-lg-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-lg-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-lg-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-lg-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-lg-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-lg-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-lg-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-lg-0 {\n margin-top: 0 !important;\n }\n .mt-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mt-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mt-lg-3 {\n margin-top: 1rem !important;\n }\n .mt-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mt-lg-5 {\n margin-top: 3rem !important;\n }\n .mt-lg-auto {\n margin-top: auto !important;\n }\n .me-lg-0 {\n margin-right: 0 !important;\n }\n .me-lg-1 {\n margin-right: 0.25rem !important;\n }\n .me-lg-2 {\n margin-right: 0.5rem !important;\n }\n .me-lg-3 {\n margin-right: 1rem !important;\n }\n .me-lg-4 {\n margin-right: 1.5rem !important;\n }\n .me-lg-5 {\n margin-right: 3rem !important;\n }\n .me-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-0 {\n margin-bottom: 0 !important;\n }\n .mb-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-lg-3 {\n margin-bottom: 1rem !important;\n }\n .mb-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-lg-5 {\n margin-bottom: 3rem !important;\n }\n .mb-lg-auto {\n margin-bottom: auto !important;\n }\n .ms-lg-0 {\n margin-left: 0 !important;\n }\n .ms-lg-1 {\n margin-left: 0.25rem !important;\n }\n .ms-lg-2 {\n margin-left: 0.5rem !important;\n }\n .ms-lg-3 {\n margin-left: 1rem !important;\n }\n .ms-lg-4 {\n margin-left: 1.5rem !important;\n }\n .ms-lg-5 {\n margin-left: 3rem !important;\n }\n .ms-lg-auto {\n margin-left: auto !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .px-lg-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-lg-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-lg-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-lg-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-lg-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-lg-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-lg-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-lg-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-lg-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-lg-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-lg-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-lg-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-lg-0 {\n padding-top: 0 !important;\n }\n .pt-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pt-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pt-lg-3 {\n padding-top: 1rem !important;\n }\n .pt-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pt-lg-5 {\n padding-top: 3rem !important;\n }\n .pe-lg-0 {\n padding-right: 0 !important;\n }\n .pe-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pe-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pe-lg-3 {\n padding-right: 1rem !important;\n }\n .pe-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pe-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-0 {\n padding-bottom: 0 !important;\n }\n .pb-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pb-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-lg-5 {\n padding-bottom: 3rem !important;\n }\n .ps-lg-0 {\n padding-left: 0 !important;\n }\n .ps-lg-1 {\n padding-left: 0.25rem !important;\n }\n .ps-lg-2 {\n padding-left: 0.5rem !important;\n }\n .ps-lg-3 {\n padding-left: 1rem !important;\n }\n .ps-lg-4 {\n padding-left: 1.5rem !important;\n }\n .ps-lg-5 {\n padding-left: 3rem !important;\n }\n .gap-lg-0 {\n gap: 0 !important;\n }\n .gap-lg-1 {\n gap: 0.25rem !important;\n }\n .gap-lg-2 {\n gap: 0.5rem !important;\n }\n .gap-lg-3 {\n gap: 1rem !important;\n }\n .gap-lg-4 {\n gap: 1.5rem !important;\n }\n .gap-lg-5 {\n gap: 3rem !important;\n }\n .row-gap-lg-0 {\n row-gap: 0 !important;\n }\n .row-gap-lg-1 {\n row-gap: 0.25rem !important;\n }\n .row-gap-lg-2 {\n row-gap: 0.5rem !important;\n }\n .row-gap-lg-3 {\n row-gap: 1rem !important;\n }\n .row-gap-lg-4 {\n row-gap: 1.5rem !important;\n }\n .row-gap-lg-5 {\n row-gap: 3rem !important;\n }\n .column-gap-lg-0 {\n -moz-column-gap: 0 !important;\n column-gap: 0 !important;\n }\n .column-gap-lg-1 {\n -moz-column-gap: 0.25rem !important;\n column-gap: 0.25rem !important;\n }\n .column-gap-lg-2 {\n -moz-column-gap: 0.5rem !important;\n column-gap: 0.5rem !important;\n }\n .column-gap-lg-3 {\n -moz-column-gap: 1rem !important;\n column-gap: 1rem !important;\n }\n .column-gap-lg-4 {\n -moz-column-gap: 1.5rem !important;\n column-gap: 1.5rem !important;\n }\n .column-gap-lg-5 {\n -moz-column-gap: 3rem !important;\n column-gap: 3rem !important;\n }\n .text-lg-start {\n text-align: left !important;\n }\n .text-lg-end {\n text-align: right !important;\n }\n .text-lg-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1200px) {\n .float-xl-start {\n float: left !important;\n }\n .float-xl-end {\n float: right !important;\n }\n .float-xl-none {\n float: none !important;\n }\n .object-fit-xl-contain {\n -o-object-fit: contain !important;\n object-fit: contain !important;\n }\n .object-fit-xl-cover {\n -o-object-fit: cover !important;\n object-fit: cover !important;\n }\n .object-fit-xl-fill {\n -o-object-fit: fill !important;\n object-fit: fill !important;\n }\n .object-fit-xl-scale {\n -o-object-fit: scale-down !important;\n object-fit: scale-down !important;\n }\n .object-fit-xl-none {\n -o-object-fit: none !important;\n object-fit: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-grid {\n display: grid !important;\n }\n .d-xl-inline-grid {\n display: inline-grid !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n .d-xl-none {\n display: none !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .justify-content-xl-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n .order-xl-first {\n order: -1 !important;\n }\n .order-xl-0 {\n order: 0 !important;\n }\n .order-xl-1 {\n order: 1 !important;\n }\n .order-xl-2 {\n order: 2 !important;\n }\n .order-xl-3 {\n order: 3 !important;\n }\n .order-xl-4 {\n order: 4 !important;\n }\n .order-xl-5 {\n order: 5 !important;\n }\n .order-xl-last {\n order: 6 !important;\n }\n .m-xl-0 {\n margin: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mx-xl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-xl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-xl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-xl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-xl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-xl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-xl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-xl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-xl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-xl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-xl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-xl-0 {\n margin-top: 0 !important;\n }\n .mt-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mt-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mt-xl-3 {\n margin-top: 1rem !important;\n }\n .mt-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mt-xl-5 {\n margin-top: 3rem !important;\n }\n .mt-xl-auto {\n margin-top: auto !important;\n }\n .me-xl-0 {\n margin-right: 0 !important;\n }\n .me-xl-1 {\n margin-right: 0.25rem !important;\n }\n .me-xl-2 {\n margin-right: 0.5rem !important;\n }\n .me-xl-3 {\n margin-right: 1rem !important;\n }\n .me-xl-4 {\n margin-right: 1.5rem !important;\n }\n .me-xl-5 {\n margin-right: 3rem !important;\n }\n .me-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-0 {\n margin-bottom: 0 !important;\n }\n .mb-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-xl-3 {\n margin-bottom: 1rem !important;\n }\n .mb-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-xl-5 {\n margin-bottom: 3rem !important;\n }\n .mb-xl-auto {\n margin-bottom: auto !important;\n }\n .ms-xl-0 {\n margin-left: 0 !important;\n }\n .ms-xl-1 {\n margin-left: 0.25rem !important;\n }\n .ms-xl-2 {\n margin-left: 0.5rem !important;\n }\n .ms-xl-3 {\n margin-left: 1rem !important;\n }\n .ms-xl-4 {\n margin-left: 1.5rem !important;\n }\n .ms-xl-5 {\n margin-left: 3rem !important;\n }\n .ms-xl-auto {\n margin-left: auto !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .px-xl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-xl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-xl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-xl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-xl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-xl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-xl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-xl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-xl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-xl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-xl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-xl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-xl-0 {\n padding-top: 0 !important;\n }\n .pt-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pt-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pt-xl-3 {\n padding-top: 1rem !important;\n }\n .pt-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pt-xl-5 {\n padding-top: 3rem !important;\n }\n .pe-xl-0 {\n padding-right: 0 !important;\n }\n .pe-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pe-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pe-xl-3 {\n padding-right: 1rem !important;\n }\n .pe-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pe-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-0 {\n padding-bottom: 0 !important;\n }\n .pb-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pb-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-xl-5 {\n padding-bottom: 3rem !important;\n }\n .ps-xl-0 {\n padding-left: 0 !important;\n }\n .ps-xl-1 {\n padding-left: 0.25rem !important;\n }\n .ps-xl-2 {\n padding-left: 0.5rem !important;\n }\n .ps-xl-3 {\n padding-left: 1rem !important;\n }\n .ps-xl-4 {\n padding-left: 1.5rem !important;\n }\n .ps-xl-5 {\n padding-left: 3rem !important;\n }\n .gap-xl-0 {\n gap: 0 !important;\n }\n .gap-xl-1 {\n gap: 0.25rem !important;\n }\n .gap-xl-2 {\n gap: 0.5rem !important;\n }\n .gap-xl-3 {\n gap: 1rem !important;\n }\n .gap-xl-4 {\n gap: 1.5rem !important;\n }\n .gap-xl-5 {\n gap: 3rem !important;\n }\n .row-gap-xl-0 {\n row-gap: 0 !important;\n }\n .row-gap-xl-1 {\n row-gap: 0.25rem !important;\n }\n .row-gap-xl-2 {\n row-gap: 0.5rem !important;\n }\n .row-gap-xl-3 {\n row-gap: 1rem !important;\n }\n .row-gap-xl-4 {\n row-gap: 1.5rem !important;\n }\n .row-gap-xl-5 {\n row-gap: 3rem !important;\n }\n .column-gap-xl-0 {\n -moz-column-gap: 0 !important;\n column-gap: 0 !important;\n }\n .column-gap-xl-1 {\n -moz-column-gap: 0.25rem !important;\n column-gap: 0.25rem !important;\n }\n .column-gap-xl-2 {\n -moz-column-gap: 0.5rem !important;\n column-gap: 0.5rem !important;\n }\n .column-gap-xl-3 {\n -moz-column-gap: 1rem !important;\n column-gap: 1rem !important;\n }\n .column-gap-xl-4 {\n -moz-column-gap: 1.5rem !important;\n column-gap: 1.5rem !important;\n }\n .column-gap-xl-5 {\n -moz-column-gap: 3rem !important;\n column-gap: 3rem !important;\n }\n .text-xl-start {\n text-align: left !important;\n }\n .text-xl-end {\n text-align: right !important;\n }\n .text-xl-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1400px) {\n .float-xxl-start {\n float: left !important;\n }\n .float-xxl-end {\n float: right !important;\n }\n .float-xxl-none {\n float: none !important;\n }\n .object-fit-xxl-contain {\n -o-object-fit: contain !important;\n object-fit: contain !important;\n }\n .object-fit-xxl-cover {\n -o-object-fit: cover !important;\n object-fit: cover !important;\n }\n .object-fit-xxl-fill {\n -o-object-fit: fill !important;\n object-fit: fill !important;\n }\n .object-fit-xxl-scale {\n -o-object-fit: scale-down !important;\n object-fit: scale-down !important;\n }\n .object-fit-xxl-none {\n -o-object-fit: none !important;\n object-fit: none !important;\n }\n .d-xxl-inline {\n display: inline !important;\n }\n .d-xxl-inline-block {\n display: inline-block !important;\n }\n .d-xxl-block {\n display: block !important;\n }\n .d-xxl-grid {\n display: grid !important;\n }\n .d-xxl-inline-grid {\n display: inline-grid !important;\n }\n .d-xxl-table {\n display: table !important;\n }\n .d-xxl-table-row {\n display: table-row !important;\n }\n .d-xxl-table-cell {\n display: table-cell !important;\n }\n .d-xxl-flex {\n display: flex !important;\n }\n .d-xxl-inline-flex {\n display: inline-flex !important;\n }\n .d-xxl-none {\n display: none !important;\n }\n .flex-xxl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xxl-row {\n flex-direction: row !important;\n }\n .flex-xxl-column {\n flex-direction: column !important;\n }\n .flex-xxl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xxl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xxl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xxl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xxl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xxl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-xxl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xxl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xxl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-xxl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xxl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xxl-center {\n justify-content: center !important;\n }\n .justify-content-xxl-between {\n justify-content: space-between !important;\n }\n .justify-content-xxl-around {\n justify-content: space-around !important;\n }\n .justify-content-xxl-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-xxl-start {\n align-items: flex-start !important;\n }\n .align-items-xxl-end {\n align-items: flex-end !important;\n }\n .align-items-xxl-center {\n align-items: center !important;\n }\n .align-items-xxl-baseline {\n align-items: baseline !important;\n }\n .align-items-xxl-stretch {\n align-items: stretch !important;\n }\n .align-content-xxl-start {\n align-content: flex-start !important;\n }\n .align-content-xxl-end {\n align-content: flex-end !important;\n }\n .align-content-xxl-center {\n align-content: center !important;\n }\n .align-content-xxl-between {\n align-content: space-between !important;\n }\n .align-content-xxl-around {\n align-content: space-around !important;\n }\n .align-content-xxl-stretch {\n align-content: stretch !important;\n }\n .align-self-xxl-auto {\n align-self: auto !important;\n }\n .align-self-xxl-start {\n align-self: flex-start !important;\n }\n .align-self-xxl-end {\n align-self: flex-end !important;\n }\n .align-self-xxl-center {\n align-self: center !important;\n }\n .align-self-xxl-baseline {\n align-self: baseline !important;\n }\n .align-self-xxl-stretch {\n align-self: stretch !important;\n }\n .order-xxl-first {\n order: -1 !important;\n }\n .order-xxl-0 {\n order: 0 !important;\n }\n .order-xxl-1 {\n order: 1 !important;\n }\n .order-xxl-2 {\n order: 2 !important;\n }\n .order-xxl-3 {\n order: 3 !important;\n }\n .order-xxl-4 {\n order: 4 !important;\n }\n .order-xxl-5 {\n order: 5 !important;\n }\n .order-xxl-last {\n order: 6 !important;\n }\n .m-xxl-0 {\n margin: 0 !important;\n }\n .m-xxl-1 {\n margin: 0.25rem !important;\n }\n .m-xxl-2 {\n margin: 0.5rem !important;\n }\n .m-xxl-3 {\n margin: 1rem !important;\n }\n .m-xxl-4 {\n margin: 1.5rem !important;\n }\n .m-xxl-5 {\n margin: 3rem !important;\n }\n .m-xxl-auto {\n margin: auto !important;\n }\n .mx-xxl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-xxl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-xxl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-xxl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-xxl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-xxl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-xxl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-xxl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-xxl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-xxl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-xxl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-xxl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-xxl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-xxl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-xxl-0 {\n margin-top: 0 !important;\n }\n .mt-xxl-1 {\n margin-top: 0.25rem !important;\n }\n .mt-xxl-2 {\n margin-top: 0.5rem !important;\n }\n .mt-xxl-3 {\n margin-top: 1rem !important;\n }\n .mt-xxl-4 {\n margin-top: 1.5rem !important;\n }\n .mt-xxl-5 {\n margin-top: 3rem !important;\n }\n .mt-xxl-auto {\n margin-top: auto !important;\n }\n .me-xxl-0 {\n margin-right: 0 !important;\n }\n .me-xxl-1 {\n margin-right: 0.25rem !important;\n }\n .me-xxl-2 {\n margin-right: 0.5rem !important;\n }\n .me-xxl-3 {\n margin-right: 1rem !important;\n }\n .me-xxl-4 {\n margin-right: 1.5rem !important;\n }\n .me-xxl-5 {\n margin-right: 3rem !important;\n }\n .me-xxl-auto {\n margin-right: auto !important;\n }\n .mb-xxl-0 {\n margin-bottom: 0 !important;\n }\n .mb-xxl-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-xxl-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-xxl-3 {\n margin-bottom: 1rem !important;\n }\n .mb-xxl-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-xxl-5 {\n margin-bottom: 3rem !important;\n }\n .mb-xxl-auto {\n margin-bottom: auto !important;\n }\n .ms-xxl-0 {\n margin-left: 0 !important;\n }\n .ms-xxl-1 {\n margin-left: 0.25rem !important;\n }\n .ms-xxl-2 {\n margin-left: 0.5rem !important;\n }\n .ms-xxl-3 {\n margin-left: 1rem !important;\n }\n .ms-xxl-4 {\n margin-left: 1.5rem !important;\n }\n .ms-xxl-5 {\n margin-left: 3rem !important;\n }\n .ms-xxl-auto {\n margin-left: auto !important;\n }\n .p-xxl-0 {\n padding: 0 !important;\n }\n .p-xxl-1 {\n padding: 0.25rem !important;\n }\n .p-xxl-2 {\n padding: 0.5rem !important;\n }\n .p-xxl-3 {\n padding: 1rem !important;\n }\n .p-xxl-4 {\n padding: 1.5rem !important;\n }\n .p-xxl-5 {\n padding: 3rem !important;\n }\n .px-xxl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-xxl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-xxl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-xxl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-xxl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-xxl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-xxl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-xxl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-xxl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-xxl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-xxl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-xxl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-xxl-0 {\n padding-top: 0 !important;\n }\n .pt-xxl-1 {\n padding-top: 0.25rem !important;\n }\n .pt-xxl-2 {\n padding-top: 0.5rem !important;\n }\n .pt-xxl-3 {\n padding-top: 1rem !important;\n }\n .pt-xxl-4 {\n padding-top: 1.5rem !important;\n }\n .pt-xxl-5 {\n padding-top: 3rem !important;\n }\n .pe-xxl-0 {\n padding-right: 0 !important;\n }\n .pe-xxl-1 {\n padding-right: 0.25rem !important;\n }\n .pe-xxl-2 {\n padding-right: 0.5rem !important;\n }\n .pe-xxl-3 {\n padding-right: 1rem !important;\n }\n .pe-xxl-4 {\n padding-right: 1.5rem !important;\n }\n .pe-xxl-5 {\n padding-right: 3rem !important;\n }\n .pb-xxl-0 {\n padding-bottom: 0 !important;\n }\n .pb-xxl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-xxl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-xxl-3 {\n padding-bottom: 1rem !important;\n }\n .pb-xxl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-xxl-5 {\n padding-bottom: 3rem !important;\n }\n .ps-xxl-0 {\n padding-left: 0 !important;\n }\n .ps-xxl-1 {\n padding-left: 0.25rem !important;\n }\n .ps-xxl-2 {\n padding-left: 0.5rem !important;\n }\n .ps-xxl-3 {\n padding-left: 1rem !important;\n }\n .ps-xxl-4 {\n padding-left: 1.5rem !important;\n }\n .ps-xxl-5 {\n padding-left: 3rem !important;\n }\n .gap-xxl-0 {\n gap: 0 !important;\n }\n .gap-xxl-1 {\n gap: 0.25rem !important;\n }\n .gap-xxl-2 {\n gap: 0.5rem !important;\n }\n .gap-xxl-3 {\n gap: 1rem !important;\n }\n .gap-xxl-4 {\n gap: 1.5rem !important;\n }\n .gap-xxl-5 {\n gap: 3rem !important;\n }\n .row-gap-xxl-0 {\n row-gap: 0 !important;\n }\n .row-gap-xxl-1 {\n row-gap: 0.25rem !important;\n }\n .row-gap-xxl-2 {\n row-gap: 0.5rem !important;\n }\n .row-gap-xxl-3 {\n row-gap: 1rem !important;\n }\n .row-gap-xxl-4 {\n row-gap: 1.5rem !important;\n }\n .row-gap-xxl-5 {\n row-gap: 3rem !important;\n }\n .column-gap-xxl-0 {\n -moz-column-gap: 0 !important;\n column-gap: 0 !important;\n }\n .column-gap-xxl-1 {\n -moz-column-gap: 0.25rem !important;\n column-gap: 0.25rem !important;\n }\n .column-gap-xxl-2 {\n -moz-column-gap: 0.5rem !important;\n column-gap: 0.5rem !important;\n }\n .column-gap-xxl-3 {\n -moz-column-gap: 1rem !important;\n column-gap: 1rem !important;\n }\n .column-gap-xxl-4 {\n -moz-column-gap: 1.5rem !important;\n column-gap: 1.5rem !important;\n }\n .column-gap-xxl-5 {\n -moz-column-gap: 3rem !important;\n column-gap: 3rem !important;\n }\n .text-xxl-start {\n text-align: left !important;\n }\n .text-xxl-end {\n text-align: right !important;\n }\n .text-xxl-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1200px) {\n .fs-1 {\n font-size: 2.5rem !important;\n }\n .fs-2 {\n font-size: 2rem !important;\n }\n .fs-3 {\n font-size: 1.75rem !important;\n }\n .fs-4 {\n font-size: 1.5rem !important;\n }\n}\n@media print {\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-grid {\n display: grid !important;\n }\n .d-print-inline-grid {\n display: inline-grid !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n .d-print-none {\n display: none !important;\n }\n}\n\n/*# sourceMappingURL=bootstrap.css.map */","// stylelint-disable scss/dimension-no-non-numeric-values\n\n// SCSS RFS mixin\n//\n// Automated responsive values for font sizes, paddings, margins and much more\n//\n// Licensed under MIT (https://github.com/twbs/rfs/blob/main/LICENSE)\n\n// Configuration\n\n// Base value\n$rfs-base-value: 1.25rem !default;\n$rfs-unit: rem !default;\n\n@if $rfs-unit != rem and $rfs-unit != px {\n @error \"`#{$rfs-unit}` is not a valid unit for $rfs-unit. Use `px` or `rem`.\";\n}\n\n// Breakpoint at where values start decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n@if $rfs-breakpoint-unit != px and $rfs-breakpoint-unit != em and $rfs-breakpoint-unit != rem {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n}\n\n// Resize values based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != number or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Mode. Possibilities: \"min-media-query\", \"max-media-query\"\n$rfs-mode: min-media-query !default;\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-rfs to false\n$enable-rfs: true !default;\n\n// Cache $rfs-base-value unit\n$rfs-base-value-unit: unit($rfs-base-value);\n\n@function divide($dividend, $divisor, $precision: 10) {\n $sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1);\n $dividend: abs($dividend);\n $divisor: abs($divisor);\n @if $dividend == 0 {\n @return 0;\n }\n @if $divisor == 0 {\n @error \"Cannot divide by 0\";\n }\n $remainder: $dividend;\n $result: 0;\n $factor: 10;\n @while ($remainder > 0 and $precision >= 0) {\n $quotient: 0;\n @while ($remainder >= $divisor) {\n $remainder: $remainder - $divisor;\n $quotient: $quotient + 1;\n }\n $result: $result * 10 + $quotient;\n $factor: $factor * .1;\n $remainder: $remainder * 10;\n $precision: $precision - 1;\n @if ($precision < 0 and $remainder >= $divisor * 5) {\n $result: $result + 1;\n }\n }\n $result: $result * $factor * $sign;\n $dividend-unit: unit($dividend);\n $divisor-unit: unit($divisor);\n $unit-map: (\n \"px\": 1px,\n \"rem\": 1rem,\n \"em\": 1em,\n \"%\": 1%\n );\n @if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) {\n $result: $result * map-get($unit-map, $dividend-unit);\n }\n @return $result;\n}\n\n// Remove px-unit from $rfs-base-value for calculations\n@if $rfs-base-value-unit == px {\n $rfs-base-value: divide($rfs-base-value, $rfs-base-value * 0 + 1);\n}\n@else if $rfs-base-value-unit == rem {\n $rfs-base-value: divide($rfs-base-value, divide($rfs-base-value * 0 + 1, $rfs-rem-value));\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == px {\n $rfs-breakpoint: divide($rfs-breakpoint, $rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == rem or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: divide($rfs-breakpoint, divide($rfs-breakpoint * 0 + 1, $rfs-rem-value));\n}\n\n// Calculate the media query value\n$rfs-mq-value: if($rfs-breakpoint-unit == px, #{$rfs-breakpoint}px, #{divide($rfs-breakpoint, $rfs-rem-value)}#{$rfs-breakpoint-unit});\n$rfs-mq-property-width: if($rfs-mode == max-media-query, max-width, min-width);\n$rfs-mq-property-height: if($rfs-mode == max-media-query, max-height, min-height);\n\n// Internal mixin used to determine which media query needs to be used\n@mixin _rfs-media-query {\n @if $rfs-two-dimensional {\n @if $rfs-mode == max-media-query {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}), (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) and (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) {\n @content;\n }\n }\n}\n\n// Internal mixin that adds disable classes to the selector if needed.\n@mixin _rfs-rule {\n @if $rfs-class == disable and $rfs-mode == max-media-query {\n // Adding an extra class increases specificity, which prevents the media query to override the property\n &,\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @else if $rfs-class == enable and $rfs-mode == min-media-query {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Internal mixin that adds enable classes to the selector if needed.\n@mixin _rfs-media-query-rule {\n\n @if $rfs-class == enable {\n @if $rfs-mode == min-media-query {\n @content;\n }\n\n @include _rfs-media-query () {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n }\n }\n @else {\n @if $rfs-class == disable and $rfs-mode == min-media-query {\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @include _rfs-media-query () {\n @content;\n }\n }\n}\n\n// Helper function to get the formatted non-responsive value\n@function rfs-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: \"\";\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + \" 0\";\n }\n @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n @if $unit == px {\n // Convert to rem if needed\n $val: $val + \" \" + if($rfs-unit == rem, #{divide($value, $value * 0 + $rfs-rem-value)}rem, $value);\n }\n @else if $unit == rem {\n // Convert to px if needed\n $val: $val + \" \" + if($rfs-unit == px, #{divide($value, $value * 0 + 1) * $rfs-rem-value}px, $value);\n } @else {\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n $val: $val + \" \" + $value;\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// Helper function to get the responsive value calculated by RFS\n@function rfs-fluid-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: \"\";\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + \" 0\";\n } @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $unit or $unit != px and $unit != rem {\n $val: $val + \" \" + $value;\n } @else {\n // Remove unit from $value for calculations\n $value: divide($value, $value * 0 + if($unit == px, 1, divide(1, $rfs-rem-value)));\n\n // Only add the media query if the value is greater than the minimum value\n @if abs($value) <= $rfs-base-value or not $enable-rfs {\n $val: $val + \" \" + if($rfs-unit == rem, #{divide($value, $rfs-rem-value)}rem, #{$value}px);\n }\n @else {\n // Calculate the minimum value\n $value-min: $rfs-base-value + divide(abs($value) - $rfs-base-value, $rfs-factor);\n\n // Calculate difference between $value and the minimum value\n $value-diff: abs($value) - $value-min;\n\n // Base value formatting\n $min-width: if($rfs-unit == rem, #{divide($value-min, $rfs-rem-value)}rem, #{$value-min}px);\n\n // Use negative value if needed\n $min-width: if($value < 0, -$min-width, $min-width);\n\n // Use `vmin` if two-dimensional is enabled\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{divide($value-diff * 100, $rfs-breakpoint)}#{$variable-unit};\n\n // Return the calculated value\n $val: $val + \" calc(\" + $min-width + if($value < 0, \" - \", \" + \") + $variable-width + \")\";\n }\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// RFS mixin\n@mixin rfs($values, $property: font-size) {\n @if $values != null {\n $val: rfs-value($values);\n $fluid-val: rfs-fluid-value($values);\n\n // Do not print the media query if responsive & non-responsive values are the same\n @if $val == $fluid-val {\n #{$property}: $val;\n }\n @else {\n @include _rfs-rule () {\n #{$property}: if($rfs-mode == max-media-query, $val, $fluid-val);\n\n // Include safari iframe resize fix if needed\n min-width: if($rfs-safari-iframe-resize-bug-fix, (0 * 1vw), null);\n }\n\n @include _rfs-media-query-rule () {\n #{$property}: if($rfs-mode == max-media-query, $fluid-val, $val);\n }\n }\n }\n}\n\n// Shorthand helper mixins\n@mixin font-size($value) {\n @include rfs($value);\n}\n\n@mixin padding($value) {\n @include rfs($value, padding);\n}\n\n@mixin padding-top($value) {\n @include rfs($value, padding-top);\n}\n\n@mixin padding-right($value) {\n @include rfs($value, padding-right);\n}\n\n@mixin padding-bottom($value) {\n @include rfs($value, padding-bottom);\n}\n\n@mixin padding-left($value) {\n @include rfs($value, padding-left);\n}\n\n@mixin margin($value) {\n @include rfs($value, margin);\n}\n\n@mixin margin-top($value) {\n @include rfs($value, margin-top);\n}\n\n@mixin margin-right($value) {\n @include rfs($value, margin-right);\n}\n\n@mixin margin-bottom($value) {\n @include rfs($value, margin-bottom);\n}\n\n@mixin margin-left($value) {\n @include rfs($value, margin-left);\n}\n","// scss-docs-start color-mode-mixin\n@mixin color-mode($mode: light, $root: false) {\n @if $color-mode-type == \"media-query\" {\n @if $root == true {\n @media (prefers-color-scheme: $mode) {\n :root {\n @content;\n }\n }\n } @else {\n @media (prefers-color-scheme: $mode) {\n @content;\n }\n }\n } @else {\n [data-bs-theme=\"#{$mode}\"] {\n @content;\n }\n }\n}\n// scss-docs-end color-mode-mixin\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n @include font-size(var(--#{$prefix}root-font-size));\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$prefix}body-font-family);\n @include font-size(var(--#{$prefix}body-font-size));\n font-weight: var(--#{$prefix}body-font-weight);\n line-height: var(--#{$prefix}body-line-height);\n color: var(--#{$prefix}body-color);\n text-align: var(--#{$prefix}body-text-align);\n background-color: var(--#{$prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n border: 0;\n border-top: $hr-border-width solid $hr-border-color;\n opacity: $hr-opacity;\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: var(--#{$prefix}heading-color);\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 2. Add explicit cursor to indicate changed behavior.\n// 3. Prevent the text-decoration to be skipped.\n\nabbr[title] {\n text-decoration: underline dotted; // 1\n cursor: help; // 2\n text-decoration-skip-ink: none; // 3\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n color: var(--#{$prefix}highlight-color);\n background-color: var(--#{$prefix}highlight-bg);\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: rgba(var(--#{$prefix}link-color-rgb), var(--#{$prefix}link-opacity, 1));\n text-decoration: $link-decoration;\n\n &:hover {\n --#{$prefix}link-color-rgb: var(--#{$prefix}link-hover-color-rgb);\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: var(--#{$prefix}code-color);\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-` + + + `; + + toastContainer.insertAdjacentHTML('beforeend', toastHTML); + const toastElement = document.getElementById(toastId); + const toast = new bootstrap.Toast(toastElement); + toast.show(); + + // Elimina el toast del DOM cuando se oculta + toastElement.addEventListener('hidden.bs.toast', () => { + toastElement.remove(); + }); +} + diff --git a/static/openapi.yaml b/static/openapi.yaml new file mode 100644 index 0000000..70644ea --- /dev/null +++ b/static/openapi.yaml @@ -0,0 +1,983 @@ +openapi: 3.0.0 +info: + title: Pastebin API + version: 1.1.0 + description: An API for managing and sharing pastes. +servers: + - url: https://pastebin.mydomain.com + description: Production Server + - url: http://localhost:5000 + description: Local Server +paths: + /login: + post: + summary: User login + description: Authenticate a user and start a session. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + password: + type: string + required: + - username + - password + responses: + '200': + description: Login successful + '401': + description: Invalid credentials + + /logout: + get: + summary: Logout the current user + description: Logs out the authenticated user. + responses: + '200': + description: User logged out successfully. + + /register: + post: + summary: Register a new user + description: Allows an admin to register a new user. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + password: + type: string + required: + - username + - password + responses: + '201': + description: User registered successfully + '400': + description: Invalid input + '403': + description: Unauthorized access + /paste: + post: + summary: Create a new paste + description: Upload a new paste, either as text or a file. The paste can be private, expire in 1 day, or be permanent. + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + c: + type: string + format: binary + description: The content or file to upload + lang: + type: string + description: Programming language for syntax highlighting + expire: + type: string + enum: [yes, no] + default: yes + description: > + Whether the paste should expire after 1 day. + - `yes` (default): The paste will expire in 24 hours. + - `no`: The paste will be permanent. + private: + type: string + enum: [yes, no] + default: no + description: > + Whether the paste should be private. + - `yes`: The paste will only be accessible to the creator and shared users. + - `no` (default): The paste will be public. + responses: + '201': + description: Paste created successfully + content: + application/json: + schema: + type: object + properties: + url: + type: string + description: The URL of the created paste + '400': + description: Missing content or invalid data + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '403': + description: Unauthorized access or user exceeded quota + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Authorization error message + + /paste/{id}: + get: + summary: Retrieve a paste by ID + description: Fetches the paste content if accessible to the user. + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Paste retrieved successfully + content: + application/json: + schema: + type: object + properties: + id: + type: integer + filename: + type: string + language: + type: string + content_type: + type: string + size: + type: integer + created_at: + type: string + format: date-time + '403': + description: Unauthorized access + '404': + description: Paste not found + + /paste/{id}/raw: + get: + summary: Get raw paste content + description: Retrieve the raw content of a paste, either as plain text or binary. + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Raw content retrieved successfully + '404': + description: Paste not found + + /pastes: + get: + summary: List user pastes + description: Retrieve a list of all pastes created by the authenticated user. + responses: + '200': + description: List of pastes retrieved successfully + '401': + description: Unauthorized access + + /stats: + get: + summary: Paste Statistics + description: Returns either an HTML page with visual statistics or JSON data based on the query parameter `format`. + parameters: + - name: format + in: query + required: false + schema: + type: string + enum: [html, json] + default: html + responses: + '200': + description: Statistics response + '400': + description: Invalid query parameters + + /user/details: + get: + summary: Get current user details + description: Retrieves the current user's role, storage used, and remaining storage. + responses: + '200': + description: User details retrieved successfully + '401': + description: Unauthorized access + + /change-password: + post: + summary: Change user password + description: Allows the authenticated user to change their password. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + current_password: + type: string + new_password: + type: string + confirm_password: + type: string + required: + - current_password + - new_password + - confirm_password + responses: + '200': + description: Password updated successfully + '400': + description: Validation errors + '401': + description: Unauthorized access or incorrect current password + + /user/{username}/stats: + get: + summary: Get user statistics + description: Retrieve statistics about a user's pastes. + parameters: + - name: username + in: path + required: true + schema: + type: string + - name: start_date + in: query + schema: + type: string + format: date + - name: end_date + in: query + schema: + type: string + format: date + responses: + '200': + description: User statistics retrieved successfully + '404': + description: User not found + + /favorites: + get: + summary: List user favorites + description: Retrieve a list of all favorites for the authenticated user. + responses: + '200': + description: Favorites retrieved successfully + '401': + description: Unauthorized access + + /paste/{id}/favorite: + post: + summary: Add a paste to favorites + description: Adds the specified paste to the authenticated user's favorites. + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Paste added to favorites + '404': + description: Paste not found + + /paste/{id}/unfavorite: + post: + summary: Remove a paste from favorites + description: Removes the specified paste from the authenticated user's favorites. + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Paste removed from favorites + '404': + description: Paste not in favorites + + /pastes/search: + get: + summary: Search pastes + description: Search for pastes by content, content type, and language. + security: + - bearerAuth: [] + parameters: + - name: q + in: query + required: true + schema: + type: string + description: Search query to match paste content. + - name: content_type + in: query + required: false + schema: + type: string + description: Filter by the MIME type of the paste. + - name: language + in: query + required: false + schema: + type: string + description: Filter by the programming language of the paste. + responses: + '200': + description: Search results retrieved successfully. + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + description: Unique ID of the paste. + title: + type: string + description: Title of the paste. + content: + type: string + description: Content of the paste (truncated for large pastes). + language: + type: string + description: Programming language of the paste. + content_type: + type: string + description: MIME type of the paste. + created_at: + type: string + format: date-time + description: Creation date of the paste. + '400': + description: Invalid search query or missing parameters. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '401': + description: Unauthorized access. + content: + application/json: + schema: + type: object + properties: + error: + type: string + '500': + description: Server error while processing the search. + content: + application/json: + schema: + type: object + properties: + error: + type: string + /api/paste/{id}/favorite: + post: + summary: Add a paste to favorites from terminal + description: Marks the specified paste as a favorite for the authenticated user. + parameters: + - name: id + in: path + required: true + description: ID of the paste to mark as favorite. + schema: + type: integer + responses: + '201': + description: Paste successfully added to favorites. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Paste added to favorites" + '400': + description: Bad request + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Paste already in favorites" + '401': + description: Unauthorized + '404': + description: Paste not found + '500': + description: Server error + /api/paste/{id}/unfavorite: + post: + summary: Remove a paste from favorites from terminal + description: Removes the specified paste from the authenticated user's favorites. + parameters: + - name: id + in: path + required: true + description: ID of the paste to remove from favorites. + schema: + type: integer + responses: + '200': + description: Paste successfully removed from favorites. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Paste removed from favorites" + '400': + description: Bad request + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Paste not in favorites" + '401': + description: Unauthorized + '404': + description: Paste not found + '500': + description: Server error + /api/paste/{id}/download: + get: + summary: Download a paste + description: Downloads the file associated with the specified paste. The server returns the original filename in the `Content-Disposition` header for proper file naming. + parameters: + - name: id + in: path + required: true + description: ID of the paste to download. + schema: + type: integer + responses: + '200': + description: Successfully downloads the file. + headers: + Content-Disposition: + description: Specifies the filename for the downloaded file. + schema: + type: string + example: attachment; filename="example.txt" + content: + application/octet-stream: + schema: + type: string + format: binary + '400': + description: Bad request + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Invalid paste ID" + '401': + description: Unauthorized + '403': + description: Forbidden + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "You do not have permission to download this file" + '404': + description: Paste not found + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "File not found" + '500': + description: Internal server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Unexpected server error" + /api/favorites: + get: + summary: List user favorites + description: Retrieve a list of all favorites for the authenticated user. + responses: + '200': + description: Favorites retrieved successfully + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer + description: Unique ID of the paste + url: + type: string + description: URL of the paste + title: + type: string + description: Title or filename of the paste + type: + type: string + description: Type of the paste + size: + type: integer + description: Size of the paste in bytes + created_at: + type: string + format: date-time + description: Creation date of the paste + '401': + description: Unauthorized access + '500': + description: Error retrieving favorites + /api/shared_with_others: + get: + summary: List pastes shared with others + description: Retrieve a list of pastes that the authenticated user has shared with others. + responses: + '200': + description: Pastes shared with others retrieved successfully. + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer + description: Unique ID of the paste. + title: + type: string + description: Title of the paste or filename if title is missing. + shared_with: + type: array + description: List of usernames the paste is shared with. + items: + type: string + can_edit: + type: boolean + description: Indicates if the shared user can edit the paste. + created_at: + type: string + format: date-time + description: Creation date of the paste. + '401': + description: Unauthorized access. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Authorization token is missing or invalid." + '500': + description: Server error. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Unexpected server error." + /api/shared_with_me: + get: + summary: List pastes shared with the user + description: Retrieve a list of pastes that have been shared with the authenticated user. + responses: + '200': + description: Pastes shared with the user retrieved successfully. + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer + description: Unique ID of the paste. + title: + type: string + description: Title of the paste or filename if title is missing. + owner: + type: string + description: Username of the owner who shared the paste. + can_edit: + type: boolean + description: Indicates if the user has edit permissions for the paste. + created_at: + type: string + format: date-time + description: Creation date of the paste. + '401': + description: Unauthorized access. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Authorization token is missing or invalid." + '500': + description: Server error. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Unexpected server error." + /api/paste/{id}/unshare: + post: + summary: Unshare a paste + description: Remove the shared permission of a paste for a specific user. + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: ID of the paste to unshare. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + description: The username of the user to unshare the paste with. + required: + - username + responses: + '200': + description: Paste successfully unshared. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Paste successfully unshared with username." + '400': + description: Invalid request or paste is not shared with the user. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Paste is not shared with username." + '404': + description: Paste or user not found. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Paste not found or user not found." + '500': + description: Internal server error. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "An unexpected error occurred." + details: + type: string + example: "Detailed error message." + /api/paste/{id}: + put: + summary: Update existing paste content + description: Overwrites the content of an existing paste with new data, assuming the user has edit permission. + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: ID of the paste to update. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + content: + type: string + description: The new content for the paste. + required: + - content + responses: + '200': + description: Paste updated successfully. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Paste updated successfully" + '400': + description: Missing or invalid content in the request body. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Missing 'content' in JSON" + '403': + description: The user does not have permission to edit this paste. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "No permission to edit this paste." + '404': + description: Paste not found. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Paste not found" + '500': + description: Server error. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "An unexpected error occurred." + details: + type: string + example: "Detailed traceback or error message." + /api/paste/{id}/share: + post: + summary: Share a paste with another user + description: Share a specific paste with another user, optionally granting them edit permissions. + parameters: + - name: id + in: path + required: true + description: ID of the paste to be shared. + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + description: Username of the recipient. + example: "test_user" + can_edit: + type: boolean + description: Indicates whether the recipient can edit the paste. + example: true + required: + - username + responses: + '200': + description: Paste shared successfully. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Paste shared successfully with test_user." + can_edit: + type: boolean + example: true + '400': + description: Bad request, e.g., if the paste is already shared with the user or invalid data is provided. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Paste is already shared with test_user." + '403': + description: Forbidden, e.g., if the paste does not belong to the authenticated user. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "You do not own this paste." + '404': + description: Paste or recipient not found. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "User test_user not found." + '500': + description: Internal server error. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "An unexpected error occurred." + details: + type: string + example: "Detailed error message for debugging." + /api/removegps: + post: + summary: Remove GPS metadata from an image + description: Removes GPS metadata from an image file owned by the authenticated user. + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + paste_id: + type: integer + description: ID of the paste containing the image. + required: + - paste_id + responses: + '200': + description: GPS metadata successfully removed. + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + '400': + description: Missing or invalid paste_id. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Missing paste_id" + '403': + description: User does not have permission to modify this paste. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "You do not have permission to modify this file" + '404': + description: Paste or file not found. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Paste not found" + '500': + description: Internal server error. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Error removing GPS metadata" + diff --git a/templates/add_user.html b/templates/add_user.html new file mode 100644 index 0000000..e28f893 --- /dev/null +++ b/templates/add_user.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% block content %} +

+{% endblock %} + diff --git a/templates/admin_login.html b/templates/admin_login.html new file mode 100644 index 0000000..cc0c497 --- /dev/null +++ b/templates/admin_login.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% block content %} +
+
+

Admin Login

+
+
+ + +
+
+ + +
+ +
+
+
+{% endblock %} + diff --git a/templates/anonymous_stats.html b/templates/anonymous_stats.html new file mode 100644 index 0000000..24a33fb --- /dev/null +++ b/templates/anonymous_stats.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} + +{% block content %} +
+

Pastes Statistics

+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + +
+
+
+
Total Pastes
+
+
{{ total_pastes }}
+
+
+
+
+
+
Text Pastes
+
+
{{ total_text_pastes }}
+
+
+
+
+
+
File Pastes
+
+
{{ total_file_pastes }}
+
+
+
+
+
+
Media Pastes
+
+
{{ total_media_pastes }}
+
+
+
+
+ + +
+
+

Pastes by Language and Type

+ +
+
+
+ + + + + + + + + + +{% endblock %} +{% block footer %} + {{ super() }} +{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..9df73df --- /dev/null +++ b/templates/base.html @@ -0,0 +1,289 @@ + + + + + + + + + + + + + + + + + + + + + + + + {% block title %}My Pastebin{% endblock %} + + + + + + + + + + + {% for style in pygments_styles %} + + {% endfor %} + + + {% block head %} + + {% endblock %} + + + + + + + + + + + + +
+ {% block content %}{% endblock %} +
+ + +
+ +
+ + +
+ + + + + {% block scripts %}{% endblock %} + + + + + + + + + + + + + + + +{% if load_tui_editor %} + + + + + + +{% endif %} + + + + + +
+

+ This project is licensed under the + + BSD 2-Clause License + . +

+

+ This website does not claim ownership of, copyright on, + and assumes no liability for provided content. +

+
+ + diff --git a/templates/binary_view.html b/templates/binary_view.html new file mode 100644 index 0000000..5333169 --- /dev/null +++ b/templates/binary_view.html @@ -0,0 +1,110 @@ +{% extends "base.html" %} + +{% block title %}Binary file viewer{% endblock %} + +{% block content %} +
+
+
+

Viewing File: {{ filename }}

+

Tipo MIME: {{ mime_type }}

+ +
+
+

File Info

+
    +
  • File Name: {{ filename }}
  • +
  • Size: {{ size }} bytes
  • +
  • Uploaded by: {{ owner.username }}
  • +
  • Upload Date: {{ paste.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
  • +
+
+
+

Actions

+
+ + + + + + {% if current_user.is_authenticated %} + + {% endif %} +
+
+
+ +
+
+
Download from smartphone
+

Scan this code from your mobile device:

+
+ +
+
+
+
+
+ +{% if 'admin' in session %} + +{% endif %} + +
+ +
+{% endblock %} + + + +{% block scripts %} +{{ super() }} +{% endblock %} + +{% block footer %} + {{ super() }} +{% endblock %} + diff --git a/templates/binary_view.html_backup b/templates/binary_view.html_backup new file mode 100644 index 0000000..ee7c713 --- /dev/null +++ b/templates/binary_view.html_backup @@ -0,0 +1,110 @@ +{% extends "base.html" %} + +{% block title %}Binary file viewer{% endblock %} + +{% block content %} +
+
+
+

Viewing File: {{ filename }}

+

Tipo MIME: {{ mime_type }}

+ +
+
+

File Info

+
    +
  • File Name: {{ filename }}
  • +
  • Size: {{ size }} bytes
  • +
  • Uploaded by: {{ owner.username }}
  • +
  • Upload Date: {{ paste.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
  • +
+
+
+

Acciones

+
+ + Descargar Archivo + + {% if 'admin' in session %} + + {% endif %} +
+
+
+ +
+
+
Download from smartphone
+

Scan this code from your mobile device:

+
+ +
+
+
+
+
+ + {% if 'admin' in session %} + +{% endif %} +{% endblock %} + +{% block scripts %} + +{% endblock %} + diff --git a/templates/change_password.html b/templates/change_password.html new file mode 100644 index 0000000..3117842 --- /dev/null +++ b/templates/change_password.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block title %}Change Password{% endblock %} + +{% block content %} +
+

Change Your Password

+
+
+ + +
+
+ + +
+
+ + +
+ + Cancel +
+
+{% endblock %} + + diff --git a/templates/contact.html b/templates/contact.html new file mode 100644 index 0000000..5d6d759 --- /dev/null +++ b/templates/contact.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% block title %}Contact Us{% endblock %} + +{% block content %} +
+

Contact Us

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+{% endblock %} +{% block footer %} + {{ super() }} +{% endblock %} diff --git a/templates/create_paste.html b/templates/create_paste.html new file mode 100644 index 0000000..3f52cf5 --- /dev/null +++ b/templates/create_paste.html @@ -0,0 +1,51 @@ + + +{% extends "base.html" %} + +{% block title %}Crear Nuevo Paste{% endblock %} + +{% block content %} +
+

Crear Nuevo Paste

+
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+{% endblock %} + diff --git a/templates/create_paste_web.html b/templates/create_paste_web.html new file mode 100644 index 0000000..749d4b6 --- /dev/null +++ b/templates/create_paste_web.html @@ -0,0 +1,165 @@ +{% extends "base.html" %} + +{% block title %}Create New Paste{% endblock %} + +{% block content %} + + +
+

Create New Paste

+ + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + {% set bg_class = 'danger' if category == 'error' else 'success' %} + + {% endfor %} + {% endif %} + {% endwith %} +
+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ +
+ + + + + +
+ + +
+ + + +
+
+{% endblock %} + +{% block scripts %} + + + + +{% endblock %} + +{% block footer %} + {{ super() }} +{% endblock %} + diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..0916a5b --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% block content %} +
+

Admin Dashboard

+
+
+
+
+
+
Users
+

Total: {{ users_count }}

+ Manage Users +
+
+
+
+
+
+
Pastes
+

Total: {{ pastes_count }}

+ Manage Pastes +
+
+
+
+{% endblock %} + diff --git a/templates/download_page.html b/templates/download_page.html new file mode 100644 index 0000000..9c23fa0 --- /dev/null +++ b/templates/download_page.html @@ -0,0 +1,43 @@ + +{% extends "base.html" %} + +{% block title %}Download File {{ paste.id }}{% endblock %} + +{% block content %} +
+

Descargar Paste {{ paste.id }}

+

You are about to download the following file:

+
    +
  • Content Type: {{ paste.content_type }}
  • +
  • Tamaño: {{ paste.size }} bytes
  • + {% if paste.filename %} +
  • File Name: {{ paste.filename }}
  • + {% endif %} +
+ + + Cancel + + + +
+{% endblock %} + diff --git a/templates/edit_paste.html b/templates/edit_paste.html new file mode 100644 index 0000000..02046e1 --- /dev/null +++ b/templates/edit_paste.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} + +{% block title %}Edit Paste {{ paste.id }}{% endblock %} + +{% block content %} +
+

Editar Paste {{ paste.id }}

+
+ +
+ + Cancelar +
+
+{% endblock %} + +{% block scripts %} + + +{% endblock %} +{% block footer %} + {{ super() }} +{% endblock %} diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..a8ae870 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,18 @@ + + + + + + + Error + + + +
+

Error

+

{{ message }}

+ Volver +
+ + + diff --git a/templates/errors/403.html b/templates/errors/403.html new file mode 100644 index 0000000..0767979 --- /dev/null +++ b/templates/errors/403.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block title %}403 - Forbidden{% endblock %} + +{% block content %} +
+

403

+

You do not have permission to view this paste.

+

If you believe this is a mistake, please contact support.

+ + {% if current_user.is_authenticated %} + Back to Dashboard + {% else %} + Login + {% endif %} +
+{% endblock %} + diff --git a/templates/errors/404.html b/templates/errors/404.html new file mode 100644 index 0000000..313870b --- /dev/null +++ b/templates/errors/404.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block title %}404 - Not Found{% endblock %} + +{% block content %} +
+

404

+

This paste has been deleted or has expired.

+

If you believe this is a mistake, please contact support.

+ + {% if current_user.is_authenticated %} + Back to Dashboard + {% endif %} + +
+{% endblock %} + diff --git a/templates/errors/500.html b/templates/errors/500.html new file mode 100644 index 0000000..c883bfb --- /dev/null +++ b/templates/errors/500.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} + +{% block title %}Server Error{% endblock %} + +{% block content %} +
+

500 - Internal Server Error

+

Something went wrong on our end. Please try again later.

+ Back to Dashboard +
+{% endblock %} + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..d923d42 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,237 @@ +{% extends "base.html" %} +{% block content %} +
+
+
+

Welcome to My Pastebin

+

Easily create, share, and manage your pastes with syntax highlighting and powerful APIs.

+
+
+ + + +
+ +
+
+
+

Bash Client

+

The Bash client simplifies interaction with the service. Download it here:

+

+ Download Bash Client | + GitLab Repository +

+

Examples

+
    +
  • Authentication: +
    ./pastebin_client.sh login admin password123
    +
  • Create a Paste (with expiration and privacy): +
    echo "Hello World" | ./pastebin_client.sh create plaintext yes no
    +

    Creates a paste in plaintext format that expires in 1 day and is public. + Use yes for a private paste.

    +
  • + +
  • Upload a File (with expiration and privacy): +
    ./pastebin_client.sh upload script.py python yes no
    +

    Uploads script.py as a Python paste that expires in 1 day and is public. + Use yes as the last argument for a private paste.

    +
  • + + +
  • View a Paste: +
    ./pastebin_client.sh view 1
    +
  • +
  • View Raw Content: +
    ./pastebin_client.sh view_raw 1
    +
  • +
  • List Pastes: +
    ./pastebin_client.sh list
    +
  • +
  • Delete a Paste: +
    ./pastebin_client.sh delete 1
    +
  • +
  • Search Paste Contents: +
    ./pastebin_client.sh search "search term"
    +
  • +
  • User Details: +
    ./pastebin_client.sh details
    +
  • +
  • Mark a Paste as Favorite: +
    ./pastebin_client.sh favorite 1
    +
  • +
  • Remove a Paste from Favorites: +
    ./pastebin_client.sh unfavorite 1
    +
  • +
  • Download a Paste: +
    ./pastebin_client.sh download 1
    +
  • +
  • List Favorites: +
    ./pastebin_client.sh list_favorites
    +
  • +
  • List Pastes Shared With Others: +
    ./pastebin_client.sh shared_with_others
    +
  • +
  • List Pastes Shared With Me: +
    ./pastebin_client.sh shared_with_me
    +
  • +
  • Share a Paste: +
    ./pastebin_client.sh share 1 username true
    +

    Shares paste with ID 1 with username, allowing edit if true is passed.

    +
  • +
  • Unshare a Paste: +
    ./pastebin_client.sh unshare 1 test
    +
  • +
  • Edit a Paste (via Editor): +
    ./pastebin_client.sh edit 1
    +

    Opens paste with ID 1 in your default editor. Upon saving and exiting, the updated content is sent to the server.

    +
  • +
  • Remove GPS Metadata from an Image: +
    ./pastebin_client.sh remove_gps 1
    +

    Removes GPS metadata from the image associated with paste ID 1.

    +
  • + + +
+
+
+
+ +
+
+
+

Curl Examples

+

Interact directly with the service using curl.

+
    +
  • Authentication: +
    curl -X POST {{ request.host_url }}api/token \
    +                -H "Content-Type: application/json" \
    +                -d '{"username":"admin","password":"password123"}'
    +
  • +
  • Create a Paste (with expiration and privacy): +
    echo "Hello World" | curl -X POST {{ request.host_url }}paste \
    +        -H "Authorization: Bearer <YOUR_TOKEN>" \
    +        -F "c=@-" \
    +        -F "expire=yes" \
    +        -F "private=no"
    +

    Creates a paste that expires in 1 day and is public. + Use private=yes to make it private.

    +
  • + +
  • Upload a File (with expiration and privacy): +
    curl -X POST {{ request.host_url }}paste \
    +        -H "Authorization: Bearer <YOUR_TOKEN>" \
    +        -F "c=@script.py" \
    +        -F "expire=yes" \
    +        -F "private=no"
    +

    Uploads script.py that expires in 1 day and is public. + Use private=yes to make it private.

    +
  • + +
  • View a Paste: +
    curl {{ request.host_url }}paste/1/json \
    +-H "Authorization: Bearer <YOUR_TOKEN>"
    +
  • +
  • View Raw Content: +
    curl {{ request.host_url }}paste/1/raw \
    +-H "Authorization: Bearer <YOUR_TOKEN>"
    +
  • +
  • List Pastes: +
    curl -H "Authorization: Bearer <YOUR_TOKEN>" {{ request.host_url }}pastes
    +
  • +
  • Delete a Paste: +
    curl -X DELETE {{ request.host_url }}paste/1 \
    +-H "Authorization: Bearer <YOUR_TOKEN>"
    +
  • +
  • Search Paste Contents: +
    curl -X GET {{ request.host_url }}pastes/search?q="search term" \
    +-H "Authorization: Bearer <YOUR_TOKEN>"
    +
  • +
  • User Details: +
    curl -X GET {{ request.host_url }}user/details \
    +-H "Authorization: Bearer <YOUR_TOKEN>"
    +
  • +
  • Mark a Paste as Favorite: +
    curl -X POST {{ request.host_url }}api/paste/1/favorite \
    +-H "Authorization: Bearer <YOUR_TOKEN>"
    +
  • +
  • Remove a Paste from Favorites: +
    curl -X POST {{ request.host_url }}api/paste/1/unfavorite \
    +-H "Authorization: Bearer <YOUR_TOKEN>"
    +
  • +
  • Download a Paste: +
    curl -X GET {{ request.host_url }}api/paste/1/download \
    +-H "Authorization: Bearer <YOUR_TOKEN>" \
    +-J -O
    +
  • +
  • List Favorites: +
    curl -X GET {{ request.host_url }}api/favorites \
    +-H "Authorization: Bearer <YOUR_TOKEN>"
    +
  • +
  • List Pastes Shared With Others: +
    curl -X GET {{ request.host_url }}api/shared_with_others \
    +-H "Authorization: Bearer <YOUR_TOKEN>"
    +
  • +
  • List Pastes Shared With Me: +
    curl -X GET {{ request.host_url }}api/shared_with_me \
    +-H "Authorization: Bearer <YOUR_TOKEN>"
    +
  • +
  • Share a Paste: +
    curl -X POST {{ request.host_url }}api/paste/1/share \
    +-H "Authorization: Bearer <YOUR_TOKEN>" \
    +-H "Content-Type: application/json" \
    +-d '{
    +  "username": "test_user",
    +  "can_edit": true
    +}'
    +

    Shares paste with ID 1 with test_user, allowing edit if can_edit is true.

    +
  • +
  • Unshare a Paste: +
    curl -X POST {{ request.host_url }}api/paste/1/unshare \
    +-H "Authorization: Bearer <YOUR_TOKEN>" \
    +-H "Content-Type: application/json" \
    +-d '{
    +  "username": "test"
    +}'
    +
  • +
  • Edit a Paste: +
    curl -X PUT {{ request.host_url }}api/paste/1 \
    +-H "Authorization: Bearer <YOUR_TOKEN>" \
    +-H "Content-Type: application/json" \
    +-d '{
    +  "content": "Updated content here"
    +}'
    +

    Overwrites the content of paste with ID 1 using JSON. Make sure the authenticated user has edit permission.

    +
  • +
  • Remove GPS Metadata from an Image: +
    curl -X POST {{ request.host_url }}api/removegps \
    +-H "Authorization: Bearer <YOUR_TOKEN>" \
    +-H "Content-Type: application/json" \
    +-d '{
    +  "paste_id": 1
    +}'
    +

    Removes GPS metadata from the image associated with paste ID 1.

    +
  • + + +
+
+
+
+
+ +
+{% endblock %} + {% block footer %} + {{ super() }} +{% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..02c4776 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

{{ 'Admin Login' if admin else 'Login' }}

+
+
+ + +
+
+ + +
+ +
+ {% if not admin %} +

+ Don't have an account? Request one here. +

+ {% endif %} +
+
+{% endblock %} +{% block footer %} + {{ super() }} +{% endblock %} diff --git a/templates/media_view.html b/templates/media_view.html new file mode 100644 index 0000000..c15a366 --- /dev/null +++ b/templates/media_view.html @@ -0,0 +1,188 @@ +{% extends "base.html" %} +{% set load_tui_editor = true %} + +{% block title %}Media Viewer{% endblock %} + +{% block content %} + + + + +
+

Viewing Media: {{ filename }}

+

MIME Type: {{ mime_type }}

+ +
+ {% if mime_type.startswith("image/") %} + Image + {% elif mime_type.startswith("video/") %} + + {% elif mime_type.startswith("audio/") %} + + {% elif mime_type == "application/pdf" %} + + {% endif %} +
+ +
+ + + + + + + + + + {% if current_user.is_authenticated %} + + {% endif %} + + {% if mime_type.startswith("image/") and can_edit and has_gps %} + + {% endif %} +
+ + + +{% if metadata %} + {% for track in metadata %} + {% for key, value in track.items() %} + {% if key == "GPS Data" and value.get("Decimal Coordinates") %} +

📍 Location Preview

+
+ + + {% endif %} + {% endfor %} + {% endfor %} +{% endif %} + +{% if mime_type.startswith("image/") and can_edit %} +
+ + + +
+{% endif %} + +{% endblock %} + +{% block scripts %} + +{{ super() }} + +{% endblock %} + +{% block footer %} + {{ super() }} +{% endblock %} + diff --git a/templates/media_view.html_backup b/templates/media_view.html_backup new file mode 100644 index 0000000..9d1eb24 --- /dev/null +++ b/templates/media_view.html_backup @@ -0,0 +1,73 @@ +{% extends "base.html" %} + +{% block title %}Media Viewer{% endblock %} + +{% block content %} +
+

Viewing Media: {{ filename }}

+

MIME Type: {{ mime_type }}

+ +
+ {% if mime_type.startswith("image/") %} + Image + {% elif mime_type.startswith("video/") %} + + {% elif mime_type.startswith("audio/") %} + + {% elif mime_type == "application/pdf" %} + + {% endif %} +
+ +
+ + 📥 Download +
+ + +
+ +{% endblock %} + diff --git a/templates/media_view.html_baclup b/templates/media_view.html_baclup new file mode 100644 index 0000000..73fff3a --- /dev/null +++ b/templates/media_view.html_baclup @@ -0,0 +1,139 @@ +{% extends "base.html" %} + +{% block title %}Media Viewer{% endblock %} + +{% block content %} +
+

Viewing Media: {{ filename }}

+

MIME Type: {{ mime_type }}

+ +
+ {% if mime_type.startswith("image/") %} + Image + {% elif mime_type.startswith("video/") %} + + {% elif mime_type.startswith("audio/") %} + + {% elif mime_type == "application/pdf" %} + + {% endif %} +
+ +
+ + 📥 Download + 🌐 View Raw + {% if current_user.is_authenticated %} + + + {% endif %} +
+ + +
+ + +
+ +
+ + +{% endblock %} +{% block footer %} + {{ super() }} +{% endblock %} diff --git a/templates/pastes.html b/templates/pastes.html new file mode 100644 index 0000000..dc90509 --- /dev/null +++ b/templates/pastes.html @@ -0,0 +1,123 @@ +{% extends "base.html" %} +{% block content %} +
+

Manage Pastes

+
+ + + + + + + + + + + +{% for paste in pastes %} + + + + + + + +{% endfor %} + +
IDOwner IDFilenameCreated AtActions
{{ paste.id }}{{ paste.owner_id }} + + {{ paste.filename or 'View Paste' }} + + {{ paste.created_at }} +
+ +
+
+Back to Dashboard + + +
+ + + + + +{% block scripts %} + {{ super() }} + +{% endblock %} + +{% endblock %} + diff --git a/templates/request_account.html b/templates/request_account.html new file mode 100644 index 0000000..3d276b4 --- /dev/null +++ b/templates/request_account.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% block content %} +
+
+

Request an Account

+
+
+ + +
+
+ + +
+ +
+ Back to Home +
+
+{% endblock %} +{% block footer %} + {{ super() }} +{% endblock %} diff --git a/templates/search_results.html b/templates/search_results.html new file mode 100644 index 0000000..8e44a4a --- /dev/null +++ b/templates/search_results.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} + +{% block content %} +
+

Search Results

+

Results for: {{ query }}

+ {% if pastes %} + + + + + + + + + + + {% for paste in pastes %} + + + + + + + {% endfor %} + +
IDContent TypeLanguageURL
{{ paste.id }}{{ paste.content_type or "N/A" }}{{ paste.language or "N/A" }}{{ paste.url }}
+ {% else %} +

No results found for your query.

+ {% endif %} + Back to Dashboard +
+{% endblock %} + diff --git a/templates/text_paste.html b/templates/text_paste.html new file mode 100644 index 0000000..5d0c876 --- /dev/null +++ b/templates/text_paste.html @@ -0,0 +1,242 @@ +{% extends "base.html" %} + +{% block title %}Paste {{ paste.id }}{% endblock %} + +{% block content %} + +{% if paste.owner_id == current_user.id %} + +{% endif %} + + +
+

Paste {{ paste.id }}

+ +

+ {% if paste.filename and '.' in paste.filename %} + Extension: {{ paste.filename.split('.')[-1] }} | + {% endif %} + MIME Type: {{ paste.content_type }} | + Language: {{ paste.language }} | + Size: + {% if paste.size < 1024 %} + {{ paste.size }} bytes + {% elif paste.size < 1024 * 1024 %} + {{ '%.2f' | format(paste.size / 1024) }} KB + {% else %} + {{ '%.2f' | format(paste.size / (1024 * 1024)) }} MB + {% endif %} +

+ + +
+ {% if paste.owner_id == current_user.id %} + + {% endif %} + + {% if current_user.is_authenticated %} + + {% endif %} + + {% if can_edit %} + + + + {% endif %} + + {% if current_user.is_authenticated %} + + {% endif %} +
+ + +
+
+
+    
+
+ + + +
+
+
+ {% if not is_markdown %} + + {% endif %} + + + + +
+
+ + +
+
+ + {% if is_markdown %} +
{{ md_html_code|safe }}
+ {% else %} +
{{ html_code|safe }}
+ {% endif %} + +

View the raw version here.

+
+
+{% endblock %} + +{% block scripts %} + {{ super() }} + + + + + + + +{% endblock %} + diff --git a/templates/text_paste.html_backup b/templates/text_paste.html_backup new file mode 100644 index 0000000..ab31c18 --- /dev/null +++ b/templates/text_paste.html_backup @@ -0,0 +1,198 @@ +{% extends "base.html" %} + +{% block title %}Paste {{ paste.id }}{% endblock %} + +{% block content %} +
+ {% if paste.owner_id == current_user.id %} + +{% endif %} + +
+

Paste {{ paste.id }}

+
+ {% if paste.owner_id == current_user.id %} + + {% endif %} + + {% if current_user.is_authenticated %} + + {% endif %} + + {% if can_edit %} + + + + {% endif %} + + +
+
+ + + + + + +
+
💻 AI-Enhanced Code:
+
+

+    
+
+ + + +

+ {% if paste.filename and '.' in paste.filename %} + Extension: {{ paste.filename.split('.')[-1] }} | + {% endif %} + MIME Type: {{ paste.content_type }} | + Language: {{ paste.language }} | + Size: + {% if paste.size < 1024 %} + {{ paste.size }} bytes + {% elif paste.size < 1024 * 1024 %} + {{ '%.2f' | format(paste.size / 1024) }} KB + {% else %} + {{ '%.2f' | format(paste.size / (1024 * 1024)) }} MB + {% endif %} +

+ +
+
+
+ {% if not is_markdown %} + + {% endif %} + + + +
+
+ + +
+
+ + {% if is_markdown %} +
{{ md_html_code|safe }}
+ {% else %} +
{{ html_code|safe }}
+ {% endif %} + +

View the raw version here.

+
+{% endblock %} + +{% block scripts %} + {{ super() }} + + + + + + + +{% endblock %} + diff --git a/templates/text_paste.html_bak b/templates/text_paste.html_bak new file mode 100644 index 0000000..b2fce7a --- /dev/null +++ b/templates/text_paste.html_bak @@ -0,0 +1,484 @@ +{% extends "base.html" %} + +{% block title %}Paste {{ paste.id }}{% endblock %} + +{% block content %} + +
+ +
+

Paste {{ paste.id }}

+ +
+ {% if paste.owner_id == current_user.id %} + + + {% endif %} + + {% if current_user.is_authenticated %} + + + {% endif %} + + + {% if can_edit %} + + Edit Paste + + {% endif %} +
+
+ + + +{% if paste.owner_id == current_user.id %} + +{% endif %} + + +
+ +
+
+ {% if not is_markdown %} + + {% endif %} + + 📥 Download + +
+ +
+ + +
+
+ + + {% if is_markdown %} +
+ {{ md_html_code|safe }} +
+ {% else %} +
+ {{ html_code|safe }} +
+ {% endif %} + +

+ View the raw version here. +

+
+ + + {% if current_user.is_authenticated %} + + +
+
+
Shared With:
+
    +
      + {% for user, can_edit in shared_with %} +
    • + {{ user }} + + {% if can_edit %} + Can Edit + {% else %} + Read Only + {% endif %} + + + + +
    • + {% endfor %} +
    + + + + + +
+
+ {% endif %} +
+{% endblock %} + +{% block scripts %} + +{% endblock %} + diff --git a/templates/user_dashboard.html b/templates/user_dashboard.html new file mode 100644 index 0000000..cbc408c --- /dev/null +++ b/templates/user_dashboard.html @@ -0,0 +1,538 @@ +{% extends "base.html" %} +{% block title %}User Dashboard{% endblock %} + +{% block content %} + + +
+
+ +
+
+ +
+ +
+
+
+ +
+ +
+ + + +
+

Welcome, {{ user.username }}

+ + +
+
+
User Profile
+
+
+

Username: {{ user.username }}

+

Role: {{ user.role }}

+ + {# Edit Profile #} +
+
+ +
+
+
Storage Information
+
+
+ {% if storage_limit > 0 %} +

Storage Used: {{ storage_used }} MB

+

Storage Available: {{ storage_available }} MB

+
+ {% set usage_percent = (storage_used / storage_limit * 100) if storage_limit > 0 else 0 %} + {% set progress_class = 'bg-success' if usage_percent < 50 else 'bg-warning' if usage_percent < 80 else 'bg-danger' %} +
+ {{ usage_percent | round(1) }}% +
+
+ {% if storage_available < (0.1 * storage_limit) %} + + {% elif storage_available < (0.25 * storage_limit) %} + + {% endif %} + {% else %} +

Storage Used: {{ storage_used }} MB

+

Storage Available: Unlimited

+
+
+
+
+ {% endif %} +
+
+ + + Create New Paste + + +
+
Advanced Search
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + +
Your Paste Statistics:
+
    +
  • Total Pastes: {{ total_pastes }}
  • +
  • Total Size: {{ total_size }} bytes
  • +
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+
+
Total Pastes
+
+
{{ total_pastes }}
+
+
+
+
+
+
Text Pastes
+
+
{{ total_text_pastes }}
+
+
+
+
+
+
File Pastes
+
+
{{ total_file_pastes }}
+
+
+
+
+
+
Media Pastes
+
+
{{ total_media_pastes }}
+
+
+
+
+ + + +
+
+
Pastes by Language and Type
+ +
+
+ + + + +
+ +
+
Your Pastes
+{% if pastes|length > 0 %} + + + + + + + + + + + + + {% for paste in pastes %} + + + + + + + + + + + + {% endfor %} + +
IDFilenameTypeSize (bytes)Created AtActions
{{ paste.id }}paste_{{ paste.id }}.{{ paste.get_extension() }}{{ paste.get_type() }}{{ paste.size }}{{ paste.created_at.strftime('%Y-%m-%d %H:%M:%S') }} + + View + + +
+{% else %} +

You do not have any pastes yet.

+{% endif %} +
+ + +
+
Your Private Pastes
+ {% if private_pastes.items %} + + + + + + + + + + + + + {% for paste in private_pastes.items %} + + + + + + + + + + {% endfor %} + +
IDFilenameTypeSize (bytes)Created AtActions
{{ paste.id }}paste_{{ paste.id }}.{{ paste.get_extension() }}{{ paste.get_type() }}{{ paste.size }}{{ paste.created_at.strftime('%Y-%m-%d %H:%M:%S') }} + + View + + +
+ + + {% if private_pastes.pages > 1 %} + + {% endif %} + {% else %} +

You do not have any private pastes yet.

+ {% endif %} +
+ + + +
+
Your Favorites
+ {% if favorite_pastes %} + + + + + + + + + + + + + {% for fav in favorite_pastes %} + + + + + + + + + {% endfor %} + +
IDFilenameTypeSize (bytes)Created AtActions
{{ fav.id }}{{ fav.filename if fav.filename else "Paste " ~ fav.id }}{{ fav.get_type() }}{{ fav.size }}{{ fav.created_at.strftime('%Y-%m-%d %H:%M:%S') }} + + View + + +
+ + {% if favorite_pagination.pages > 1 %} + + {% endif %} + {% else %} +

You don't have any favorites yet.

+ {% endif %} +
+ + +
+
Shared Pastes
+ {% if shared_pastes %} + + + + + + + + + + + + + + {% for sp in shared_pastes %} + + + + + + + + + + {% endfor %} + +
IDTitleOwnerTypeSize (bytes)Created AtActions
{{ sp.id }}{{ sp.title or "Untitled" }}{{ sp.owner.username }}{{ sp.get_type() }}{{ sp.size }}{{ sp.created_at.strftime('%Y-%m-%d %H:%M:%S') }} + + View + + + {% if sp.has_edit_permission(current_user) %} + Edit + {% endif %} +
+ + {% if shared_pastes|length > 10 %} + + {% endif %} + {% else %} +

You don't have any shared pastes yet.

+ {% endif %} +
+
+ +
Account Settings
+ +
+ + + + + + + + + + + + + + + + +{% endblock %} +{% block footer %} + {{ super() }} +{% endblock %} diff --git a/templates/users.html b/templates/users.html new file mode 100644 index 0000000..6fb12e3 --- /dev/null +++ b/templates/users.html @@ -0,0 +1,112 @@ +{% extends "base.html" %} +{% block content %} +
+

Manage Users

+ Add New User +
+ + + + + + + + + + + {% for user in users %} + + + + + + + + + + {% endfor %} + +
IDUsernameRoleActions
{{ user.id }}{{ user.username }} + + +
+
+ +
+Back to Dashboard + + +{% endblock %} + +