diff --git a/app.py b/app.py index 1a748d6..660b3ca 100644 --- a/app.py +++ b/app.py @@ -30,6 +30,7 @@ from routes.digicard import digicard_bp, set_oidc as set_oidc_digicard from routes.misc import misc_routes_bp from routes.coupons_admin import coupons_admin_bp +from routes.notifications_admin import notifications_admin_bp from utils.cache import init_cache, generate_top_page_cache from utils.achievements import sync_achievements from utils.db import init_db @@ -110,6 +111,7 @@ def utility_processor(): app.register_blueprint(digicard_bp) app.register_blueprint(misc_routes_bp) app.register_blueprint(coupons_admin_bp) +app.register_blueprint(notifications_admin_bp) # Background scheduler for cache generation scheduler = BackgroundScheduler() diff --git a/config_example.py b/config_example.py index 5c5a360..49441f7 100644 --- a/config_example.py +++ b/config_example.py @@ -31,3 +31,12 @@ } } oidc_logout_url = "" + +# Web Push notifications. +vapid_subject = "mailto:change-me@example.com" +vapid_public_key = "" +vapid_private_key = "" + +notifications_shared_secret = "" + +accountmanager_url = "http://localhost:9011" diff --git a/migrations/env.py b/migrations/env.py index a7cbcfe..1a62a2c 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -2,11 +2,11 @@ from sqlalchemy import create_engine, pool import config as app_config -from utils.db import db +from utils.db import CHECKOUT_BIND, db config = context.config -target_metadata = db.metadata +target_metadata = db.metadatas[CHECKOUT_BIND] url = app_config.checkout_db_url diff --git a/migrations/versions/3404e6b4acfe_add_notification_preferences.py b/migrations/versions/3404e6b4acfe_add_notification_preferences.py new file mode 100644 index 0000000..a743d59 --- /dev/null +++ b/migrations/versions/3404e6b4acfe_add_notification_preferences.py @@ -0,0 +1,34 @@ +"""add notification_preferences + +Revision ID: 3404e6b4acfe +Revises: b8f326057d6d +Create Date: 2026-09-01 21:59:25.577785 + +""" +from alembic import op +import sqlalchemy as sa + + +revision = '3404e6b4acfe' +down_revision = 'b8f326057d6d' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('notification_preferences', + sa.Column('username', sa.Text(), nullable=False), + sa.Column('category', sa.Text(), nullable=False), + sa.Column('enabled', sa.Boolean(), server_default=sa.text('true'), nullable=False), + sa.PrimaryKeyConstraint('username', 'category') + ) + op.drop_index(op.f('idx_push_subscriptions_username'), table_name='push_subscriptions') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_index(op.f('idx_push_subscriptions_username'), 'push_subscriptions', ['username'], unique=False) + op.drop_table('notification_preferences') + # ### end Alembic commands ### diff --git a/migrations/versions/b8f326057d6d_add_push_subscriptions_table.py b/migrations/versions/b8f326057d6d_add_push_subscriptions_table.py new file mode 100644 index 0000000..96cf99e --- /dev/null +++ b/migrations/versions/b8f326057d6d_add_push_subscriptions_table.py @@ -0,0 +1,50 @@ +"""add push_subscriptions table + +Revision ID: b8f326057d6d +Revises: 0001 +Create Date: 2026-09-01 20:26:26.866492 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +revision = "b8f326057d6d" +down_revision = "0001" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "push_subscriptions", + sa.Column("endpoint", sa.Text(), nullable=False), + sa.Column("username", sa.Text(), nullable=False), + sa.Column("p256dh", sa.Text(), nullable=False), + sa.Column("auth", sa.Text(), nullable=False), + sa.Column( + "created_at", + postgresql.TIMESTAMP(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.PrimaryKeyConstraint("endpoint"), + ) + op.create_index( + op.f("ix_push_subscriptions_username"), + "push_subscriptions", + ["username"], + unique=False, + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index( + op.f("ix_push_subscriptions_username"), table_name="push_subscriptions" + ) + op.drop_table("push_subscriptions") + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index 8d2d103..5377bdc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,5 @@ colorgram.py redis sqlalchemy alembic +requests +pywebpush diff --git a/routes/misc.py b/routes/misc.py index b44d637..16535b7 100644 --- a/routes/misc.py +++ b/routes/misc.py @@ -1,5 +1,12 @@ -from flask import Blueprint, render_template, request, redirect, url_for +import hmac +from flask import Blueprint, jsonify, render_template, request, redirect, url_for +from urllib.parse import urlparse import random +from sqlalchemy import func +from utils.db import PushSubscription, db +from utils.push import CATEGORIES, send_push_to_all, send_push_to_user + + import config from utils.utils import ( search_authentik_users_by_name, @@ -7,6 +14,8 @@ _run_query, ) +MAX_SUBSCRIPTIONS_PER_USER = 10 + misc_routes_bp = Blueprint("misc_routes", __name__, url_prefix="") @@ -183,6 +192,202 @@ def search(): ) +def _shared_secret_authorized(): + auth = request.headers.get("Authorization", "") + expected = f"Bearer {config.notifications_shared_secret}" + return bool(config.notifications_shared_secret) and hmac.compare_digest( + auth, expected + ) + + +@misc_routes_bp.route( + "/notifications/test", methods=["POST"], endpoint="test_notification" +) +def test_notification(): + if not _shared_secret_authorized(): + return jsonify({"error": "unauthorized"}), 401 + + data = request.get_json(silent=True) or {} + username = (data.get("username") or "").strip() + if not username: + return jsonify({"error": "username is required"}), 400 + + sent = send_push_to_user( + username, + title="Checkout Test Notification", + body="Push notifications are working!", + url="/", + ) + return jsonify({"sent": sent}) + + +@misc_routes_bp.route( + "/api/notifications/send", methods=["POST"], endpoint="send_notification" +) +def send_notification(): + """ + Body: {"username": "...", "category": "...", "title": "...", "body": "...", + "url": "...", "tag": "...", "icon": "..."}""" + if not _shared_secret_authorized(): + return jsonify({"error": "unauthorized"}), 401 + + data = request.get_json(silent=True) or {} + title = (data.get("title") or "").strip() + if not title: + return jsonify({"error": "title is required"}), 400 + + category = (data.get("category") or "").strip() or None + if category and category not in CATEGORIES: + return jsonify({"error": "unknown category"}), 400 + + username = (data.get("username") or "").strip() + if username: + sent = send_push_to_user( + username, + title=title, + body=data.get("body", ""), + url=data.get("url", "/"), + tag=data.get("tag"), + category=category, + icon=data.get("icon"), + ) + else: + sent = send_push_to_all( + title=title, + body=data.get("body", ""), + url=data.get("url", "/"), + tag=data.get("tag"), + category=category, + icon=data.get("icon"), + ) + return jsonify({"sent": sent}) + + +def _safe_return_target(): + target = request.args.get("return", "/") + if target.startswith("/") and not target.startswith("//"): + return target + + trusted_origin = _accountmanager_origin() + if trusted_origin: + parsed = urlparse(target) + if parsed.scheme in ("http", "https") and ( + f"{parsed.scheme}://{parsed.netloc}" == trusted_origin + ): + return target + return "/" + + +def _accountmanager_origin(): + parsed = urlparse(config.accountmanager_url or "") + if parsed.scheme and parsed.netloc: + return f"{parsed.scheme}://{parsed.netloc}" + return None + + +@misc_routes_bp.route("/notifications/enable", endpoint="enable_notifications") +def enable_notifications(): + user_info = get_logged_in_user_info() + if not user_info: + return redirect(url_for("oidc_auth.login")) + + configured = bool(config.vapid_public_key) + return render_template( + "notifications_enable.html", + public_key=config.vapid_public_key, + return_url=_safe_return_target(), + status_message=( + None + if configured + else "Notifications are not configured on this server yet." + ), + ) + + +@misc_routes_bp.route("/notifications/disable", endpoint="disable_notifications") +def disable_notifications(): + user_info = get_logged_in_user_info() + if not user_info: + return redirect(url_for("oidc_auth.login")) + return render_template( + "notifications_disable.html", return_url=_safe_return_target() + ) + + +@misc_routes_bp.route( + "/notifications/subscribe", methods=["POST"], endpoint="subscribe_notification" +) +def subscribe_notification(): + user_info = get_logged_in_user_info() + username = (user_info or {}).get("username", "") + if not username: + return jsonify({"error": "Not logged in"}), 401 + + data = request.get_json(silent=True) or {} + endpoint = (data.get("endpoint") or "").strip() + keys = data.get("keys") or {} + p256dh = keys.get("p256dh") + auth = keys.get("auth") + if not endpoint or not p256dh or not auth: + return jsonify({"error": "Invalid subscription"}), 400 + + existing = db.session.get(PushSubscription, endpoint) + if existing: + existing.username = username + existing.p256dh = p256dh + existing.auth = auth + else: + count = db.session.execute( + db.select(func.count()) + .select_from(PushSubscription) + .where(PushSubscription.username == username) + ).scalar() + if count >= MAX_SUBSCRIPTIONS_PER_USER: + oldest = ( + db.session.execute( + db.select(PushSubscription) + .where(PushSubscription.username == username) + .order_by(PushSubscription.created_at) + .limit(count - MAX_SUBSCRIPTIONS_PER_USER + 1) + ) + .scalars() + .all() + ) + for row in oldest: + db.session.delete(row) + db.session.add( + PushSubscription( + endpoint=endpoint, username=username, p256dh=p256dh, auth=auth + ) + ) + db.session.commit() + return jsonify({"ok": True}) + + +@misc_routes_bp.route( + "/notifications/unsubscribe", methods=["POST"], endpoint="unsubscribe_notification" +) +def unsubscribe_notification(): + user_info = get_logged_in_user_info() + username = (user_info or {}).get("username", "") + if not username: + return jsonify({"error": "Not logged in"}), 401 + + data = request.get_json(silent=True) or {} + endpoint = (data.get("endpoint") or "").strip() + if not endpoint: + return jsonify({"error": "Invalid subscription"}), 400 + + db.session.execute( + db.delete(PushSubscription).where( + PushSubscription.endpoint == endpoint, + PushSubscription.username == username, + ) + ) + db.session.commit() + return jsonify({"ok": True}) + + @misc_routes_bp.route("/logout", endpoint="logout") def logout(): """Logout user and redirect to login page""" diff --git a/routes/notifications_admin.py b/routes/notifications_admin.py new file mode 100644 index 0000000..871be8a --- /dev/null +++ b/routes/notifications_admin.py @@ -0,0 +1,100 @@ +from functools import wraps + +import config +from flask import ( + Blueprint, + render_template, + request, + redirect, + url_for, + flash, + abort, +) + +from routes.auth import get_logged_in_user_info +from utils.push import CATEGORIES, send_push_to_all, send_push_to_user + +NOTIFICATIONS_ADMIN_GROUP_UUID = getattr(config, "coupon_admin_group_uuid", "") + +notifications_admin_bp = Blueprint( + "notifications_admin", __name__, url_prefix="/notifications/admin" +) + + +def notifications_admin_required(f): + @wraps(f) + def decorated(*args, **kwargs): + user_info = get_logged_in_user_info() + if not user_info: + abort(401) + groups = user_info.get("groups") or [] + if NOTIFICATIONS_ADMIN_GROUP_UUID not in groups: + abort(403) + return f(*args, **kwargs) + + return decorated + + +def _valid_url(value): + return (value.startswith("/") and not value.startswith("//")) or value.startswith( + ("http://", "https://") + ) + + +@notifications_admin_bp.route("/", methods=["GET", "POST"]) +@notifications_admin_required +def panel(): + user_info = get_logged_in_user_info() + + if request.method == "POST": + title = (request.form.get("title") or "").strip() + body = request.form.get("body") or "" + username = (request.form.get("username") or "").strip() + category = (request.form.get("category") or "").strip() + url = (request.form.get("url") or "").strip() or "/" + icon = (request.form.get("icon") or "").strip() or None + tag = (request.form.get("tag") or "").strip() or None + + if not title: + flash("A notification title is required.") + return redirect(url_for("notifications_admin.panel")) + if not _valid_url(url): + flash("The click URL must be a relative path or an http(s) URL.") + return redirect(url_for("notifications_admin.panel")) + if icon and not _valid_url(icon): + flash("The icon must be a relative path or an http(s) URL.") + return redirect(url_for("notifications_admin.panel")) + + if category and category not in CATEGORIES: + flash("Unknown notification category.") + return redirect(url_for("notifications_admin.panel")) + category = category or None + + if username: + sent = send_push_to_user( + username, + title=title, + body=body, + url=url, + tag=tag, + category=category, + icon=icon, + ) + else: + sent = send_push_to_all( + title=title, + body=body, + url=url, + tag=tag, + category=category, + icon=icon, + ) + flash(f"Notification sent to {sent} device(s).") + return redirect(url_for("notifications_admin.panel")) + + categories = sorted(CATEGORIES) + return render_template( + "notifications_admin.html", + user_info=user_info, + categories=categories, + ) diff --git a/static/css/main.css b/static/css/main.css index 2c02c1f..a797c82 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -1 +1,3006 @@ -*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-x-0{left:0;right:0}.inset-x-2{left:.5rem;right:.5rem}.bottom-0{bottom:0}.bottom-full{bottom:100%}.left-0{left:0}.left-1\/2{left:50%}.left-full{left:100%}.right-0{right:0}.right-3{right:.75rem}.right-4{right:1rem}.top-0{top:0}.top-1{top:.25rem}.top-1\/2{top:50%}.top-2{top:.5rem}.top-4{top:1rem}.top-full{top:100%}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.m-4{margin:1rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.\!mb-0{margin-bottom:0!important}.-ml-4{margin-left:-1rem}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-16{margin-bottom:4rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-1\.5{margin-right:.375rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-20{margin-top:5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-\[2\/2\.8\]{aspect-ratio:2/2.8}.aspect-square{aspect-ratio:1/1}.h-1{height:.25rem}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-20{height:5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-3\/4{height:75%}.h-32{height:8rem}.h-4{height:1rem}.h-40{height:10rem}.h-44{height:11rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-\[150px\]{height:150px}.h-\[200px\]{height:200px}.h-\[250px\]{height:250px}.h-\[400px\]{height:400px}.h-\[500px\]{height:500px}.h-\[calc\(100\%-25px\)\]{height:calc(100% - 25px)}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-72{max-height:18rem}.min-h-\[132px\]{min-height:132px}.min-h-\[150px\]{min-height:150px}.min-h-\[44px\]{min-height:44px}.min-h-screen{min-height:100vh}.w-1\/3{width:33.333333%}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-2\/3{width:66.666667%}.w-20{width:5rem}.w-24{width:6rem}.w-3\.5{width:.875rem}.w-32{width:8rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-\[100px\]{width:100px}.w-\[160px\]{width:160px}.w-\[200px\]{width:200px}.w-\[calc\(100\%-37px\)\]{width:calc(100% - 37px)}.w-\[calc\(100vw-2rem\)\]{width:calc(100vw - 2rem)}.w-full{width:100%}.w-screen{width:100vw}.min-w-0{min-width:0}.min-w-max{min-width:-moz-max-content;min-width:max-content}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-\[300px\]{max-width:300px}.max-w-\[400px\]{max-width:400px}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-shrink{flex-shrink:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.-translate-y-3{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-3{--tw-translate-y:-0.75rem}.-translate-y-8{--tw-translate-y:-2rem}.-translate-y-8,.-translate-y-\[120\%\]{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-\[120\%\]{--tw-translate-y:-120%}.-translate-y-\[50px\]{--tw-translate-y:-50px}.-translate-y-\[50px\],.-translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-full{--tw-translate-y:-100%}.translate-y-5{--tw-translate-y:1.25rem}.translate-y-5,.translate-y-\[27px\]{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-\[27px\]{--tw-translate-y:27px}.translate-y-full{--tw-translate-y:100%}.transform,.translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-help{cursor:help}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-\[var\(--user-transparent\)\]>:not([hidden])~:not([hidden]){border-color:var(--user-transparent)}.self-center{align-self:center}.justify-self-center{justify-self:center}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-scroll{overflow:scroll}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-scroll{overflow-y:scroll}.truncate{overflow:hidden;white-space:nowrap}.overflow-ellipsis,.truncate{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-\[20px\]{border-radius:20px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-b-md{border-bottom-right-radius:.375rem}.rounded-b-md,.rounded-l-md{border-bottom-left-radius:.375rem}.rounded-l-md{border-top-left-radius:.375rem}.rounded-l-xl{border-top-left-radius:.75rem;border-bottom-left-radius:.75rem}.rounded-r-md{border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.rounded-r-xl{border-top-right-radius:.75rem;border-bottom-right-radius:.75rem}.rounded-t-2xl{border-top-left-radius:1rem;border-top-right-radius:1rem}.rounded-t-xl{border-top-left-radius:.75rem;border-top-right-radius:.75rem}.border{border-width:1px}.border-0{border-width:0}.border-2{border-width:2px}.border-\[5px\]{border-width:5px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-\[var\(--user-base\)\]{border-color:var(--user-base)}.border-\[var\(--user-dark\)\]{border-color:var(--user-dark)}.border-\[var\(--user-light\)\]{border-color:var(--user-light)}.border-\[var\(--user-soft\)\]{border-color:var(--user-soft)}.border-\[var\(--user-transparent\)\]{border-color:var(--user-transparent)}.border-amber-500\/30{border-color:rgba(245,158,11,.3)}.border-black{--tw-border-opacity:1;border-color:rgb(0 0 0/var(--tw-border-opacity,1))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity,1))}.border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity,1))}.border-indigo-500{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.border-red-500\/30{border-color:rgba(239,68,68,.3)}.border-red-700{--tw-border-opacity:1;border-color:rgb(185 28 28/var(--tw-border-opacity,1))}.border-white{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity,1))}.border-white\/5{border-color:hsla(0,0%,100%,.05)}.border-yellow-600\/30{border-color:rgba(202,138,4,.3)}.border-yellow-600\/50{border-color:rgba(202,138,4,.5)}.\!bg-\[var\(--user-soft\)\]{background-color:var(--user-soft)!important}.\!bg-gray-200{--tw-bg-opacity:1!important;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))!important}.bg-\[rgb\(76\2c 130\2c 163\)\]{--tw-bg-opacity:1;background-color:rgb(76 130 163/var(--tw-bg-opacity,1))}.bg-\[var\(--user-base\)\]{background-color:var(--user-base)}.bg-\[var\(--user-dark\)\]{background-color:var(--user-dark)}.bg-\[var\(--user-light\)\]{background-color:var(--user-light)}.bg-\[var\(--user-soft\)\]{background-color:var(--user-soft)}.bg-\[var\(--user-transparent\)\]{background-color:var(--user-transparent)}.bg-amber-500\/10{background-color:rgba(245,158,11,.1)}.bg-amber-700\/50{background-color:rgba(180,83,9,.5)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-black\/70{background-color:rgba(0,0,0,.7)}.bg-emerald-400{--tw-bg-opacity:1;background-color:rgb(52 211 153/var(--tw-bg-opacity,1))}.bg-emerald-600{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-gray-500\/10{background-color:hsla(220,9%,46%,.1)}.bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity,1))}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.bg-gray-700\/30{background-color:rgba(55,65,81,.3)}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.bg-gray-800\/60{background-color:rgba(31,41,55,.6)}.bg-gray-800\/70{background-color:rgba(31,41,55,.7)}.bg-gray-900\/50{background-color:rgba(17,24,39,.5)}.bg-gray-900\/60{background-color:rgba(17,24,39,.6)}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-green-600{--tw-bg-opacity:1;background-color:rgb(22 163 74/var(--tw-bg-opacity,1))}.bg-indigo-400{--tw-bg-opacity:1;background-color:rgb(129 140 248/var(--tw-bg-opacity,1))}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity,1))}.bg-pink-500{--tw-bg-opacity:1;background-color:rgb(236 72 153/var(--tw-bg-opacity,1))}.bg-red-400{--tw-bg-opacity:1;background-color:rgb(248 113 113/var(--tw-bg-opacity,1))}.bg-red-500\/10{background-color:rgba(239,68,68,.1)}.bg-red-600\/20{background-color:rgba(220,38,38,.2)}.bg-red-900\/30{background-color:rgba(127,29,29,.3)}.bg-rose-500\/20{background-color:rgba(244,63,94,.2)}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-white\/10{background-color:hsla(0,0%,100%,.1)}.bg-yellow-400{--tw-bg-opacity:1;background-color:rgb(250 204 21/var(--tw-bg-opacity,1))}.bg-yellow-500\/20{background-color:rgba(234,179,8,.2)}.bg-yellow-900\/30{background-color:rgba(113,63,18,.3)}.bg-yellow-900\/50{background-color:rgba(113,63,18,.5)}.bg-opacity-0{--tw-bg-opacity:0}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-black\/70{--tw-gradient-from:rgba(0,0,0,.7) var(--tw-gradient-from-position);--tw-gradient-to:transparent var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-gray-900{--tw-gradient-from:#111827 var(--tw-gradient-from-position);--tw-gradient-to:rgba(17,24,39,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-gray-700{--tw-gradient-to:#374151 var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.bg-cover{background-size:cover}.bg-center{background-position:50%}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.object-bottom{-o-object-position:bottom;object-position:bottom}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-12{padding:3rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-3\.5{padding-left:.875rem;padding-right:.875rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-\[env\(safe-area-inset-bottom\)\]{padding-bottom:env(safe-area-inset-bottom)}.pb-\[max\(0\.5rem\2c env\(safe-area-inset-bottom\)\)\]{padding-bottom:max(.5rem,env(safe-area-inset-bottom))}.pl-1{padding-left:.25rem}.pl-2{padding-left:.5rem}.pr-1{padding-right:.25rem}.pr-2{padding-right:.5rem}.pr-4{padding-right:1rem}.pt-1{padding-top:.25rem}.pt-10{padding-top:2.5rem}.pt-2{padding-top:.5rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.\!text-2xl{font-size:1.5rem!important;line-height:2rem!important}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[10px\]{font-size:10px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-light{font-weight:300}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-\[var\(--user-soft\)\]{color:var(--user-soft)}.text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.text-emerald-800{--tw-text-opacity:1;color:rgb(6 95 70/var(--tw-text-opacity,1))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity,1))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1))}.text-red-200{--tw-text-opacity:1;color:rgb(254 202 202/var(--tw-text-opacity,1))}.text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.text-rose-300{--tw-text-opacity:1;color:rgb(253 164 175/var(--tw-text-opacity,1))}.text-rose-400{--tw-text-opacity:1;color:rgb(251 113 133/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/30{color:hsla(0,0%,100%,.3)}.text-white\/50{color:hsla(0,0%,100%,.5)}.text-white\/80{color:hsla(0,0%,100%,.8)}.text-yellow-200{--tw-text-opacity:1;color:rgb(254 240 138/var(--tw-text-opacity,1))}.text-yellow-300{--tw-text-opacity:1;color:rgb(253 224 71/var(--tw-text-opacity,1))}.text-yellow-800{--tw-text-opacity:1;color:rgb(133 77 14/var(--tw-text-opacity,1))}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.opacity-0{opacity:0}.opacity-10{opacity:.1}.opacity-100{opacity:1}.opacity-30{opacity:.3}.opacity-40{opacity:.4}.opacity-5{opacity:.05}.opacity-50{opacity:.5}.opacity-55{opacity:.55}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-sm,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.blur{--tw-blur:blur(8px)}.blur,.blur-lg{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-lg{--tw-blur:blur(16px)}.blur-md{--tw-blur:blur(12px)}.blur-md,.blur-sm{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-sm{--tw-blur:blur(4px)}.blur-xl{--tw-blur:blur(24px)}.blur-xl,.drop-shadow{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px rgba(0,0,0,.1)) drop-shadow(0 1px 1px rgba(0,0,0,.06))}.grayscale{--tw-grayscale:grayscale(100%)}.grayscale,.invert{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert:invert(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-lg{--tw-backdrop-blur:blur(16px)}.backdrop-blur-lg,.backdrop-blur-md{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur-sm,.backdrop-blur-xl{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur:blur(24px)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\[color-scheme\:dark\]{color-scheme:dark}.hover\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-\[var\(--user-soft\)\]:hover{border-color:var(--user-soft)}.hover\:border-indigo-500:hover{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.hover\:bg-\[var\(--user-base\)\]:hover{background-color:var(--user-base)}.hover\:bg-\[var\(--user-dark\)\]:hover{background-color:var(--user-dark)}.hover\:bg-amber-700\/60:hover{background-color:rgba(180,83,9,.6)}.hover\:bg-gray-600:hover{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity,1))}.hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.hover\:bg-gray-700\/60:hover{background-color:rgba(55,65,81,.6)}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgb(21 128 61/var(--tw-bg-opacity,1))}.hover\:bg-indigo-600:hover{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity,1))}.hover\:bg-indigo-700:hover{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity,1))}.hover\:bg-red-600\/30:hover{background-color:rgba(220,38,38,.3)}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-2xl:hover{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.hover\:shadow-2xl:hover,.hover\:shadow-lg:hover{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:border-\[var\(--user-light\)\]:focus{border-color:var(--user-light)}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity,1))}.active\:bg-green-800:active{--tw-bg-opacity:1;background-color:rgb(22 101 52/var(--tw-bg-opacity,1))}.active\:bg-indigo-600:active{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity,1))}.group:hover .group-hover\:translate-x-1{--tw-translate-x:0.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:bg-opacity-50{--tw-bg-opacity:0.5}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width:640px){.sm\:absolute{position:absolute}.sm\:left-1\/2{left:50%}.sm\:mt-0{margin-top:0}.sm\:mt-72{margin-top:18rem}.sm\:block{display:block}.sm\:grid{display:grid}.sm\:hidden{display:none}.sm\:h-12{height:3rem}.sm\:h-14{height:3.5rem}.sm\:h-\[calc\(100\%-80px\)\]{height:calc(100% - 80px)}.sm\:w-12{width:3rem}.sm\:w-14{width:3.5rem}.sm\:w-56{width:14rem}.sm\:-translate-x-1\/2{--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:flex-wrap{flex-wrap:wrap}.sm\:items-end{align-items:flex-end}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-3{gap:.75rem}.sm\:overflow-visible{overflow:visible}.sm\:p-12{padding:3rem}.sm\:p-6{padding:1.5rem}.sm\:p-8{padding:2rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-12{padding-top:3rem;padding-bottom:3rem}.sm\:pt-0{padding-top:0}.sm\:text-left{text-align:left}.sm\:\!text-4xl{font-size:2.25rem!important;line-height:2.5rem!important}.sm\:text-2xl{font-size:1.5rem;line-height:2rem}.sm\:text-4xl{font-size:2.25rem;line-height:2.5rem}.sm\:text-5xl{font-size:3rem;line-height:1}}@media (min-width:768px){.md\:bottom-auto{bottom:auto}.md\:left-\[300px\]{left:300px}.md\:right-6{right:1.5rem}.md\:right-auto{right:auto}.md\:top-0{top:0}.md\:top-6{top:1.5rem}.md\:col-span-2{grid-column:span 2/span 2}.md\:ml-\[300px\]{margin-left:300px}.md\:block{display:block}.md\:flex{display:flex}.md\:hidden{display:none}.md\:h-screen{height:100vh}.md\:w-1\/2{width:50%}.md\:w-\[calc\(100\%-300px\)\]{width:calc(100% - 300px)}.md\:w-full{width:100%}.md\:max-w-\[300px\]{max-width:300px}.md\:-translate-x-\[calc\(50\%-140px\)\]{--tw-translate-x:calc(-50% - -140px)}.md\:-translate-x-\[calc\(50\%-140px\)\],.md\:translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:translate-x-0{--tw-translate-x:0px}.md\:translate-y-0{--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:rounded-none{border-radius:0}.md\:border-r{border-right-width:1px}.md\:border-t-0{border-top-width:0}.md\:pb-0{padding-bottom:0}}@media (min-width:1024px){.lg\:bottom-5{bottom:1.25rem}.lg\:left-full{left:100%}.lg\:top-0{top:0}.lg\:-ml-4{margin-left:-1rem}.lg\:block{display:block}.lg\:inline-block{display:inline-block}.lg\:grid{display:grid}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (min-width:1280px){.xl\:flex{display:flex}.xl\:max-h-\[460px\]{max-height:460px}.xl\:w-\[500px\]{width:500px}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:flex-row{flex-direction:row}.xl\:items-center{align-items:center}.xl\:pr-1{padding-right:.25rem}} \ No newline at end of file +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +/* +! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + letter-spacing: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +input:where([type='button']), +input:where([type='reset']), +input:where([type='submit']) { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden]:where(:not([hidden="until-found"])) { + display: none; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.pointer-events-none { + pointer-events: none; +} + +.visible { + visibility: visible; +} + +.static { + position: static; +} + +.fixed { + position: fixed; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.sticky { + position: sticky; +} + +.inset-0 { + inset: 0px; +} + +.inset-x-0 { + left: 0px; + right: 0px; +} + +.inset-x-2 { + left: 0.5rem; + right: 0.5rem; +} + +.bottom-0 { + bottom: 0px; +} + +.bottom-full { + bottom: 100%; +} + +.left-0 { + left: 0px; +} + +.left-1\/2 { + left: 50%; +} + +.left-full { + left: 100%; +} + +.right-0 { + right: 0px; +} + +.right-3 { + right: 0.75rem; +} + +.right-4 { + right: 1rem; +} + +.top-0 { + top: 0px; +} + +.top-1 { + top: 0.25rem; +} + +.top-1\/2 { + top: 50%; +} + +.top-2 { + top: 0.5rem; +} + +.top-4 { + top: 1rem; +} + +.top-full { + top: 100%; +} + +.z-0 { + z-index: 0; +} + +.z-10 { + z-index: 10; +} + +.z-20 { + z-index: 20; +} + +.z-30 { + z-index: 30; +} + +.z-40 { + z-index: 40; +} + +.z-50 { + z-index: 50; +} + +.m-4 { + margin: 1rem; +} + +.mx-2 { + margin-left: 0.5rem; + margin-right: 0.5rem; +} + +.mx-4 { + margin-left: 1rem; + margin-right: 1rem; +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.my-1 { + margin-top: 0.25rem; + margin-bottom: 0.25rem; +} + +.\!mb-0 { + margin-bottom: 0px !important; +} + +.-ml-4 { + margin-left: -1rem; +} + +.mb-0\.5 { + margin-bottom: 0.125rem; +} + +.mb-1 { + margin-bottom: 0.25rem; +} + +.mb-10 { + margin-bottom: 2.5rem; +} + +.mb-16 { + margin-bottom: 4rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mb-3 { + margin-bottom: 0.75rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mb-5 { + margin-bottom: 1.25rem; +} + +.mb-6 { + margin-bottom: 1.5rem; +} + +.mb-8 { + margin-bottom: 2rem; +} + +.ml-1 { + margin-left: 0.25rem; +} + +.ml-2 { + margin-left: 0.5rem; +} + +.ml-4 { + margin-left: 1rem; +} + +.ml-auto { + margin-left: auto; +} + +.mr-1 { + margin-right: 0.25rem; +} + +.mr-1\.5 { + margin-right: 0.375rem; +} + +.mr-2 { + margin-right: 0.5rem; +} + +.mr-3 { + margin-right: 0.75rem; +} + +.mt-0\.5 { + margin-top: 0.125rem; +} + +.mt-1 { + margin-top: 0.25rem; +} + +.mt-10 { + margin-top: 2.5rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-20 { + margin-top: 5rem; +} + +.mt-3 { + margin-top: 0.75rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.mt-5 { + margin-top: 1.25rem; +} + +.mt-6 { + margin-top: 1.5rem; +} + +.mt-8 { + margin-top: 2rem; +} + +.line-clamp-2 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.block { + display: block; +} + +.inline-block { + display: inline-block; +} + +.inline { + display: inline; +} + +.flex { + display: flex; +} + +.inline-flex { + display: inline-flex; +} + +.table { + display: table; +} + +.grid { + display: grid; +} + +.hidden { + display: none; +} + +.aspect-\[2\/2\.8\] { + aspect-ratio: 2/2.8; +} + +.aspect-square { + aspect-ratio: 1 / 1; +} + +.h-1 { + height: 0.25rem; +} + +.h-10 { + height: 2.5rem; +} + +.h-11 { + height: 2.75rem; +} + +.h-12 { + height: 3rem; +} + +.h-14 { + height: 3.5rem; +} + +.h-16 { + height: 4rem; +} + +.h-2 { + height: 0.5rem; +} + +.h-2\.5 { + height: 0.625rem; +} + +.h-20 { + height: 5rem; +} + +.h-3 { + height: 0.75rem; +} + +.h-3\.5 { + height: 0.875rem; +} + +.h-3\/4 { + height: 75%; +} + +.h-32 { + height: 8rem; +} + +.h-4 { + height: 1rem; +} + +.h-40 { + height: 10rem; +} + +.h-44 { + height: 11rem; +} + +.h-5 { + height: 1.25rem; +} + +.h-6 { + height: 1.5rem; +} + +.h-7 { + height: 1.75rem; +} + +.h-8 { + height: 2rem; +} + +.h-\[150px\] { + height: 150px; +} + +.h-\[200px\] { + height: 200px; +} + +.h-\[250px\] { + height: 250px; +} + +.h-\[400px\] { + height: 400px; +} + +.h-\[500px\] { + height: 500px; +} + +.h-\[calc\(100\%-25px\)\] { + height: calc(100% - 25px); +} + +.h-auto { + height: auto; +} + +.h-full { + height: 100%; +} + +.h-px { + height: 1px; +} + +.h-screen { + height: 100vh; +} + +.max-h-72 { + max-height: 18rem; +} + +.min-h-\[132px\] { + min-height: 132px; +} + +.min-h-\[150px\] { + min-height: 150px; +} + +.min-h-\[44px\] { + min-height: 44px; +} + +.min-h-screen { + min-height: 100vh; +} + +.w-1\/3 { + width: 33.333333%; +} + +.w-10 { + width: 2.5rem; +} + +.w-11 { + width: 2.75rem; +} + +.w-12 { + width: 3rem; +} + +.w-14 { + width: 3.5rem; +} + +.w-16 { + width: 4rem; +} + +.w-2 { + width: 0.5rem; +} + +.w-2\.5 { + width: 0.625rem; +} + +.w-2\/3 { + width: 66.666667%; +} + +.w-20 { + width: 5rem; +} + +.w-24 { + width: 6rem; +} + +.w-3\.5 { + width: 0.875rem; +} + +.w-32 { + width: 8rem; +} + +.w-4 { + width: 1rem; +} + +.w-48 { + width: 12rem; +} + +.w-5 { + width: 1.25rem; +} + +.w-6 { + width: 1.5rem; +} + +.w-64 { + width: 16rem; +} + +.w-7 { + width: 1.75rem; +} + +.w-8 { + width: 2rem; +} + +.w-\[100px\] { + width: 100px; +} + +.w-\[160px\] { + width: 160px; +} + +.w-\[200px\] { + width: 200px; +} + +.w-\[calc\(100\%-37px\)\] { + width: calc(100% - 37px); +} + +.w-\[calc\(100vw-2rem\)\] { + width: calc(100vw - 2rem); +} + +.w-full { + width: 100%; +} + +.w-screen { + width: 100vw; +} + +.min-w-0 { + min-width: 0px; +} + +.min-w-max { + min-width: -moz-max-content; + min-width: max-content; +} + +.max-w-2xl { + max-width: 42rem; +} + +.max-w-3xl { + max-width: 48rem; +} + +.max-w-4xl { + max-width: 56rem; +} + +.max-w-5xl { + max-width: 64rem; +} + +.max-w-6xl { + max-width: 72rem; +} + +.max-w-\[300px\] { + max-width: 300px; +} + +.max-w-\[400px\] { + max-width: 400px; +} + +.max-w-md { + max-width: 28rem; +} + +.max-w-sm { + max-width: 24rem; +} + +.max-w-xl { + max-width: 36rem; +} + +.flex-1 { + flex: 1 1 0%; +} + +.flex-shrink { + flex-shrink: 1; +} + +.flex-shrink-0 { + flex-shrink: 0; +} + +.shrink-0 { + flex-shrink: 0; +} + +.-translate-x-1\/2 { + --tw-translate-x: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-x-full { + --tw-translate-x: -100%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-y-1\/2 { + --tw-translate-y: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-y-3 { + --tw-translate-y: -0.75rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-y-8 { + --tw-translate-y: -2rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-y-\[120\%\] { + --tw-translate-y: -120%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-y-\[50px\] { + --tw-translate-y: -50px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-y-full { + --tw-translate-y: -100%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-y-5 { + --tw-translate-y: 1.25rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-y-\[27px\] { + --tw-translate-y: 27px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-y-full { + --tw-translate-y: 100%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.transform { + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.cursor-help { + cursor: help; +} + +.cursor-not-allowed { + cursor: not-allowed; +} + +.cursor-pointer { + cursor: pointer; +} + +.select-none { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.resize { + resize: both; +} + +.list-none { + list-style-type: none; +} + +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + +.grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.grid-cols-5 { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.flex-row { + flex-direction: row; +} + +.flex-col { + flex-direction: column; +} + +.flex-col-reverse { + flex-direction: column-reverse; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.items-start { + align-items: flex-start; +} + +.items-end { + align-items: flex-end; +} + +.items-center { + align-items: center; +} + +.justify-end { + justify-content: flex-end; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.gap-1 { + gap: 0.25rem; +} + +.gap-1\.5 { + gap: 0.375rem; +} + +.gap-2 { + gap: 0.5rem; +} + +.gap-3 { + gap: 0.75rem; +} + +.gap-4 { + gap: 1rem; +} + +.gap-5 { + gap: 1.25rem; +} + +.gap-6 { + gap: 1.5rem; +} + +.gap-8 { + gap: 2rem; +} + +.space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(1rem * var(--tw-space-x-reverse)); + margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-y-1 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.25rem * var(--tw-space-y-reverse)); +} + +.space-y-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); +} + +.space-y-3 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.75rem * var(--tw-space-y-reverse)); +} + +.space-y-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1rem * var(--tw-space-y-reverse)); +} + +.space-y-6 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); +} + +.divide-y > :not([hidden]) ~ :not([hidden]) { + --tw-divide-y-reverse: 0; + border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); +} + +.divide-\[var\(--user-transparent\)\] > :not([hidden]) ~ :not([hidden]) { + border-color: var(--user-transparent); +} + +.self-center { + align-self: center; +} + +.justify-self-center { + justify-self: center; +} + +.overflow-hidden { + overflow: hidden; +} + +.overflow-visible { + overflow: visible; +} + +.overflow-scroll { + overflow: scroll; +} + +.overflow-x-auto { + overflow-x: auto; +} + +.overflow-y-auto { + overflow-y: auto; +} + +.overflow-y-scroll { + overflow-y: scroll; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.overflow-ellipsis { + text-overflow: ellipsis; +} + +.whitespace-nowrap { + white-space: nowrap; +} + +.break-words { + overflow-wrap: break-word; +} + +.rounded { + border-radius: 0.25rem; +} + +.rounded-2xl { + border-radius: 1rem; +} + +.rounded-3xl { + border-radius: 1.5rem; +} + +.rounded-\[20px\] { + border-radius: 20px; +} + +.rounded-full { + border-radius: 9999px; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.rounded-md { + border-radius: 0.375rem; +} + +.rounded-xl { + border-radius: 0.75rem; +} + +.rounded-b-md { + border-bottom-right-radius: 0.375rem; + border-bottom-left-radius: 0.375rem; +} + +.rounded-l-md { + border-top-left-radius: 0.375rem; + border-bottom-left-radius: 0.375rem; +} + +.rounded-l-xl { + border-top-left-radius: 0.75rem; + border-bottom-left-radius: 0.75rem; +} + +.rounded-r-md { + border-top-right-radius: 0.375rem; + border-bottom-right-radius: 0.375rem; +} + +.rounded-r-xl { + border-top-right-radius: 0.75rem; + border-bottom-right-radius: 0.75rem; +} + +.rounded-t-2xl { + border-top-left-radius: 1rem; + border-top-right-radius: 1rem; +} + +.rounded-t-xl { + border-top-left-radius: 0.75rem; + border-top-right-radius: 0.75rem; +} + +.border { + border-width: 1px; +} + +.border-0 { + border-width: 0px; +} + +.border-2 { + border-width: 2px; +} + +.border-\[5px\] { + border-width: 5px; +} + +.border-b { + border-bottom-width: 1px; +} + +.border-l { + border-left-width: 1px; +} + +.border-r { + border-right-width: 1px; +} + +.border-t { + border-top-width: 1px; +} + +.border-dashed { + border-style: dashed; +} + +.border-\[var\(--user-base\)\] { + border-color: var(--user-base); +} + +.border-\[var\(--user-dark\)\] { + border-color: var(--user-dark); +} + +.border-\[var\(--user-light\)\] { + border-color: var(--user-light); +} + +.border-\[var\(--user-soft\)\] { + border-color: var(--user-soft); +} + +.border-\[var\(--user-transparent\)\] { + border-color: var(--user-transparent); +} + +.border-amber-500\/30 { + border-color: rgb(245 158 11 / 0.3); +} + +.border-black { + --tw-border-opacity: 1; + border-color: rgb(0 0 0 / var(--tw-border-opacity, 1)); +} + +.border-gray-300 { + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity, 1)); +} + +.border-gray-600 { + --tw-border-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-border-opacity, 1)); +} + +.border-gray-700 { + --tw-border-opacity: 1; + border-color: rgb(55 65 81 / var(--tw-border-opacity, 1)); +} + +.border-indigo-500 { + --tw-border-opacity: 1; + border-color: rgb(99 102 241 / var(--tw-border-opacity, 1)); +} + +.border-red-500\/30 { + border-color: rgb(239 68 68 / 0.3); +} + +.border-red-700 { + --tw-border-opacity: 1; + border-color: rgb(185 28 28 / var(--tw-border-opacity, 1)); +} + +.border-white { + --tw-border-opacity: 1; + border-color: rgb(255 255 255 / var(--tw-border-opacity, 1)); +} + +.border-white\/5 { + border-color: rgb(255 255 255 / 0.05); +} + +.border-yellow-600\/30 { + border-color: rgb(202 138 4 / 0.3); +} + +.border-yellow-600\/50 { + border-color: rgb(202 138 4 / 0.5); +} + +.\!bg-\[var\(--user-soft\)\] { + background-color: var(--user-soft) !important; +} + +.\!bg-gray-200 { + --tw-bg-opacity: 1 !important; + background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1)) !important; +} + +.bg-\[rgb\(76\2c 130\2c 163\)\] { + --tw-bg-opacity: 1; + background-color: rgb(76 130 163 / var(--tw-bg-opacity, 1)); +} + +.bg-\[var\(--user-base\)\] { + background-color: var(--user-base); +} + +.bg-\[var\(--user-dark\)\] { + background-color: var(--user-dark); +} + +.bg-\[var\(--user-light\)\] { + background-color: var(--user-light); +} + +.bg-\[var\(--user-soft\)\] { + background-color: var(--user-soft); +} + +.bg-\[var\(--user-transparent\)\] { + background-color: var(--user-transparent); +} + +.bg-amber-500\/10 { + background-color: rgb(245 158 11 / 0.1); +} + +.bg-amber-700\/50 { + background-color: rgb(180 83 9 / 0.5); +} + +.bg-black { + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity, 1)); +} + +.bg-black\/50 { + background-color: rgb(0 0 0 / 0.5); +} + +.bg-black\/70 { + background-color: rgb(0 0 0 / 0.7); +} + +.bg-emerald-400 { + --tw-bg-opacity: 1; + background-color: rgb(52 211 153 / var(--tw-bg-opacity, 1)); +} + +.bg-emerald-600 { + --tw-bg-opacity: 1; + background-color: rgb(5 150 105 / var(--tw-bg-opacity, 1)); +} + +.bg-gray-200 { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1)); +} + +.bg-gray-500\/10 { + background-color: rgb(107 114 128 / 0.1); +} + +.bg-gray-600 { + --tw-bg-opacity: 1; + background-color: rgb(75 85 99 / var(--tw-bg-opacity, 1)); +} + +.bg-gray-700 { + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1)); +} + +.bg-gray-700\/30 { + background-color: rgb(55 65 81 / 0.3); +} + +.bg-gray-800 { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1)); +} + +.bg-gray-800\/60 { + background-color: rgb(31 41 55 / 0.6); +} + +.bg-gray-800\/70 { + background-color: rgb(31 41 55 / 0.7); +} + +.bg-gray-800\/80 { + background-color: rgb(31 41 55 / 0.8); +} + +.bg-gray-900\/50 { + background-color: rgb(17 24 39 / 0.5); +} + +.bg-gray-900\/60 { + background-color: rgb(17 24 39 / 0.6); +} + +.bg-green-500 { + --tw-bg-opacity: 1; + background-color: rgb(34 197 94 / var(--tw-bg-opacity, 1)); +} + +.bg-green-600 { + --tw-bg-opacity: 1; + background-color: rgb(22 163 74 / var(--tw-bg-opacity, 1)); +} + +.bg-indigo-400 { + --tw-bg-opacity: 1; + background-color: rgb(129 140 248 / var(--tw-bg-opacity, 1)); +} + +.bg-indigo-600 { + --tw-bg-opacity: 1; + background-color: rgb(79 70 229 / var(--tw-bg-opacity, 1)); +} + +.bg-pink-500 { + --tw-bg-opacity: 1; + background-color: rgb(236 72 153 / var(--tw-bg-opacity, 1)); +} + +.bg-red-400 { + --tw-bg-opacity: 1; + background-color: rgb(248 113 113 / var(--tw-bg-opacity, 1)); +} + +.bg-red-500\/10 { + background-color: rgb(239 68 68 / 0.1); +} + +.bg-red-600\/20 { + background-color: rgb(220 38 38 / 0.2); +} + +.bg-red-900\/30 { + background-color: rgb(127 29 29 / 0.3); +} + +.bg-rose-500\/20 { + background-color: rgb(244 63 94 / 0.2); +} + +.bg-transparent { + background-color: transparent; +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); +} + +.bg-white\/10 { + background-color: rgb(255 255 255 / 0.1); +} + +.bg-yellow-400 { + --tw-bg-opacity: 1; + background-color: rgb(250 204 21 / var(--tw-bg-opacity, 1)); +} + +.bg-yellow-500\/20 { + background-color: rgb(234 179 8 / 0.2); +} + +.bg-yellow-900\/30 { + background-color: rgb(113 63 18 / 0.3); +} + +.bg-yellow-900\/50 { + background-color: rgb(113 63 18 / 0.5); +} + +.bg-opacity-0 { + --tw-bg-opacity: 0; +} + +.bg-gradient-to-b { + background-image: linear-gradient(to bottom, var(--tw-gradient-stops)); +} + +.bg-gradient-to-t { + background-image: linear-gradient(to top, var(--tw-gradient-stops)); +} + +.from-black\/70 { + --tw-gradient-from: rgb(0 0 0 / 0.7) var(--tw-gradient-from-position); + --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); +} + +.from-gray-900 { + --tw-gradient-from: #111827 var(--tw-gradient-from-position); + --tw-gradient-to: rgb(17 24 39 / 0) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); +} + +.to-gray-700 { + --tw-gradient-to: #374151 var(--tw-gradient-to-position); +} + +.to-transparent { + --tw-gradient-to: transparent var(--tw-gradient-to-position); +} + +.bg-cover { + background-size: cover; +} + +.bg-center { + background-position: center; +} + +.object-contain { + -o-object-fit: contain; + object-fit: contain; +} + +.object-cover { + -o-object-fit: cover; + object-fit: cover; +} + +.object-bottom { + -o-object-position: bottom; + object-position: bottom; +} + +.p-0 { + padding: 0px; +} + +.p-1 { + padding: 0.25rem; +} + +.p-1\.5 { + padding: 0.375rem; +} + +.p-12 { + padding: 3rem; +} + +.p-2 { + padding: 0.5rem; +} + +.p-3 { + padding: 0.75rem; +} + +.p-4 { + padding: 1rem; +} + +.p-5 { + padding: 1.25rem; +} + +.p-6 { + padding: 1.5rem; +} + +.p-8 { + padding: 2rem; +} + +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.px-3\.5 { + padding-left: 0.875rem; + padding-right: 0.875rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.px-5 { + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.px-8 { + padding-left: 2rem; + padding-right: 2rem; +} + +.py-0\.5 { + padding-top: 0.125rem; + padding-bottom: 0.125rem; +} + +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.py-1\.5 { + padding-top: 0.375rem; + padding-bottom: 0.375rem; +} + +.py-12 { + padding-top: 3rem; + padding-bottom: 3rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.py-2\.5 { + padding-top: 0.625rem; + padding-bottom: 0.625rem; +} + +.py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + +.pb-1 { + padding-bottom: 0.25rem; +} + +.pb-2 { + padding-bottom: 0.5rem; +} + +.pb-24 { + padding-bottom: 6rem; +} + +.pb-3 { + padding-bottom: 0.75rem; +} + +.pb-4 { + padding-bottom: 1rem; +} + +.pb-6 { + padding-bottom: 1.5rem; +} + +.pb-\[env\(safe-area-inset-bottom\)\] { + padding-bottom: env(safe-area-inset-bottom); +} + +.pb-\[max\(0\.5rem\2c env\(safe-area-inset-bottom\)\)\] { + padding-bottom: max(0.5rem, env(safe-area-inset-bottom)); +} + +.pl-1 { + padding-left: 0.25rem; +} + +.pl-2 { + padding-left: 0.5rem; +} + +.pr-1 { + padding-right: 0.25rem; +} + +.pr-2 { + padding-right: 0.5rem; +} + +.pr-4 { + padding-right: 1rem; +} + +.pt-1 { + padding-top: 0.25rem; +} + +.pt-10 { + padding-top: 2.5rem; +} + +.pt-2 { + padding-top: 0.5rem; +} + +.pt-6 { + padding-top: 1.5rem; +} + +.text-left { + text-align: left; +} + +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.align-middle { + vertical-align: middle; +} + +.font-mono { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +.\!text-2xl { + font-size: 1.5rem !important; + line-height: 2rem !important; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; +} + +.text-5xl { + font-size: 3rem; + line-height: 1; +} + +.text-\[10px\] { + font-size: 10px; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.text-xs { + font-size: 0.75rem; + line-height: 1rem; +} + +.font-bold { + font-weight: 700; +} + +.font-light { + font-weight: 300; +} + +.font-medium { + font-weight: 500; +} + +.font-semibold { + font-weight: 600; +} + +.uppercase { + text-transform: uppercase; +} + +.leading-relaxed { + line-height: 1.625; +} + +.leading-snug { + line-height: 1.375; +} + +.leading-tight { + line-height: 1.25; +} + +.tracking-wide { + letter-spacing: 0.025em; +} + +.tracking-wider { + letter-spacing: 0.05em; +} + +.tracking-widest { + letter-spacing: 0.1em; +} + +.text-\[var\(--user-soft\)\] { + color: var(--user-soft); +} + +.text-emerald-300 { + --tw-text-opacity: 1; + color: rgb(110 231 183 / var(--tw-text-opacity, 1)); +} + +.text-emerald-800 { + --tw-text-opacity: 1; + color: rgb(6 95 70 / var(--tw-text-opacity, 1)); +} + +.text-gray-200 { + --tw-text-opacity: 1; + color: rgb(229 231 235 / var(--tw-text-opacity, 1)); +} + +.text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity, 1)); +} + +.text-gray-500 { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity, 1)); +} + +.text-gray-700 { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity, 1)); +} + +.text-gray-800 { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity, 1)); +} + +.text-gray-900 { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity, 1)); +} + +.text-red-200 { + --tw-text-opacity: 1; + color: rgb(254 202 202 / var(--tw-text-opacity, 1)); +} + +.text-red-300 { + --tw-text-opacity: 1; + color: rgb(252 165 165 / var(--tw-text-opacity, 1)); +} + +.text-red-400 { + --tw-text-opacity: 1; + color: rgb(248 113 113 / var(--tw-text-opacity, 1)); +} + +.text-red-800 { + --tw-text-opacity: 1; + color: rgb(153 27 27 / var(--tw-text-opacity, 1)); +} + +.text-rose-300 { + --tw-text-opacity: 1; + color: rgb(253 164 175 / var(--tw-text-opacity, 1)); +} + +.text-rose-400 { + --tw-text-opacity: 1; + color: rgb(251 113 133 / var(--tw-text-opacity, 1)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity, 1)); +} + +.text-white\/30 { + color: rgb(255 255 255 / 0.3); +} + +.text-white\/50 { + color: rgb(255 255 255 / 0.5); +} + +.text-white\/80 { + color: rgb(255 255 255 / 0.8); +} + +.text-yellow-200 { + --tw-text-opacity: 1; + color: rgb(254 240 138 / var(--tw-text-opacity, 1)); +} + +.text-yellow-300 { + --tw-text-opacity: 1; + color: rgb(253 224 71 / var(--tw-text-opacity, 1)); +} + +.text-yellow-800 { + --tw-text-opacity: 1; + color: rgb(133 77 14 / var(--tw-text-opacity, 1)); +} + +.placeholder-gray-500::-moz-placeholder { + --tw-placeholder-opacity: 1; + color: rgb(107 114 128 / var(--tw-placeholder-opacity, 1)); +} + +.placeholder-gray-500::placeholder { + --tw-placeholder-opacity: 1; + color: rgb(107 114 128 / var(--tw-placeholder-opacity, 1)); +} + +.opacity-0 { + opacity: 0; +} + +.opacity-10 { + opacity: 0.1; +} + +.opacity-100 { + opacity: 1; +} + +.opacity-30 { + opacity: 0.3; +} + +.opacity-40 { + opacity: 0.4; +} + +.opacity-5 { + opacity: 0.05; +} + +.opacity-50 { + opacity: 0.5; +} + +.opacity-55 { + opacity: 0.55; +} + +.opacity-60 { + opacity: 0.6; +} + +.opacity-70 { + opacity: 0.7; +} + +.opacity-80 { + opacity: 0.8; +} + +.shadow-2xl { + --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25); + --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-lg { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-sm { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-xl { + --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.outline-none { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.outline { + outline-style: solid; +} + +.blur { + --tw-blur: blur(8px); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.blur-lg { + --tw-blur: blur(16px); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.blur-md { + --tw-blur: blur(12px); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.blur-sm { + --tw-blur: blur(4px); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.blur-xl { + --tw-blur: blur(24px); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.drop-shadow { + --tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06)); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.grayscale { + --tw-grayscale: grayscale(100%); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.invert { + --tw-invert: invert(100%); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.backdrop-blur-lg { + --tw-backdrop-blur: blur(16px); + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + +.backdrop-blur-md { + --tw-backdrop-blur: blur(12px); + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + +.backdrop-blur-sm { + --tw-backdrop-blur: blur(4px); + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + +.backdrop-blur-xl { + --tw-backdrop-blur: blur(24px); + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + +.backdrop-filter { + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + +.transition { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-all { + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-colors { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-opacity { + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-transform { + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.duration-200 { + transition-duration: 200ms; +} + +.duration-300 { + transition-duration: 300ms; +} + +.ease-out { + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); +} + +.\[color-scheme\:dark\] { + color-scheme: dark; +} + +.hover\:scale-110:hover { + --tw-scale-x: 1.1; + --tw-scale-y: 1.1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.hover\:border-\[var\(--user-soft\)\]:hover { + border-color: var(--user-soft); +} + +.hover\:border-indigo-500:hover { + --tw-border-opacity: 1; + border-color: rgb(99 102 241 / var(--tw-border-opacity, 1)); +} + +.hover\:bg-\[var\(--user-base\)\]:hover { + background-color: var(--user-base); +} + +.hover\:bg-\[var\(--user-dark\)\]:hover { + background-color: var(--user-dark); +} + +.hover\:bg-amber-700\/60:hover { + background-color: rgb(180 83 9 / 0.6); +} + +.hover\:bg-gray-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(75 85 99 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-gray-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-gray-700\/60:hover { + background-color: rgb(55 65 81 / 0.6); +} + +.hover\:bg-green-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(21 128 61 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-indigo-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(79 70 229 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-indigo-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(67 56 202 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-red-600\/30:hover { + background-color: rgb(220 38 38 / 0.3); +} + +.hover\:text-white:hover { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity, 1)); +} + +.hover\:opacity-100:hover { + opacity: 1; +} + +.hover\:shadow-2xl:hover { + --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25); + --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.hover\:shadow-lg:hover { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.hover\:shadow-xl:hover { + --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.focus\:border-\[var\(--user-light\)\]:focus { + border-color: var(--user-light); +} + +.focus\:border-indigo-500:focus { + --tw-border-opacity: 1; + border-color: rgb(99 102 241 / var(--tw-border-opacity, 1)); +} + +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.focus\:ring-1:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.focus\:ring-indigo-500:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity, 1)); +} + +.active\:bg-green-800:active { + --tw-bg-opacity: 1; + background-color: rgb(22 101 52 / var(--tw-bg-opacity, 1)); +} + +.active\:bg-indigo-600:active { + --tw-bg-opacity: 1; + background-color: rgb(79 70 229 / var(--tw-bg-opacity, 1)); +} + +.disabled\:cursor-not-allowed:disabled { + cursor: not-allowed; +} + +.disabled\:opacity-50:disabled { + opacity: 0.5; +} + +.group:hover .group-hover\:translate-x-1 { + --tw-translate-x: 0.25rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.group:hover .group-hover\:bg-opacity-50 { + --tw-bg-opacity: 0.5; +} + +.group:hover .group-hover\:opacity-100 { + opacity: 1; +} + +@media (min-width: 640px) { + .sm\:absolute { + position: absolute; + } + + .sm\:left-1\/2 { + left: 50%; + } + + .sm\:mt-0 { + margin-top: 0px; + } + + .sm\:mt-72 { + margin-top: 18rem; + } + + .sm\:block { + display: block; + } + + .sm\:grid { + display: grid; + } + + .sm\:hidden { + display: none; + } + + .sm\:h-12 { + height: 3rem; + } + + .sm\:h-14 { + height: 3.5rem; + } + + .sm\:h-\[calc\(100\%-80px\)\] { + height: calc(100% - 80px); + } + + .sm\:w-12 { + width: 3rem; + } + + .sm\:w-14 { + width: 3.5rem; + } + + .sm\:w-56 { + width: 14rem; + } + + .sm\:-translate-x-1\/2 { + --tw-translate-x: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + } + + .sm\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .sm\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .sm\:flex-row { + flex-direction: row; + } + + .sm\:flex-wrap { + flex-wrap: wrap; + } + + .sm\:items-end { + align-items: flex-end; + } + + .sm\:items-center { + align-items: center; + } + + .sm\:justify-between { + justify-content: space-between; + } + + .sm\:gap-3 { + gap: 0.75rem; + } + + .sm\:overflow-visible { + overflow: visible; + } + + .sm\:p-12 { + padding: 3rem; + } + + .sm\:p-6 { + padding: 1.5rem; + } + + .sm\:p-8 { + padding: 2rem; + } + + .sm\:px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; + } + + .sm\:py-12 { + padding-top: 3rem; + padding-bottom: 3rem; + } + + .sm\:pt-0 { + padding-top: 0px; + } + + .sm\:text-left { + text-align: left; + } + + .sm\:\!text-4xl { + font-size: 2.25rem !important; + line-height: 2.5rem !important; + } + + .sm\:text-2xl { + font-size: 1.5rem; + line-height: 2rem; + } + + .sm\:text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; + } + + .sm\:text-5xl { + font-size: 3rem; + line-height: 1; + } +} + +@media (min-width: 768px) { + .md\:bottom-auto { + bottom: auto; + } + + .md\:left-\[300px\] { + left: 300px; + } + + .md\:right-6 { + right: 1.5rem; + } + + .md\:right-auto { + right: auto; + } + + .md\:top-0 { + top: 0px; + } + + .md\:top-6 { + top: 1.5rem; + } + + .md\:col-span-2 { + grid-column: span 2 / span 2; + } + + .md\:ml-\[300px\] { + margin-left: 300px; + } + + .md\:block { + display: block; + } + + .md\:flex { + display: flex; + } + + .md\:hidden { + display: none; + } + + .md\:h-screen { + height: 100vh; + } + + .md\:w-1\/2 { + width: 50%; + } + + .md\:w-\[calc\(100\%-300px\)\] { + width: calc(100% - 300px); + } + + .md\:w-full { + width: 100%; + } + + .md\:max-w-\[300px\] { + max-width: 300px; + } + + .md\:-translate-x-\[calc\(50\%-140px\)\] { + --tw-translate-x: calc(calc(50% - 140px) * -1); + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + } + + .md\:translate-x-0 { + --tw-translate-x: 0px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + } + + .md\:translate-y-0 { + --tw-translate-y: 0px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + } + + .md\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .md\:flex-row { + flex-direction: row; + } + + .md\:rounded-none { + border-radius: 0px; + } + + .md\:border-r { + border-right-width: 1px; + } + + .md\:border-t-0 { + border-top-width: 0px; + } + + .md\:pb-0 { + padding-bottom: 0px; + } +} + +@media (min-width: 1024px) { + .lg\:bottom-5 { + bottom: 1.25rem; + } + + .lg\:left-full { + left: 100%; + } + + .lg\:top-0 { + top: 0px; + } + + .lg\:top-auto { + top: auto; + } + + .lg\:-ml-4 { + margin-left: -1rem; + } + + .lg\:block { + display: block; + } + + .lg\:inline-block { + display: inline-block; + } + + .lg\:grid { + display: grid; + } + + .lg\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .lg\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .lg\:flex-row { + flex-direction: row; + } + + .lg\:px-8 { + padding-left: 2rem; + padding-right: 2rem; + } +} + +@media (min-width: 1280px) { + .xl\:flex { + display: flex; + } + + .xl\:max-h-\[460px\] { + max-height: 460px; + } + + .xl\:w-\[500px\] { + width: 500px; + } + + .xl\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .xl\:flex-row { + flex-direction: row; + } + + .xl\:items-center { + align-items: center; + } + + .xl\:pr-1 { + padding-right: 0.25rem; + } +} diff --git a/static/icon/bell.svg b/static/icon/bell.svg new file mode 100644 index 0000000..04323eb --- /dev/null +++ b/static/icon/bell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/sw.js b/static/sw.js index f9335ce..131780d 100644 --- a/static/sw.js +++ b/static/sw.js @@ -54,3 +54,35 @@ self.addEventListener("fetch", (event) => { .catch(() => caches.match(event.request)) ); }); + +/* Web Push notifications. */ +self.addEventListener("push", (event) => { + let data = {}; + try { + data = event.data.json(); + } catch (err) { + data = { body: event.data ? event.data.text() : "" }; + } + event.waitUntil( + self.registration.showNotification(data.title || "WiiLink Checkout", { + body: data.body || "", + icon: data.icon || "/static/icons/pwa-192.png", + badge: "/static/icons/pwa-192.png", + tag: data.tag || undefined, + data: { url: data.url || "/" }, + }) + ); +}); + +self.addEventListener("notificationclick", (event) => { + event.notification.close(); + const url = (event.notification.data && event.notification.data.url) || "/"; + event.waitUntil( + self.clients.matchAll({ type: "window", includeUncontrolled: true }).then((clientList) => { + for (const client of clientList) { + if (client.url.includes(url) && "focus" in client) return client.focus(); + } + return self.clients.openWindow(url); + }) + ); +}); diff --git a/templates/generic/base.html b/templates/generic/base.html index 5cd5995..98ed034 100644 --- a/templates/generic/base.html +++ b/templates/generic/base.html @@ -462,7 +462,9 @@

Friends

{{ user_info.achievements.points.balance if user_info.achievements and user_info.achievements.points else 0 }} pts. {% if is_coupon_admin %} - {{ menu_item('Coupon Admin', 'coupons_admin.panel', 'badge-dollar-sign') }} + {{ menu_section('ADMINISTRATION') }} + {{ menu_item('Coupon Management', 'coupons_admin.panel', 'badge-dollar-sign') }} + {{ menu_item('Notification Management', 'notifications_admin.panel', 'bell') }} {% endif %} {{ menu_item_external('Account Settings', 'https://accounts.wiilink.ca/manage', 'settings') }} diff --git a/templates/notifications_admin.html b/templates/notifications_admin.html new file mode 100644 index 0000000..8ff8460 --- /dev/null +++ b/templates/notifications_admin.html @@ -0,0 +1,69 @@ +{% extends "generic/base.html" %} + +{% block title %}WiiLink Checkout | Notification Management{% endblock %} + +{% block content %} +
+
+
+
+

Notification Management

+
+
+ +
+
+

Send Notification

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+

Icon support varies by browser (Safari ignores custom icons and always uses the site icon). Users who disabled the chosen category are skipped.

+ +
+
+
+
+
+{% endblock %} diff --git a/templates/notifications_disable.html b/templates/notifications_disable.html new file mode 100644 index 0000000..b308d03 --- /dev/null +++ b/templates/notifications_disable.html @@ -0,0 +1,53 @@ + + + + + + + Disable Notifications | WiiLink Checkout + + + + + +
+ Notifications +

Disable Notifications

+

Working…

+
+ + + + + diff --git a/templates/notifications_enable.html b/templates/notifications_enable.html new file mode 100644 index 0000000..0cfc97d --- /dev/null +++ b/templates/notifications_enable.html @@ -0,0 +1,91 @@ + + + + + + + Enable Notifications | WiiLink Checkout + + + + + +
+ Notifications +

Enable Notifications

+

Get push notifications from WiiLink services on this device through WiiLink + Checkout. Your browser will ask for permission.

+

+ {% if status_message %}{{ status_message }}{% else %}Click the button below to get started.{% endif %} +

+ {% if public_key %} + + {% endif %} +
+ + + + + diff --git a/templates/recommendations.html b/templates/recommendations.html index 911f7cb..65a4a9e 100644 --- a/templates/recommendations.html +++ b/templates/recommendations.html @@ -13,7 +13,7 @@ {% set sort_base = base_url ~ '/recommendations' if base_url else '/recommendations' %} {% if not is_unclaimed %}
+ class="fixed top-0 lg:top-auto lg:bottom-5 left-1/2 -translate-x-1/2 md:-translate-x-[calc(50%-140px)] p-3 rounded-xl bg-gray-800/70 backdrop-blur-lg flex flex-row items-center justify-center gap-2 mt-6 z-10"> Best Rated diff --git a/templates/time_played.html b/templates/time_played.html index 4bdbe4e..455383d 100644 --- a/templates/time_played.html +++ b/templates/time_played.html @@ -10,7 +10,7 @@ {% if not is_unclaimed %}
+ class="fixed top-0 lg:top-auto lg:bottom-5 left-1/2 -translate-x-1/2 md:-translate-x-[calc(50%-140px)] p-3 rounded-xl bg-gray-800/70 backdrop-blur-lg flex flex-row items-center justify-center gap-2 mt-6 z-10"> {% set sort_base = base_url ~ '/time_played' if base_url else '/time_played' %} diff --git a/utils/db.py b/utils/db.py index a4a6b21..35efd32 100644 --- a/utils/db.py +++ b/utils/db.py @@ -7,7 +7,16 @@ import config from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import Integer, String, Text, func, text +from sqlalchemy import ( + Boolean, + Index, + Integer, + PrimaryKeyConstraint, + String, + Text, + func, + text, +) from sqlalchemy.dialects.postgresql import JSONB, TIMESTAMP, UUID from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column @@ -65,6 +74,7 @@ class Coupon(db.Model): class CouponRedemption(db.Model): __bind_key__ = CHECKOUT_BIND __tablename__ = "coupon_redemptions" + __table_args__ = (Index("idx_coupon_redemptions_coupon", "coupon_uuid"),) id: Mapped[str] = mapped_column( UUID(as_uuid=False), @@ -81,9 +91,35 @@ class CouponRedemption(db.Model): class Friend(db.Model): __bind_key__ = CHECKOUT_BIND __tablename__ = "friends" + __table_args__ = (Index("idx_friends_followed", "followed_wii_number"),) follower_wii_number: Mapped[str] = mapped_column(String(16), primary_key=True) followed_wii_number: Mapped[str] = mapped_column(String(16), primary_key=True) created_at: Mapped[datetime] = mapped_column( TIMESTAMP(timezone=True), server_default=func.now() ) + + +class PushSubscription(db.Model): + __bind_key__ = CHECKOUT_BIND + __tablename__ = "push_subscriptions" + + endpoint: Mapped[str] = mapped_column(Text, primary_key=True) + username: Mapped[str] = mapped_column(Text, index=True) + p256dh: Mapped[str] = mapped_column(Text) + auth: Mapped[str] = mapped_column(Text) + created_at: Mapped[datetime] = mapped_column( + TIMESTAMP(timezone=True), server_default=func.now() + ) + + +class NotificationPreference(db.Model): + __bind_key__ = CHECKOUT_BIND + __tablename__ = "notification_preferences" + __table_args__ = (PrimaryKeyConstraint("username", "category"),) + + username: Mapped[str] = mapped_column(Text, primary_key=True) + category: Mapped[str] = mapped_column(Text, primary_key=True) + enabled: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default=text("true") + ) diff --git a/utils/push.py b/utils/push.py new file mode 100644 index 0000000..a334704 --- /dev/null +++ b/utils/push.py @@ -0,0 +1,125 @@ +import json +import logging + +import config +from pywebpush import WebPushException, webpush + +from utils.db import NotificationPreference, PushSubscription, db + +logger = logging.getLogger(__name__) + +# Categories a notification can target +CATEGORIES = frozenset( + { + "nintendo_channel", + "evc", + "cmoc", + "wii_room", + "general_announcements", + "critical_announcements", + "newsletter_announcements", + "food_channel", + "digicam_prints", + "kirby_tv_channel", + } +) + + +def _payload(title, body, url, tag, icon=None): + return { + "title": title or "WiiLink Checkout", + "body": body or "", + "url": url or "/", + "tag": tag, + # The service worker falls back to the Checkout PWA icon when empty. + "icon": icon or "", + } + + +def _category_disabled_usernames(category): + rows = db.session.execute( + db.select(NotificationPreference.username).where( + NotificationPreference.category == category, + NotificationPreference.enabled.is_(False), + ) + ).scalars() + return set(rows) + + +def _deliver(subscription, payload): + """Send one notification; returns (delivered, stale).""" + try: + webpush( + subscription_info={ + "endpoint": subscription.endpoint, + "keys": {"p256dh": subscription.p256dh, "auth": subscription.auth}, + }, + data=json.dumps(payload), + vapid_private_key=config.vapid_private_key, + vapid_claims={"sub": config.vapid_subject}, + ) + return True, False + except WebPushException as exc: + status = getattr(exc.response, "status_code", None) + # 404/410 means the subscription is gone (browser unsubscribed/expired). + return False, status in (404, 410) + except Exception: + logger.exception("Unexpected error pushing to %s", subscription.endpoint) + return False, False + + +def _prune(endpoint): + db.session.execute( + db.delete(PushSubscription).where(PushSubscription.endpoint == endpoint) + ) + db.session.commit() + + +def send_push_to_user( + username, title, body="", url="/", tag=None, category=None, icon=None +): + if category: + pref = db.session.execute( + db.select(NotificationPreference).where( + NotificationPreference.username == username, + NotificationPreference.category == category, + ) + ).scalar_one_or_none() + if pref is not None and not pref.enabled: + return 0 + + payload = _payload(title, body, url, tag, icon) + subscriptions = ( + db.session.execute( + db.select(PushSubscription).where(PushSubscription.username == username) + ) + .scalars() + .all() + ) + + sent = 0 + for subscription in subscriptions: + delivered, stale = _deliver(subscription, payload) + if stale: + _prune(subscription.endpoint) + if delivered: + sent += 1 + return sent + + +def send_push_to_all(title, body="", url="/", tag=None, category=None, icon=None): + payload = _payload(title, body, url, tag, icon) + subscriptions = db.session.execute(db.select(PushSubscription)).scalars().all() + + skipped = _category_disabled_usernames(category) if category else set() + + sent = 0 + for subscription in subscriptions: + if subscription.username in skipped: + continue + delivered, stale = _deliver(subscription, payload) + if stale: + _prune(subscription.endpoint) + if delivered: + sent += 1 + return sent