From 74cd08f765bc4a5ef79b53ba482a45527a2af0e2 Mon Sep 17 00:00:00 2001 From: ChrisBeWithYou Date: Thu, 2 Jul 2026 06:47:55 -0500 Subject: [PATCH] library: MusicBrainz text matching + Match-Review UI (P8) (#710) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * library: MusicBrainz text matching + Match-Review UI — P8 Replaces the enrichment plumbing's no-op matcher (P7) with the real pipeline, per the library-metadata design: a wrong match is worse than a slow one, so medium confidence goes to a human review queue and never straight to canonical values. - lib/mb_match.py (new, pure — no network/DB/server imports): denoise (author credits, (440Hz)/(Live)/(No Lead)/(v2) parentheticals, diacritics/punctuation, ACDC / AC DC / AC/DC folding via compacted token equality), token-set similarity, scoring with year/duration corroboration bonuses, tier classification (auto needs combined >= 0.95 AND per-field floors — a perfect-title cover by the wrong artist, or a chart with no artist, can never auto-match), Lucene query building, MusicBrainz response normalization. - Matcher precedence in _enrich_one: content-hash cache copy (another chart of the same recording matches with no network) -> manifest mbid (tier 0) / isrc (tier 1) exact keys, feature-detected and strictly shape-validated, read-only -> text search tiers (auto / review / failed). - Lifecycle: review rows store their ranked candidate list (JSON) and write NO canonical fields until a human accepts; failed rows retry on an exponential backoff (1 h doubling, 7 d cap) via the attempts column; user-rejected rows never auto-retry; an identity edit re-queues anything and resets the backoff; never-overwrite-manual is enforced inside the single writer (apply_enrichment_match) so no call path can forget it. - Network: _mb_http_get is the one transport seam — throttled to <= 1 req/s through P7's _enrich_throttle, identified with a real User-Agent from VERSION, and a 503 pauses the whole pass without burning attempts. Offline guard: no sockets under FEEDBACK_ENRICH_OFFLINE or FEEDBACK_SKIP_STARTUP_TASKS, so pytest can never reach MusicBrainz; the pass still stamps identity hashes (two-phase), which is why every P7 test passes unchanged. - Routes: GET /api/enrichment/review, POST /api/enrichment/review/{filename}/accept|reject|pick, GET /api/enrichment/search (throttled manual-search proxy). All four are demo-mode blocked. - Match facet: match= CSV accepted by /api/library AND /api/library/stats (the A-Z rail's letter counts stay lockstep with the grid) — review / matched (incl. manual) / unmatched / pending, the same EXISTS idiom as the mastery facet. - UI: static/v3/match-review.js (new, self-contained) — an ambient "N to review" chip beside the song count (rendered only when non-zero; silent on success, no toasts), and a review drawer on the filter-drawer slide idiom (Escape + focus trap; row click accepts, "Not a match" rejects, "Search instead" is the fix-match escape hatch). songs.js gets the chip mount, a Match filter section, and session-only match state; also fixes the latent applySavedPrefs bug where restored filters dropped the mastery key, which made the filter drawer throw for anyone with saved prefs. - static/tailwind.min.css regenerated (scripts/build-tailwind.sh) for the new utility classes; conflicts with sibling PRs resolve by re-running the script. Nothing is ever written to pack files — canonical values live only in the song_enrichment display cache. Cover art caching and acoustic fingerprinting are follow-up slices. 22 pure unit tests + 19 server tests (fake transport injected over the _mb_http_get seam) + demo-mode route cases; full-suite failure set A/B-identical with the change stashed vs applied. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Nm7tHs1Yvjjtnnu4nzJgdN * library: match-review modal + configurable auto-apply confidence (P8 R0) Follow-up to the initial P8 commit, folding in the first round of tester feedback on the review surface and the matcher's knobs: - Review GUI is a centred MODAL now, not a sidebar — one chart at a time (the scraper-review model from media-server / emulation-frontend apps): the chart's current metadata with explicit amber "Missing: album / year / cover art" chips (art detected via the art request failing), candidates each carrying "Adds: year - genres - ISRC" / "Shows as: ACDC -> AC/DC" per-field chips, and Skip / Not a match / Search instead / Use selected with prev-next + arrow-key navigation. Chip + window API surface unchanged, so songs.js needed no edits for the rework. - Auto-apply confidence is a SETTING: default drops 0.95 -> 0.90 (mb_match.AUTO_MIN; classify() takes an auto_min override). The per-field floors are untouched and threshold-independent — a perfect-title cover by the wrong artist still can't auto-match at any setting. New validated settings keys: enrich_enabled (bool) + enrich_auto_threshold (0.5–1.01; >1.0 = "Always review", since a capped score can equal exactly 1.0). Read once per pass; disabling gates only the BACKGROUND matcher — manual search/fix stays available. - Settings -> Library -> "Metadata matching" card: enable toggle, confidence select (85 / 90 / 95 / Always review), a Match Now button (new POST /api/enrichment/kick, single-flight like every other kick, demo-mode blocked), and a live status line fed by the same fetch as the review chip. Markup in index.html per the v3 settings pattern, wired by match-review.js, null-guarded so v2 no-ops. - Review queue orders missing-data charts first — confirming those has the most to gain; complete charts only stand to be re-labelled. Tests: threshold moves the auto/review boundary via settings; the enable toggle gates matching but not the manual proxy; settings validation; kick route; queue ordering; classify(auto_min=...) floors. Full-suite failure set byte-identical to the pre-change baseline. tailwind.min.css regenerated for the modal's utility classes. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Nm7tHs1Yvjjtnnu4nzJgdN * fix(library): lock MusicBrainz throttle across sleep + de-dup enrich queue (PR #710 review) Hold a module-level lock across _enrich_throttle's read/sleep/write so the background daemon and threadpooled sync search route serialize outbound MB requests instead of bursting past the 1 req/s limit. De-dup the enrich queue by filename so a changed-hash failed row isn't processed twice per pass. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Fable 5 Co-authored-by: byrongamatos --- lib/mb_match.py | 295 +++++++++++++++ server.py | 712 +++++++++++++++++++++++++++++++++--- static/tailwind.min.css | 2 +- static/v3/index.html | 25 ++ static/v3/match-review.js | 414 +++++++++++++++++++++ static/v3/songs.js | 27 +- tests/test_demo_mode.py | 7 + tests/test_mb_enrichment.py | 512 ++++++++++++++++++++++++++ tests/test_mb_match.py | 227 ++++++++++++ 9 files changed, 2173 insertions(+), 48 deletions(-) create mode 100644 lib/mb_match.py create mode 100644 static/v3/match-review.js create mode 100644 tests/test_mb_enrichment.py create mode 100644 tests/test_mb_match.py diff --git a/lib/mb_match.py b/lib/mb_match.py new file mode 100644 index 0000000..2efb233 --- /dev/null +++ b/lib/mb_match.py @@ -0,0 +1,295 @@ +"""Text-matching engine for MusicBrainz metadata enrichment (P8). + +Pure functions only — no network, no database, no server imports — so the +whole matching pipeline is unit-testable in isolation. server.py owns the +throttled HTTP transport and the song_enrichment writes; this module owns: + +* denoise/tokenize: fold community chart-title noise (author suffixes, + ``(440Hz)``/``(Live)``/``(No Lead)``/``(v2)`` parentheticals, punctuation, + diacritics, ``AC DC``/``ACDC``/``AC/DC`` spelling drift) into a comparable + token form, +* similarity + scoring: token-set similarity on artist+title with year and + duration proximity as corroborating bonuses, +* tier classification: auto (high) / review (medium) / none (low) — the + design rule is that a WRONG match is worse than no match, so the auto + tier is deliberately strict and medium confidence goes to a human, +* MusicBrainz JSON parsing: normalize ``/ws/2`` recording documents into + the flat candidate dicts the review UI and song_enrichment store. +""" + +import re +import unicodedata + +# ── Tier thresholds ─────────────────────────────────────────────────────────── +# Combined score = 0.5*artist_sim + 0.5*title_sim + corroboration bonuses +# (capped at 1.0). Wrong-match is worse than slow (design §5), so `auto` +# additionally requires BOTH fields to individually agree — a perfect title +# with a mismatched artist (a cover) must never auto-canonicalize, whatever +# the combined threshold is set to. AUTO_MIN is only the DEFAULT: the host +# surfaces it as the user-configurable "auto-apply confidence" setting and +# passes the chosen value into classify(auto_min=…). +AUTO_MIN = 0.90 +AUTO_ARTIST_MIN = 0.8 +AUTO_TITLE_MIN = 0.6 +REVIEW_MIN = 0.65 + +YEAR_BONUS = 0.05 # candidate year within ±1 of the chart's year +DURATION_BONUS = 0.05 # candidate length within 5s of the chart's audio +DURATION_BONUS_LOOSE = 0.025 # …within 15s +_DURATION_TIGHT = 5 +_DURATION_LOOSE = 15 + +# ── Denoise ─────────────────────────────────────────────────────────────────── +# A parenthetical/bracketed group is dropped when it contains any of these +# noise terms as a whole word (chart-variant markers, tuning/pitch notes, +# performance qualifiers) or when it reads as an author credit ("by X", +# "charted by X"). Both sides of a comparison are denoised symmetrically, so +# over-stripping a meaningful group costs a little precision but never +# produces an asymmetric mismatch. +_NOISE_TERMS = ( + r"440\s*hz", r"a440", r"432\s*hz", + r"live", r"acoustic", r"instrumental", + r"no\s+(?:lead|rhythm|bass|vocals?|drums)", + r"(?:lead|rhythm|bass)\s+only", + r"v\d+", r"ver(?:sion)?\s*\d+", + r"remaster(?:ed)?(?:\s*\d{4})?", r"re-?recorded?", + r"fix(?:ed)?", r"updated?", + r"bonus", r"custom", +) +_NOISE_GROUP_RE = re.compile( + r"[(\[][^)\]]*\b(?:" + "|".join(_NOISE_TERMS) + r")\b[^)\]]*[)\]]", + re.IGNORECASE, +) +# Author credits: "(by SomeCharter)", "[charted by X]", "(chart by X)". +_AUTHOR_GROUP_RE = re.compile( + r"[(\[]\s*(?:chart(?:ed)?\s+)?by\s+[^)\]]*[)\]]", re.IGNORECASE) +# Trailing "- by SomeCharter" outside parens. +_AUTHOR_TAIL_RE = re.compile(r"\s+-\s+(?:chart(?:ed)?\s+)?by\s+.+$", re.IGNORECASE) + +_PUNCT_RE = re.compile(r"[^\w\s]|_") +_WS_RE = re.compile(r"\s+") + + +def _strip_diacritics(s: str) -> str: + return "".join( + ch for ch in unicodedata.normalize("NFKD", s) + if not unicodedata.combining(ch) + ) + + +def denoise(s, *, strip_leading_the: bool = False) -> str: + """Fold a community metadata string into its comparable form: + lowercase, diacritics stripped, noise parentheticals and author credits + removed, punctuation collapsed to spaces. ``strip_leading_the`` drops a + leading "The " — used for ARTIST comparison only ("The Beatles" == + "Beatles"), never titles ("The Trooper" must keep its "the").""" + s = str(s or "") + s = _NOISE_GROUP_RE.sub(" ", s) + s = _AUTHOR_GROUP_RE.sub(" ", s) + s = _AUTHOR_TAIL_RE.sub(" ", s) + s = _strip_diacritics(s).casefold() + s = s.replace("&", " and ") + s = _PUNCT_RE.sub(" ", s) + s = _WS_RE.sub(" ", s).strip() + if strip_leading_the and s.startswith("the "): + s = s[4:] + return s + + +def tokens(s, **kw) -> list[str]: + d = denoise(s, **kw) + return d.split() if d else [] + + +def _compact(toks: list[str]) -> str: + return "".join(toks) + + +def similarity(a, b, *, artist: bool = False) -> float: + """Token-set similarity in [0, 1]. Dice coefficient over the denoised + token sets, with a compacted-string equality fold so spelling drift that + only moves token boundaries ("ACDC" / "AC DC" / "AC/DC", "Greenday" / + "Green Day") counts as identical.""" + kw = {"strip_leading_the": artist} + ta, tb = tokens(a, **kw), tokens(b, **kw) + if not ta or not tb: + return 0.0 + if _compact(ta) == _compact(tb): + return 1.0 + sa, sb = set(ta), set(tb) + return 2.0 * len(sa & sb) / (len(sa) + len(sb)) + + +def _year_int(v): + try: + y = int(str(v)[:4]) + return y if y > 0 else None + except (TypeError, ValueError): + return None + + +def _duration_int(v): + try: + d = int(round(float(v))) + return d if d > 0 else None + except (TypeError, ValueError): + return None + + +def score_candidate(song: dict, cand: dict) -> float: + """Combined confidence that MusicBrainz candidate `cand` is the song the + chart transcribes. 0.5*artist + 0.5*title, plus small year/duration + corroboration bonuses, capped at 1.0. Missing fields score 0 on their + half — classify() separately refuses to auto-match without both.""" + artist_sim = similarity(song.get("artist"), cand.get("artist"), artist=True) + title_sim = similarity(song.get("title"), cand.get("title")) + score = 0.5 * artist_sim + 0.5 * title_sim + sy, cy = _year_int(song.get("year")), _year_int(cand.get("year")) + if sy and cy and abs(sy - cy) <= 1: + score += YEAR_BONUS + sd, cd = _duration_int(song.get("duration")), _duration_int(cand.get("duration")) + if sd and cd: + diff = abs(sd - cd) + if diff <= _DURATION_TIGHT: + score += DURATION_BONUS + elif diff <= _DURATION_LOOSE: + score += DURATION_BONUS_LOOSE + return min(score, 1.0) + + +def classify(song: dict, cand: dict, score: float, auto_min: float | None = None) -> str: + """Tier for a scored candidate: 'auto' | 'review' | 'none'. + + `auto` (tier-2) needs the combined score AND per-field agreement AND + both fields present — a perfect-title/wrong-artist cover, or a chart + with no artist at all, is at best a review item, never an auto match. + `auto_min` overrides the default combined-score threshold (the user's + "auto-apply confidence" setting); the per-field floors always apply. + """ + if auto_min is None: + auto_min = AUTO_MIN + artist_sim = similarity(song.get("artist"), cand.get("artist"), artist=True) + title_sim = similarity(song.get("title"), cand.get("title")) + if (score >= auto_min and artist_sim >= AUTO_ARTIST_MIN + and title_sim >= AUTO_TITLE_MIN): + return "auto" + if score >= REVIEW_MIN: + return "review" + return "none" + + +def rank_candidates(song: dict, candidates: list[dict]) -> list[dict]: + """Score every candidate against the song and return them sorted by our + score (MusicBrainz's own search score is only a tiebreak). Each returned + dict is a copy carrying `score` (rounded — it's displayed and stored).""" + ranked = [] + for cand in candidates or []: + c = dict(cand) + c["score"] = round(score_candidate(song, cand), 4) + ranked.append(c) + ranked.sort(key=lambda c: (c["score"], c.get("mb_score") or 0), reverse=True) + return ranked + + +# ── MusicBrainz query + response parsing ────────────────────────────────────── + +def _lucene_escape_phrase(s: str) -> str: + """Escape a string for use inside a quoted Lucene phrase.""" + return s.replace("\\", "\\\\").replace('"', '\\"') + + +def build_recording_query(artist, title) -> str: + """Lucene query for /ws/2/recording. Built from the DENOISED fields — + the noise we strip (author credits, "(Live)", "(v2)") would otherwise + poison the search server's own scoring.""" + t = denoise(title) + a = denoise(artist) + parts = [] + if t: + parts.append('recording:"%s"' % _lucene_escape_phrase(t)) + if a: + parts.append('artist:"%s"' % _lucene_escape_phrase(a)) + return " AND ".join(parts) + + +def _artist_credit(doc: dict) -> tuple[str, str, str]: + """(display name, artist mbid, sort name) from an artist-credit array.""" + credits = doc.get("artist-credit") or [] + name = "" + for part in credits: + if isinstance(part, dict): + name += str(part.get("name", "")) + str(part.get("joinphrase", "") or "") + else: # ws/2 can emit bare join strings in older serializations + name += str(part) + first = next((p for p in credits if isinstance(p, dict)), None) or {} + artist = first.get("artist") or {} + return name, str(artist.get("id", "") or ""), str(artist.get("sort-name", "") or "") + + +def _best_release(doc: dict) -> dict: + """Pick the release used for canon album/year: prefer Official status and + an Album release-group, then the earliest date. Returns {} if none.""" + releases = [r for r in (doc.get("releases") or []) if isinstance(r, dict)] + if not releases: + return {} + + def sort_key(r): + status_ok = 0 if str(r.get("status", "")).lower() == "official" else 1 + rg = r.get("release-group") or {} + album_ok = 0 if str(rg.get("primary-type", "")).lower() == "album" else 1 + date = str(r.get("date", "") or "9999") + return (status_ok, album_ok, date) + + return sorted(releases, key=sort_key)[0] + + +def _genres(doc: dict, limit: int = 5) -> list[str]: + """Genre names from a recording doc. Search results carry folksonomy + `tags`; lookups with inc=genres carry curated `genres`. Both are + [{name, count}] — take the most-voted few.""" + raw = doc.get("genres") or doc.get("tags") or [] + entries = [e for e in raw if isinstance(e, dict) and e.get("name")] + entries.sort(key=lambda e: e.get("count") or 0, reverse=True) + return [str(e["name"]) for e in entries[:limit]] + + +def parse_recording_doc(doc: dict) -> dict | None: + """Normalize one /ws/2 recording document (search hit or direct lookup) + into the flat candidate dict stored in song_enrichment.candidates and + rendered by the review drawer. Returns None for malformed docs.""" + if not isinstance(doc, dict) or not doc.get("id") or not doc.get("title"): + return None + artist_name, artist_id, artist_sort = _artist_credit(doc) + release = _best_release(doc) + length = doc.get("length") + try: + duration = int(round(float(length) / 1000.0)) if length else None + except (TypeError, ValueError): + duration = None + isrcs = doc.get("isrcs") or [] + isrcs = [str(i) for i in isrcs if isinstance(i, (str,))] + return { + "recording_id": str(doc["id"]), + "title": str(doc.get("title", "")), + "artist": artist_name, + "artist_id": artist_id, + "artist_sort": artist_sort, + "release_id": str(release.get("id", "") or ""), + "album": str(release.get("title", "") or ""), + "year": str(release.get("date", "") or "")[:4], + "duration": duration, + "isrc": isrcs[0] if isrcs else "", + "genres": _genres(doc), + "mb_score": int(doc.get("score") or 0), + } + + +def parse_search_response(body: dict) -> list[dict]: + """Candidates from a /ws/2/recording search response.""" + docs = (body or {}).get("recordings") or [] + out = [] + for doc in docs: + cand = parse_recording_doc(doc) + if cand: + out.append(cand) + return out diff --git a/server.py b/server.py index 47f169c..49db16d 100644 --- a/server.py +++ b/server.py @@ -47,6 +47,10 @@ import sloppak as sloppak_mod import drums as drums_mod import notation as notation_mod import loosefolder as loosefolder_mod +# Pure text-matching engine for MusicBrainz enrichment (P8): denoise/score/ +# tier classification + response parsing. No network/DB in there — the +# throttled transport and the song_enrichment writes live in this module. +import mb_match # Metadata extraction lives in a side-effect-free module so ProcessPool # scan workers can import + unpickle _scan_one without re-running this # module's import-time side effects (see lib/scan_worker.py). @@ -230,6 +234,12 @@ _DEMO_BLOCKED: list[tuple[str, re.Pattern]] = [ ("POST", re.compile(r"^/api/progression/events$")), ("POST", re.compile(r"^/api/shop/buy$")), ("POST", re.compile(r"^/api/shop/equip$")), + # Enrichment (P8): review writes mutate the local match cache, and the + # search proxy / manual kick relay to MusicBrainz — none of it belongs to + # anonymous demo visitors (they'd spend the shared rate limit). + ("POST", re.compile(r"^/api/enrichment/review/.+$")), + ("POST", re.compile(r"^/api/enrichment/kick$")), + ("GET", re.compile(r"^/api/enrichment/search$")), ] @@ -850,6 +860,19 @@ class MetadataDB: """) self.conn.execute("CREATE INDEX IF NOT EXISTS idx_enrichment_hash ON song_enrichment(content_hash)") self.conn.execute("CREATE INDEX IF NOT EXISTS idx_enrichment_state ON song_enrichment(match_state)") + # P8 (the matcher): `candidates` holds the review tier's ranked + # candidate list (JSON) so the Match-Review drawer never re-queries + # MusicBrainz just to render; `last_attempt_at` anchors the failed-row + # retry backoff (epoch seconds). Idempotent ALTERs, same pattern as + # the `songs` migrations above. + for ddl in ( + "ALTER TABLE song_enrichment ADD COLUMN candidates TEXT", + "ALTER TABLE song_enrichment ADD COLUMN last_attempt_at REAL", + ): + try: + self.conn.execute(ddl) + except sqlite3.OperationalError: + pass # Progression (spec 010): instrument paths, challenges, quests, the # Decibels wallet, and the cosmetics shop. Targets/titles live in the # bundled content (data/progression/); these tables hold only player @@ -2586,30 +2609,35 @@ class MetadataDB: return hashlib.sha1(raw.encode("utf-8")).hexdigest() def enrichment_pending(self, limit: int = 500) -> list[dict]: - """Songs whose enrichment row needs (re)matching: no row yet, or an - `unscanned`/`matched` row whose content_hash no longer matches the - song's current metadata (an edit changed the identity → re-match). - `manual` rows are the user's pinned pick and are NEVER re-queued; - `failed` rows wait for the matcher's backoff policy (next slice) rather - than being re-queued here every pass.""" + """Songs whose enrichment row needs (re)matching: no row yet, or a + row whose content_hash no longer matches the song's current metadata + (an edit changed the identity → re-match), or an `unscanned` row. + `manual` rows are the user's pinned pick and are NEVER re-queued. + `matched`/`review`/`failed` rows with an UNCHANGED hash are settled + here — a review row stands until the user acts, and a failed row + retries only via the matcher's backoff policy (enrichment_failed_rows) + rather than being re-queued every pass. An identity edit (say, the + user fixes the typo that made matching fail) re-queues any of them + immediately via the hash mismatch.""" # Read under _lock: the worker commits on this shared connection under # _lock, so an unlocked SELECT could interleave with its execute+commit. with self._lock: rows = self.conn.execute( - "SELECT s.filename, s.artist, s.title, s.album, s.duration, " + "SELECT s.filename, s.artist, s.title, s.album, s.year, s.duration, " "e.content_hash, e.match_state " "FROM songs s LEFT JOIN song_enrichment e ON e.filename = s.filename " - "WHERE s.title != '' AND (e.filename IS NULL OR e.match_state IN ('unscanned', 'matched')) " + "WHERE s.title != '' AND (e.filename IS NULL " + "OR e.match_state IN ('unscanned', 'matched', 'review', 'failed')) " "ORDER BY s.filename LIMIT ?", (max(1, int(limit)),)).fetchall() out = [] - for fn, artist, title, album, duration, ehash, state in rows: + for fn, artist, title, album, year, duration, ehash, state in rows: h = self.enrichment_content_hash(artist, title, album, duration) # No row yet, still unmatched, or the identity changed under a - # match → needs the matcher. A matched row with an unchanged hash - # is settled (idempotence). + # settled row → needs the matcher. A settled row with an + # unchanged hash stays settled (idempotence). if state is None or state == "unscanned" or ehash != h: out.append({"filename": fn, "artist": artist, "title": title, - "album": album, "duration": duration, + "album": album, "year": year, "duration": duration, "content_hash": h, "match_state": state}) return out @@ -2641,6 +2669,14 @@ class MetadataDB: " WHEN song_enrichment.content_hash IS NOT excluded.content_hash " " THEN 'unscanned' " " ELSE song_enrichment.match_state END, " + # An identity change restarts the failure backoff too — the + # accumulated attempts belonged to the OLD identity (e.g. the + # user just fixed the typo that made matching fail). + " attempts = CASE WHEN song_enrichment.match_state = 'manual' " + " THEN song_enrichment.attempts " + " WHEN song_enrichment.content_hash IS NOT excluded.content_hash " + " THEN 0 " + " ELSE song_enrichment.attempts END, " " content_hash = CASE WHEN song_enrichment.match_state = 'manual' " " THEN song_enrichment.content_hash " " ELSE excluded.content_hash END", @@ -2654,19 +2690,21 @@ class MetadataDB: "SELECT filename, content_hash, match_state, match_source, match_score, attempts, " "mb_recording_id, mb_release_id, mb_artist_id, isrc, " "canon_artist, canon_album, canon_title, canon_year, canon_artist_sort, " - "genres, art_cache_path, art_state, fetched_at " + "genres, art_cache_path, art_state, fetched_at, candidates, last_attempt_at " "FROM song_enrichment WHERE filename = ?", (filename,)).fetchone() if not row: return None keys = ("filename", "content_hash", "match_state", "match_source", "match_score", "attempts", "mb_recording_id", "mb_release_id", "mb_artist_id", "isrc", "canon_artist", "canon_album", "canon_title", "canon_year", - "canon_artist_sort", "genres", "art_cache_path", "art_state", "fetched_at") + "canon_artist_sort", "genres", "art_cache_path", "art_state", "fetched_at", + "candidates", "last_attempt_at") out = dict(zip(keys, row)) - try: - out["genres"] = json.loads(out["genres"]) if out["genres"] else [] - except (ValueError, TypeError): - out["genres"] = [] + for k in ("genres", "candidates"): + try: + out[k] = json.loads(out[k]) if out[k] else [] + except (ValueError, TypeError): + out[k] = [] return out def enrichment_state_counts(self) -> dict: @@ -2679,6 +2717,160 @@ class MetadataDB: "JOIN songs s ON s.filename = e.filename GROUP BY e.match_state").fetchall() return {r[0]: r[1] for r in rows} + def enrichment_song_row(self, filename: str) -> dict | None: + """The identity fields the matcher/scorer keys on, for one song.""" + row = self.conn.execute( + "SELECT filename, artist, title, album, year, duration " + "FROM songs WHERE filename = ?", (filename,)).fetchone() + if not row: + return None + return dict(zip(("filename", "artist", "title", "album", "year", "duration"), row)) + + def enrichment_failed_rows(self, limit: int = 500) -> list[dict]: + """`failed` rows that MAY retry, with the fields the backoff policy + (worker-side) needs to decide eligibility. `rejected` rows are the + user's explicit "none of these" — never auto-retried (an identity + edit re-queues them through enrichment_pending's hash mismatch + instead).""" + rows = self.conn.execute( + "SELECT s.filename, s.artist, s.title, s.album, s.year, s.duration, " + "e.attempts, e.last_attempt_at " + "FROM songs s JOIN song_enrichment e ON e.filename = s.filename " + "WHERE s.title != '' AND e.match_state = 'failed' " + "AND COALESCE(e.match_source, '') != 'rejected' " + "ORDER BY s.filename LIMIT ?", (max(1, int(limit)),)).fetchall() + out = [] + for fn, artist, title, album, year, duration, attempts, last_at in rows: + out.append({"filename": fn, "artist": artist, "title": title, + "album": album, "year": year, "duration": duration, + "content_hash": self.enrichment_content_hash(artist, title, album, duration), + "attempts": attempts or 0, "last_attempt_at": last_at}) + return out + + def enrichment_cache_lookup(self, content_hash: str, exclude_filename: str = "") -> dict | None: + """A settled match for the same identity hash — another chart of the + same recording already matched/pinned → copy it, no network (design + §5 step 1: the local match-cache).""" + row = self.conn.execute( + "SELECT match_score, mb_recording_id, mb_release_id, mb_artist_id, isrc, " + "canon_artist, canon_album, canon_title, canon_year, canon_artist_sort, genres " + "FROM song_enrichment WHERE content_hash = ? AND filename != ? " + "AND match_state IN ('matched', 'manual') AND mb_recording_id IS NOT NULL " + "LIMIT 1", (content_hash, exclude_filename or "")).fetchone() + if not row: + return None + try: + genres = json.loads(row[10]) if row[10] else [] + except (ValueError, TypeError): + genres = [] + return { + "score": row[0], + "recording_id": row[1], "release_id": row[2] or "", "artist_id": row[3] or "", + "isrc": row[4] or "", "artist": row[5] or "", "album": row[6] or "", + "title": row[7] or "", "year": row[8] or "", "artist_sort": row[9] or "", + "genres": genres, + } + + def apply_enrichment_match(self, filename: str, content_hash: str, state: str, + source: str | None = None, score: float | None = None, + cand: dict | None = None, candidates: list | None = None, + bump_attempts: bool = False, + allow_manual_overwrite: bool = False) -> bool: + """The single writer for every matcher/review outcome. Writes the + full lifecycle row: state + source + score, the canonical fields a + confident match supplies (`cand`), and/or the review tier's ranked + `candidates`. Returns False without touching anything when the row is + `manual` and the caller isn't explicitly acting for the user — the + never-overwrite-manual contract lives HERE so no future call path + can forget it. Art-cache fields are preserved verbatim (they belong + to the art slice, not the matcher).""" + cand = cand or {} + now = time.time() + with self._lock: + cur = self.conn.execute( + "SELECT match_state, attempts, art_cache_path, art_state, fetched_at " + "FROM song_enrichment WHERE filename = ?", (filename,)).fetchone() + if cur and cur[0] == "manual" and not allow_manual_overwrite: + return False + attempts = int(cur[1] or 0) if cur else 0 + if bump_attempts: + attempts += 1 + fetched_at = (time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + if state in ("matched", "manual", "review") + else (cur[4] if cur else None)) + self.conn.execute( + "INSERT OR REPLACE INTO song_enrichment (filename, content_hash, " + "match_state, match_source, match_score, attempts, " + "mb_recording_id, mb_release_id, mb_artist_id, isrc, " + "canon_artist, canon_album, canon_title, canon_year, canon_artist_sort, " + "genres, art_cache_path, art_state, fetched_at, candidates, last_attempt_at) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (filename, content_hash, state, source, score, attempts, + cand.get("recording_id") or None, cand.get("release_id") or None, + cand.get("artist_id") or None, cand.get("isrc") or None, + cand.get("artist") or None, cand.get("album") or None, + cand.get("title") or None, cand.get("year") or None, + cand.get("artist_sort") or None, + json.dumps(cand.get("genres") or []) if cand else "[]", + cur[2] if cur else None, cur[3] if cur else None, + fetched_at, + json.dumps(candidates) if candidates else None, + now if state == "failed" else None)) + self.conn.commit() + return True + + def set_enrichment_manual(self, filename: str, cand: dict, source: str = "search") -> bool: + """User-pinned match (review Accept / manual search-and-pick). The + highest-authority state: never auto-reset, survives identity edits. + `source` records HOW it was pinned ('review' = accepted a proposed + candidate, 'search' = picked from a manual search).""" + song = self.enrichment_song_row(filename) + if not song: + return False + h = self.enrichment_content_hash( + song["artist"], song["title"], song["album"], song["duration"]) + return self.apply_enrichment_match( + filename, h, "manual", source=source, score=1.0, cand=cand, + allow_manual_overwrite=True) + + def set_enrichment_rejected(self, filename: str) -> bool: + """User said "none of these candidates" — clear any canonical values + and park the row as failed/rejected (never auto-retried; an identity + edit re-queues it). Refused for `manual` rows: un-pinning a pick the + user explicitly made is not a review-drawer action.""" + row = self.get_enrichment(filename) + if not row or row["match_state"] not in ("review", "matched"): + return False + return self.apply_enrichment_match( + filename, row["content_hash"], "failed", source="rejected", + score=None, candidates=row.get("candidates") or None) + + def enrichment_review_queue(self, limit: int = 200) -> list[dict]: + """The Match-Review drawer's queue: review-tier rows joined to their + (still-existing) songs, with the stored candidate list parsed.""" + rows = self.conn.execute( + "SELECT e.filename, s.title, s.artist, s.album, s.year, s.duration, s.mtime, " + "e.match_score, e.candidates, e.attempts " + "FROM song_enrichment e JOIN songs s ON s.filename = e.filename " + "WHERE e.match_state = 'review' " + # Charts that are MISSING data (no album / no year) surface first — + # confirming those has the most to gain; complete charts only + # stand to be re-labelled. + "ORDER BY ((COALESCE(s.album, '') = '') + (COALESCE(s.year, '') = '')) DESC, " + "s.artist COLLATE NOCASE, s.title COLLATE NOCASE, e.filename " + "LIMIT ?", (max(1, int(limit)),)).fetchall() + out = [] + for fn, title, artist, album, year, duration, mtime, score, cands, attempts in rows: + try: + candidates = json.loads(cands) if cands else [] + except (ValueError, TypeError): + candidates = [] + out.append({"filename": fn, "title": title, "artist": artist, + "album": album, "year": year, "duration": duration, + "mtime": mtime, "match_score": score, + "candidates": candidates, "attempts": attempts or 0}) + return out + def _estd_set(self) -> set[str]: """Get set of filenames that have a retuned variant (_EStd_ or _DropD_) in the DB.""" rows = self.conn.execute( @@ -2733,6 +2925,7 @@ class MetadataDB: mastery: list[str] | None = None, tags_has: list[str] | None = None, user_difficulty_in: list[str] | None = None, + match_states: list[str] | None = None, naming_mode: str = "legacy", include_intrinsic: bool = True) -> tuple[str, list]: """Shared WHERE-clause builder for query_page / query_artists / @@ -2798,6 +2991,21 @@ class MetadataDB: where += (" AND filename IN (SELECT filename FROM song_user_meta " f"WHERE user_difficulty IN ({ph}))") params += _diffs + # Match facet (P8) = the song's enrichment lifecycle state, from the + # separate song_enrichment table (same EXISTS idiom as mastery above). + # 'matched' folds in 'manual' (a user pin IS a match); 'pending' means + # no verdict yet (no row, or still unscanned). OR within the set. + if match_states: + _esub = "SELECT 1 FROM song_enrichment e WHERE e.filename = songs.filename" + _mstates = { + "review": f"EXISTS ({_esub} AND e.match_state = 'review')", + "matched": f"EXISTS ({_esub} AND e.match_state IN ('matched', 'manual'))", + "unmatched": f"EXISTS ({_esub} AND e.match_state = 'failed')", + "pending": f"NOT EXISTS ({_esub} AND e.match_state != 'unscanned')", + } + _msel = [_mstates[b] for b in match_states if b in _mstates] + if _msel: + where += " AND (" + " OR ".join(_msel) + ")" if q: where += " AND (title LIKE ? COLLATE NOCASE OR artist LIKE ? COLLATE NOCASE OR album LIKE ? COLLATE NOCASE)" params += [f"%{q}%"] * 3 @@ -3228,6 +3436,7 @@ class MetadataDB: mastery: list[str] | None = None, tags_has: list[str] | None = None, user_difficulty_in: list[str] | None = None, + match_states: list[str] | None = None, after: str | None = None, group: bool = False, naming_mode: str = "legacy") -> tuple[list[dict], int]: @@ -3258,6 +3467,7 @@ class MetadataDB: stems_has=stems_has, stems_lacks=stems_lacks, has_lyrics=has_lyrics, tunings=tunings, mastery=mastery, tags_has=tags_has, user_difficulty_in=user_difficulty_in, + match_states=match_states, naming_mode=naming_mode, include_intrinsic=not group, ) ifrag, iparams = "", [] @@ -3595,6 +3805,7 @@ class MetadataDB: stems_lacks: list[str] | None = None, has_lyrics: int | None = None, tunings: list[str] | None = None, + match_states: list[str] | None = None, sort: str = "artist", want_sort_letters: bool = False, group: bool = False, @@ -3624,7 +3835,8 @@ class MetadataDB: artist_filter=artist_filter, album_filter=album_filter, arrangements_has=arrangements_has, arrangements_lacks=arrangements_lacks, stems_has=stems_has, stems_lacks=stems_lacks, - has_lyrics=has_lyrics, tunings=tunings, naming_mode=naming_mode, + has_lyrics=has_lyrics, tunings=tunings, match_states=match_states, + naming_mode=naming_mode, include_intrinsic=not group, ) if group: @@ -4390,7 +4602,8 @@ def _require_library_provider_capability(provider: object, capability: str) -> N ) -_OPTIONAL_NEW_PROVIDER_KWARGS = ("naming_mode", "sort", "want_sort_letters", "after") +_OPTIONAL_NEW_PROVIDER_KWARGS = ("naming_mode", "sort", "want_sort_letters", "after", + "mastery", "match_states") def _filter_provider_kwargs(method: object, kwargs: dict) -> dict: @@ -5229,13 +5442,14 @@ def _scan_runner(): _kick_enrich() -# ── Metadata enrichment worker (P7 — plumbing) ──────────────────────────────── +# ── Metadata enrichment worker (P7 plumbing + P8 matcher) ───────────────────── # A single throttled daemon thread + queue, mirroring _kick_scan/_scan_runner # (single-flight + coalescing; NOT a pool — external lookups are rate-limited -# to ~1/s, which makes a pool pointless). This slice ships the full lifecycle -# around a NO-OP matcher: the queue walk, the identity hashing, the throttle -# seam, and the status surface — so the real text matcher (next slice) replaces -# exactly one function (_enrich_one) and inherits everything else. +# to ~1/s, which makes a pool pointless). P7 shipped the lifecycle; P8 fills +# in the matcher (_enrich_one): local cache → manifest mbid/isrc exact keys → +# MusicBrainz text search, scored into auto/review/failed tiers by +# lib/mb_match.py. Wrong-match is worse than slow (design §5): medium +# confidence goes to the Match-Review queue, never straight to canonical. _enrich_kick_lock = threading.Lock() _enrich_pending_pass = False @@ -5243,6 +5457,9 @@ _enrich_status = {"running": False, "processed": 0, "last_pass_at": None} # Minimum spacing between EXTERNAL lookups (design: ≤1 req/s + local cache). _ENRICH_MIN_INTERVAL = 1.1 _enrich_last_fetch = 0.0 +# Serializes throttling across the background daemon thread AND the sync +# /api/enrichment/search route (FastAPI runs sync routes in a threadpool). +_enrich_throttle_lock = threading.Lock() def _enrichment_art_dir() -> Path: @@ -5259,25 +5476,233 @@ def _enrich_throttle(): before every network request — and must NOT hold meta_db._lock across the request (fetch outside the lock, write inside).""" global _enrich_last_fetch - wait = _ENRICH_MIN_INTERVAL - (time.monotonic() - _enrich_last_fetch) - if wait > 0: - time.sleep(wait) - _enrich_last_fetch = time.monotonic() + # Hold the lock across the read, sleep, and write so concurrent callers + # serialize instead of all reading the same stale timestamp and firing + # together (which would burst past MusicBrainz's 1 req/s limit). + with _enrich_throttle_lock: + wait = _ENRICH_MIN_INTERVAL - (time.monotonic() - _enrich_last_fetch) + if wait > 0: + time.sleep(wait) + _enrich_last_fetch = time.monotonic() -def _enrich_one(row: dict) -> None: - """P7's NO-OP matcher: stamp/refresh the row's identity hash (which also - drops a stale match back to `unscanned`, never a `manual` pick) and stop. - The text-match pipeline replaces this function; anything that reaches the - network must go through _enrich_throttle() and must not hold meta_db._lock - across the fetch.""" - meta_db.upsert_enrichment_stub(row["filename"], row["content_hash"]) +class EnrichTransportError(Exception): + """Network-level enrichment failure — offline, DNS, MusicBrainz down or + rate-limiting. Pauses the current pass (rows keep their state and no + attempt is consumed); the next kick (scan-complete / the 5-min periodic + rescan) retries naturally.""" + + +_MB_API_ROOT = "https://musicbrainz.org/ws/2" +_enrich_ua_cache: str | None = None + + +def _enrich_user_agent() -> str: + """MusicBrainz etiquette requires a real identifying User-Agent + (app/version + contact URL); anonymous defaults get throttled/blocked.""" + global _enrich_ua_cache + if _enrich_ua_cache is None: + version = "unknown" + try: + vf = Path(__file__).parent / "VERSION" + if vf.exists(): + version = vf.read_text().strip() or "unknown" + except (OSError, UnicodeDecodeError): + pass + _enrich_ua_cache = f"feedBack/{version} (https://github.com/got-feedback/feedBack)" + return _enrich_ua_cache + + +def _enrich_network_enabled() -> bool: + """False = the matcher runs local-only (hash stamping, cache copies) and + never opens a socket. FEEDBACK_ENRICH_OFFLINE is the explicit user + kill-switch (privacy / air-gapped installs); FEEDBACK_SKIP_STARTUP_TASKS + marks the test/CI environment, where pytest must never reach the network + no matter what a test triggers.""" + return not (_env_flag("FEEDBACK_ENRICH_OFFLINE") + or _env_flag("FEEDBACK_SKIP_STARTUP_TASKS")) + + +def _mb_http_get(path: str, params: dict) -> dict | None: + """The ONE place enrichment touches the network (tests fake exactly this + seam). Throttled (≤1 req/s via _enrich_throttle), identified (real + User-Agent), offline-guarded. Returns the parsed JSON body, or None for + a 404 lookup; raises EnrichTransportError for anything network-shaped. + NEVER call this while holding meta_db._lock — fetch outside, write + inside.""" + if not _enrich_network_enabled(): + raise EnrichTransportError("enrichment network disabled") + import requests # declared in requirements.txt; lazy so tests never need it + _enrich_throttle() + try: + resp = requests.get( + f"{_MB_API_ROOT}/{path.lstrip('/')}", + params={**params, "fmt": "json"}, + headers={"User-Agent": _enrich_user_agent()}, + timeout=10, + ) + except requests.RequestException as e: + raise EnrichTransportError(str(e)) from e + if resp.status_code == 404: + return None + if resp.status_code == 503: + # MusicBrainz signals rate-limit pressure with 503 — back the whole + # pass off rather than hammering on. + raise EnrichTransportError("musicbrainz 503 (rate limited)") + if resp.status_code != 200: + raise EnrichTransportError(f"musicbrainz HTTP {resp.status_code}") + try: + return resp.json() + except ValueError as e: + raise EnrichTransportError("bad JSON from musicbrainz") from e + + +def _mb_search_recordings(artist, title, limit: int = 8) -> list[dict]: + """Text search (tier 2–4): denoised Lucene query over /recording.""" + query = mb_match.build_recording_query(artist, title) + if not query: + return [] + body = _mb_http_get("recording", {"query": query, "limit": limit}) + return mb_match.parse_search_response(body or {}) + + +def _mb_lookup_recording(mbid: str) -> dict | None: + """Direct lookup for a manifest-carried recording MBID (tier 0).""" + body = _mb_http_get( + f"recording/{mbid}", + {"inc": "artist-credits+releases+release-groups+isrcs+genres"}) + return mb_match.parse_recording_doc(body) if body else None + + +def _mb_lookup_isrc(isrc: str) -> list[dict]: + """Recordings registered under a manifest-carried ISRC (tier 1).""" + body = _mb_http_get( + f"isrc/{isrc}", {"inc": "artist-credits+releases+release-groups"}) + if not body: + return [] + docs = body.get("recordings") or [] + return [c for c in (mb_match.parse_recording_doc(d) for d in docs) if c] + + +# Strict shapes for the manifest's optional identity keys (feedpak spec §5.1). +# Validated before use — the mbid is interpolated into a URL path, so junk or +# hostile manifest values must never reach the request line. +_MBID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$") +_ISRC_RE = re.compile(r"^[A-Z]{2}[A-Z0-9]{3}[0-9]{7}$") + + +def _manifest_exact_ids(filename: str) -> dict: + """Optional `mbid`/`isrc` from the pack manifest — the spec's additive + identity keys. Feature-detected: packs published before that spec + revision simply lack them and fall through to text matching. READ-only: + enrichment never writes anything into pack files.""" + try: + dlc = _get_dlc_dir() + if not dlc: + return {} + p = _resolve_dlc_path(dlc, filename) + if p is None or not p.exists() or not sloppak_mod.is_sloppak(p): + return {} + manifest = sloppak_mod.load_manifest(p) or {} + except Exception: + return {} + out = {} + mbid = str(manifest.get("mbid", "") or "").strip().lower() + if _MBID_RE.match(mbid): + out["mbid"] = mbid + isrc = str(manifest.get("isrc", "") or "").strip().upper() + if _ISRC_RE.match(isrc): + out["isrc"] = isrc + return out + + +# Failed-row retry backoff: 1 h after the first failed attempt, doubling per +# attempt, capped at a week — a permanently-unmatchable obscure chart must +# not re-hammer MusicBrainz on every scan kick. +_ENRICH_BACKOFF_BASE = 3600.0 +_ENRICH_BACKOFF_CAP = 7 * 86400.0 + + +def _enrich_backoff_elapsed(attempts, last_attempt_at, now: float) -> bool: + if not last_attempt_at: + return True + delay = min(_ENRICH_BACKOFF_BASE * (2 ** max(0, int(attempts or 1) - 1)), + _ENRICH_BACKOFF_CAP) + return (now - float(last_attempt_at)) >= delay + + +# Review tier keeps a short ranked candidate list for the drawer; more than a +# handful is noise the user has to scroll past. +_ENRICH_MAX_CANDIDATES = 5 + + +def _enrich_one(row: dict, auto_min: float | None = None) -> None: + """The matcher (P8; replaces P7's no-op). Precedence per design §5: + + 1. local match-cache by content_hash — another chart of the same + recording already matched/pinned → copy it, NO network; + 2. manifest `mbid` (tier 0) / `isrc` (tier 1) exact keys → direct + lookup, auto; + 3. text search → scored tiers: auto (high) / review (medium — a human + confirms before anything canonicalizes) / failed (low, retried on + backoff). + + `auto_min` is the user's auto-apply confidence setting (None → the + engine default); it moves only the auto/review boundary of step 3 — + the per-field floors and exact-key tiers are unaffected. Never touches + a `manual` row (the writer enforces it). Network errors raise + EnrichTransportError so the pass pauses instead of burning attempts + while offline.""" + fn, chash = row["filename"], row["content_hash"] + + cached = meta_db.enrichment_cache_lookup(chash, exclude_filename=fn) + if cached: + score = cached.pop("score", None) + meta_db.apply_enrichment_match(fn, chash, "matched", source="cache", + score=score, cand=cached) + return + + ids = _manifest_exact_ids(fn) + if ids.get("mbid"): + cand = _mb_lookup_recording(ids["mbid"]) + if cand: + meta_db.apply_enrichment_match(fn, chash, "matched", source="mbid", + score=1.0, cand=cand) + return + # A 404'd mbid (typo'd manifest) falls through to the text tiers. + if ids.get("isrc"): + cands = mb_match.rank_candidates(row, _mb_lookup_isrc(ids["isrc"])) + if cands: + meta_db.apply_enrichment_match(fn, chash, "matched", source="isrc", + score=1.0, cand=cands[0]) + return + + ranked = mb_match.rank_candidates(row, _mb_search_recordings(row.get("artist"), row.get("title"))) + best = ranked[0] if ranked else None + tier = mb_match.classify(row, best, best["score"], auto_min=auto_min) if best else "none" + if tier == "auto": + meta_db.apply_enrichment_match(fn, chash, "matched", source="text", + score=best["score"], cand=best) + elif tier == "review": + meta_db.apply_enrichment_match(fn, chash, "review", source="text", + score=best["score"], + candidates=ranked[:_ENRICH_MAX_CANDIDATES]) + else: + meta_db.apply_enrichment_match(fn, chash, "failed", source="text", + score=(best["score"] if best else None), + candidates=ranked[:_ENRICH_MAX_CANDIDATES] or None, + bump_attempts=True) def _background_enrich(): - """One pass over the rows needing (re)matching. A single bounded pass — - with the no-op matcher, `unscanned` rows legitimately stay unscanned, so - looping until the queue drains would spin forever.""" + """One bounded pass, two phases. Phase 1 stamps/refreshes identity-hash + stubs for every song whose identity is new or changed — pure-local, so + hashes stay fresh (and stale matches drop back to `unscanned`) even + fully offline. Phase 2 runs the matcher over those rows plus any + `failed` rows whose backoff has elapsed; a transport failure pauses it + (state untouched, no attempt burned) and the next kick retries. Offline + (kill-switch or the test env) skips phase 2 entirely. Never drains in a + loop — a dead network would make that spin forever.""" _enrich_status["processed"] = 0 try: pending = meta_db.enrichment_pending(limit=100000) @@ -5286,13 +5711,66 @@ def _background_enrich(): return for row in pending: try: - _enrich_one(row) + meta_db.upsert_enrichment_stub(row["filename"], row["content_hash"]) except Exception as e: - log.warning("enrichment failed for %s: %s", row.get("filename"), e) + log.warning("enrichment stub failed for %s: %s", row.get("filename"), e) _enrich_status["processed"] += 1 _enrich_status["last_pass_at"] = time.time() - if pending: - log.info("Enrichment pass: %d rows refreshed", len(pending)) + + # User settings gate the BACKGROUND matcher only (the review modal's + # manual search/fix stays available when it's off); read once per pass. + cfg = _load_config(CONFIG_DIR / "config.json") or {} + if cfg.get("enrich_enabled", True) is False: + if pending: + log.info("Enrichment pass: %d rows stamped (matching disabled in Settings)", len(pending)) + return + try: + auto_min = float(cfg.get("enrich_auto_threshold", 0.9)) + except (TypeError, ValueError): + auto_min = 0.9 + + if not _enrich_network_enabled(): + if pending: + log.info("Enrichment pass: %d rows stamped (network disabled — matching skipped)", len(pending)) + return + + now = time.time() + retriable = [] + try: + retriable = [r for r in meta_db.enrichment_failed_rows(limit=100000) + if _enrich_backoff_elapsed(r.get("attempts"), r.get("last_attempt_at"), now)] + except Exception: + log.exception("enrichment: failed-row query failed") + matched = 0 + # A `failed` row with a changed identity hash can surface in BOTH lists; + # de-dup by filename so each row consumes the rate budget only once. + seen_filenames = set() + queue = [] + for row in pending + retriable: + fn = row.get("filename") + if fn in seen_filenames: + continue + seen_filenames.add(fn) + queue.append(row) + for row in queue: + try: + _enrich_one(row, auto_min=auto_min) + matched += 1 + except EnrichTransportError as e: + log.info("enrichment: network unavailable, pass paused (%s)", e) + break + except Exception as e: + log.warning("enrichment failed for %s: %s", row.get("filename"), e) + try: + # Park the row on the failure backoff instead of retrying a + # poisoned input every pass. + meta_db.apply_enrichment_match( + row["filename"], row["content_hash"], "failed", + source="error", bump_attempts=True) + except Exception: + pass + if pending or retriable: + log.info("Enrichment pass: %d rows stamped, %d matched", len(pending), matched) def _kick_enrich() -> bool: @@ -5807,6 +6285,112 @@ def enrichment_status(): } +@app.post("/api/enrichment/kick") +def api_enrichment_kick(): + """The Settings "Match now" button: request an enrichment pass without + waiting for a scan to complete. Single-flight + coalescing like every + other kick — spamming it queues at most one follow-up pass.""" + return {"started": _kick_enrich()} + + +@app.get("/api/enrichment/review") +def api_enrichment_review(limit: int = 200): + """The Match-Review queue: songs whose text match landed in the medium- + confidence review tier, each with its stored candidate list — the drawer + renders straight from this, no MusicBrainz round-trip.""" + limit = max(1, min(int(limit), 500)) + return { + "songs": meta_db.enrichment_review_queue(limit=limit), + "total_review": meta_db.enrichment_state_counts().get("review", 0), + } + + +@app.post("/api/enrichment/review/{filename:path}/accept") +def api_enrichment_accept(filename: str, data: dict = Body(...)): + """Accept one of the stored review candidates: the row becomes a + user-pinned `manual` match (never auto-reset). Display-only, like every + enrichment write — nothing touches the pack file.""" + recording_id = str((data or {}).get("recording_id") or "") + row = meta_db.get_enrichment(filename) + if not row or row["match_state"] != "review": + raise HTTPException(status_code=404, detail="no review row for this song") + cand = next((c for c in (row.get("candidates") or []) + if c.get("recording_id") == recording_id), None) + if not cand: + raise HTTPException(status_code=404, detail="candidate not in the stored list") + if not meta_db.set_enrichment_manual(filename, cand, source="review"): + raise HTTPException(status_code=404, detail="unknown song") + return {"ok": True, "enrichment": meta_db.get_enrichment(filename)} + + +@app.post("/api/enrichment/review/{filename:path}/reject") +def api_enrichment_reject(filename: str): + """"None of these" — clears any canonical values and parks the row as + failed/rejected (never auto-retried; editing the song's metadata + re-queues it). Valid from `review` or `matched`, never from `manual`.""" + if not meta_db.set_enrichment_rejected(filename): + raise HTTPException(status_code=404, detail="no rejectable match for this song") + return {"ok": True, "enrichment": meta_db.get_enrichment(filename)} + + +# The candidate fields a manual pick is allowed to carry — the payload comes +# from our own /api/enrichment/search proxy, but the route re-sanitizes so a +# hand-rolled client can't stuff arbitrary keys/types into the cache row. +_CAND_STR_FIELDS = ("recording_id", "title", "artist", "artist_id", + "artist_sort", "release_id", "album", "year", "isrc") + + +def _sanitize_candidate(raw: dict) -> dict | None: + if not isinstance(raw, dict): + return None + out = {k: str(raw.get(k) or "") for k in _CAND_STR_FIELDS} + if not out["recording_id"] or not out["title"]: + return None + genres = raw.get("genres") or [] + out["genres"] = [str(g) for g in genres if isinstance(g, str)][:5] \ + if isinstance(genres, list) else [] + return out + + +@app.post("/api/enrichment/review/{filename:path}/pick") +def api_enrichment_pick(filename: str, data: dict = Body(...)): + """Fix-match / manual search-and-pick: pin a candidate the user found via + /api/enrichment/search (not limited to the stored review list — this is + the escape hatch for a wrong auto-match too). Sets `manual`, the + highest-authority state.""" + cand = _sanitize_candidate((data or {}).get("candidate")) + if not cand: + raise HTTPException(status_code=400, detail="candidate needs recording_id + title") + if not meta_db.set_enrichment_manual(filename, cand, source="search"): + raise HTTPException(status_code=404, detail="unknown song") + return {"ok": True, "enrichment": meta_db.get_enrichment(filename)} + + +@app.get("/api/enrichment/search") +def api_enrichment_search(artist: str = "", title: str = "", limit: int = 8, + filename: str = ""): + """Manual-search proxy to MusicBrainz (throttled + identified like the + background matcher — a user typing in the drawer must not sidestep the + rate limit). `filename` optionally scores results against that song's + stored identity (year/duration corroboration) instead of just the typed + text. Sync route on purpose: FastAPI runs it in the threadpool, so the + throttle's sleep never blocks the event loop.""" + if not (artist.strip() or title.strip()): + raise HTTPException(status_code=400, detail="artist or title required") + limit = max(1, min(int(limit), 25)) + try: + cands = _mb_search_recordings(artist, title, limit=limit) + except EnrichTransportError as e: + return JSONResponse({"error": "musicbrainz unavailable", "detail": str(e)}, + status_code=503) + ref = None + if filename: + ref = meta_db.enrichment_song_row(filename) + if ref is None: + ref = {"artist": artist, "title": title} + return {"candidates": mb_match.rank_candidates(ref, cands)} + + @app.get("/api/startup-status") def startup_status(): return _get_startup_status() @@ -6405,7 +6989,8 @@ async def list_library(q: str = "", page: int = 0, size: int = 24, sort: str = " stems_has: str = "", stems_lacks: str = "", has_lyrics: str = "", tunings: str = "", provider: str = "local", mastery: str = "", tags: str = "", user_difficulty: str = "", - after: str = "", group: int = 0, naming_mode: str = "legacy"): + match: str = "", after: str = "", group: int = 0, + naming_mode: str = "legacy"): """Paginated library search through the selected library provider. `after` is an opaque keyset cursor (feedBack#636 item 3): pass back the @@ -6433,6 +7018,7 @@ async def list_library(q: str = "", page: int = 0, size: int = 24, sort: str = " mastery=_split_csv(mastery), tags_has=_split_csv(tags), user_difficulty_in=_split_csv(user_difficulty), + match_states=_split_csv(match), **_library_filter_args( q=q, favorites=favorites, format=format, artist=artist, album=album, @@ -6544,6 +7130,7 @@ async def library_stats(favorites: int = 0, q: str = "", format: str = "", arrangements_has: str = "", arrangements_lacks: str = "", stems_has: str = "", stems_lacks: str = "", has_lyrics: str = "", tunings: str = "", provider: str = "local", + match: str = "", sort: str = "artist", sort_letters: int = 0, group: int = 0, naming_mode: str = "legacy"): """Aggregate stats for the UI. Accepts the same filter params as @@ -6561,6 +7148,10 @@ async def library_stats(favorites: int = 0, q: str = "", format: str = "", sort=sort, want_sort_letters=bool(sort_letters), group=bool(group), + # The match facet rides the stats call too — the A–Z rail's letter + # counts must agree with the grid under the facet or its cumulative + # seek + sizer geometry break. + match_states=_split_csv(match), **_library_filter_args( q=q, favorites=favorites, format=format, artist=artist, album=album, @@ -7862,6 +8453,17 @@ def _default_settings(): # renderer to gate its saved-chain restore. Inert on the pure-web build, # which has no native amp sims. "use_amp_sims": False, + # Metadata matching (P8). `enrich_enabled` gates only the BACKGROUND + # matcher — manual Fix-match/search in the review modal keeps working + # when it's off (the media-server model: scraper off ≠ no manual fix); + # the FEEDBACK_ENRICH_OFFLINE env var is the hard everything-off kill. + # `enrich_auto_threshold` is the auto-apply confidence — matches at or + # above it canonicalize automatically, below it queue for review. The + # per-field floors in lib/mb_match.py always apply on top, so lowering + # this can't make a wrong-artist cover auto-match. >1.0 (the "Always + # review" option) sends every text match to review. + "enrich_enabled": True, + "enrich_auto_threshold": 0.9, } @@ -8010,6 +8612,28 @@ def save_settings(data: dict): if not isinstance(raw, bool): return {"error": "use_amp_sims must be a boolean"} updates["use_amp_sims"] = raw + if "enrich_enabled" in data: + raw = data["enrich_enabled"] + if raw is not None: + if not isinstance(raw, bool): + return {"error": "enrich_enabled must be a boolean"} + updates["enrich_enabled"] = raw + if "enrich_auto_threshold" in data: + # Auto-apply confidence for the metadata matcher. 0.5–1.0 are real + # thresholds; values just above 1.0 are the "Always review" option (a + # capped score can equal exactly 1.0, so "never auto" must sit above + # the cap). Same defensive coercion shape as av_offset_ms. + raw = data["enrich_auto_threshold"] + if raw is not None: + if isinstance(raw, bool): + return {"error": "enrich_auto_threshold must be a number between 0.5 and 1.01"} + try: + t = float(raw) + except (TypeError, ValueError, OverflowError): + return {"error": "enrich_auto_threshold must be a number between 0.5 and 1.01"} + if not math.isfinite(t) or not (0.5 <= t <= 1.01): + return {"error": "enrich_auto_threshold must be a number between 0.5 and 1.01"} + updates["enrich_auto_threshold"] = t if "miss_penalty" in data: raw = data["miss_penalty"] if raw is not None: diff --git a/static/tailwind.min.css b/static/tailwind.min.css index c4090ce..2f9c6b9 100644 --- a/static/tailwind.min.css +++ b/static/tailwind.min.css @@ -1,2 +1,2 @@ *,: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%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.-left-\[1\.4375rem\]{left:-1.4375rem}.-right-\[1\.4375rem\]{right:-1.4375rem}.bottom-0{bottom:0}.bottom-1{bottom:.25rem}.bottom-1\.5{bottom:.375rem}.bottom-4{bottom:1rem}.bottom-5{bottom:1.25rem}.bottom-6{bottom:1.5rem}.bottom-\[30\%\]{bottom:30%}.bottom-full{bottom:100%}.left-0{left:0}.left-0\.5{left:.125rem}.left-1\.5{left:.375rem}.left-1\/2{left:50%}.left-2{left:.5rem}.left-2\.5{left:.625rem}.left-3{left:.75rem}.left-9{left:2.25rem}.left-\[30\%\]{left:30%}.right-0{right:0}.right-0\.5{right:.125rem}.right-1\.5{right:.375rem}.right-2{right:.5rem}.right-3{right:.75rem}.right-4{right:1rem}.right-5{right:1.25rem}.right-\[30\%\]{right:30%}.top-0{top:0}.top-1{top:.25rem}.top-1\.5{top:.375rem}.top-1\/2{top:50%}.top-10{top:2.5rem}.top-2{top:.5rem}.top-3{top:.75rem}.top-4{top:1rem}.top-\[30\%\]{top:30%}.top-full{top:100%}.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}.z-\[1000\]{z-index:1000}.z-\[1001\]{z-index:1001}.z-\[100\]{z-index:100}.z-\[120\]{z-index:120}.z-\[200\]{z-index:200}.z-\[210\]{z-index:210}.z-\[250\]{z-index:250}.z-\[300\]{z-index:300}.z-\[60\]{z-index:60}.z-\[61\]{z-index:61}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.m-1{margin:.25rem}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.-mx-6{margin-left:-1.5rem;margin-right:-1.5rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-10{margin-bottom:2.5rem}.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-3{margin-left:.75rem}.ml-5{margin-left:1.25rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-10{margin-top:2.5rem}.mt-2{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-auto{margin-top:auto}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.\!block{display:block!important}.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!important}.grid{display:grid}.contents{display:contents}.hidden{display:none}.aspect-\[3\/1\]{aspect-ratio:3/1}.aspect-square{aspect-ratio:1/1}.aspect-video{aspect-ratio:16/9}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-1\/2{height:50%}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-2\/3{height:66.666667%}.h-20{height:5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-32{height:8rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[3px\]{height:3px}.h-\[92px\]{height:92px}.h-\[calc\(50\%-1\.5px\)\]{height:calc(50% - 1.5px)}.h-\[calc\(50\%-2\.5px\)\]{height:calc(50% - 2.5px)}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-32{max-height:8rem}.max-h-64{max-height:16rem}.max-h-72{max-height:18rem}.max-h-96{max-height:24rem}.max-h-\[80vh\]{max-height:80vh}.max-h-\[85vh\]{max-height:85vh}.min-h-5{min-height:1.25rem}.min-h-6{min-height:1.5rem}.min-h-\[18rem\]{min-height:18rem}.min-h-\[2\.5em\]{min-height:2.5em}.min-h-\[480px\]{min-height:480px}.min-h-\[6rem\]{min-height:6rem}.w-0\.5{width:.125rem}.w-1{width:.25rem}.w-1\/2{width:50%}.w-10{width:2.5rem}.w-12{width:3rem}.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{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-60{width:15rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-\[96px\]{width:96px}.w-\[calc\(50\%-1px\)\]{width:calc(50% - 1px)}.w-\[calc\(50\%-2px\)\]{width:calc(50% - 2px)}.w-full{width:100%}.min-w-0{min-width:0}.min-w-40{min-width:10rem}.min-w-\[1\.25rem\]{min-width:1.25rem}.min-w-\[1\.75rem\]{min-width:1.75rem}.min-w-\[10rem\]{min-width:10rem}.min-w-\[180px\]{min-width:180px}.min-w-full{min-width:100%}.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-7xl{max-width:80rem}.max-w-\[11rem\]{max-width:11rem}.max-w-\[130px\]{max-width:130px}.max-w-\[160px\]{max-width:160px}.max-w-\[210px\]{max-width:210px}.max-w-\[5\.5rem\]{max-width:5.5rem}.max-w-\[90\%\]{max-width:90%}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-y-1\/2{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%}.translate-x-full{--tw-translate-x:100%}.-rotate-12,.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))}.-rotate-12{--tw-rotate:-12deg}.rotate-12{--tw-rotate:12deg}.rotate-12,.rotate-45{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))}.rotate-45{--tw-rotate:45deg}.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))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-default{cursor:default}.cursor-grab{cursor:grab}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.resize{resize:both}.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-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-10{gap:2.5rem}.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}.gap-\[3px\]{gap:3px}.gap-px{gap:1px}.space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.125rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem*var(--tw-space-y-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-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem*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-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*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-fb-border\/30>:not([hidden])~:not([hidden]){border-color:rgba(51,65,85,.3)}.self-start{align-self:flex-start}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.scroll-smooth{scroll-behavior:smooth}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.text-wrap{text-wrap:wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.rounded-bl-md{border-bottom-left-radius:.375rem}.rounded-br-md{border-bottom-right-radius:.375rem}.rounded-tl-md{border-top-left-radius:.375rem}.rounded-tr-md{border-top-right-radius:.375rem}.border{border-width:1px}.border-0{border-width:0}.border-2{border-width:2px}.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-none{border-style:none}.border-accent{--tw-border-opacity:1;border-color:rgb(64 128 224/var(--tw-border-opacity,1))}.border-accent-dark{--tw-border-opacity:1;border-color:rgb(32 96 176/var(--tw-border-opacity,1))}.border-accent-dark\/0{border-color:rgba(32,96,176,0)}.border-accent-dark\/10{border-color:rgba(32,96,176,.1)}.border-accent-dark\/100{border-color:#2060b0}.border-accent-dark\/15{border-color:rgba(32,96,176,.15)}.border-accent-dark\/20{border-color:rgba(32,96,176,.2)}.border-accent-dark\/25{border-color:rgba(32,96,176,.25)}.border-accent-dark\/30{border-color:rgba(32,96,176,.3)}.border-accent-dark\/35{border-color:rgba(32,96,176,.35)}.border-accent-dark\/40{border-color:rgba(32,96,176,.4)}.border-accent-dark\/45{border-color:rgba(32,96,176,.45)}.border-accent-dark\/5{border-color:rgba(32,96,176,.05)}.border-accent-dark\/50{border-color:rgba(32,96,176,.5)}.border-accent-dark\/55{border-color:rgba(32,96,176,.55)}.border-accent-dark\/60{border-color:rgba(32,96,176,.6)}.border-accent-dark\/65{border-color:rgba(32,96,176,.65)}.border-accent-dark\/70{border-color:rgba(32,96,176,.7)}.border-accent-dark\/75{border-color:rgba(32,96,176,.75)}.border-accent-dark\/80{border-color:rgba(32,96,176,.8)}.border-accent-dark\/85{border-color:rgba(32,96,176,.85)}.border-accent-dark\/90{border-color:rgba(32,96,176,.9)}.border-accent-dark\/95{border-color:rgba(32,96,176,.95)}.border-accent-light{--tw-border-opacity:1;border-color:rgb(96 160 255/var(--tw-border-opacity,1))}.border-accent-light\/0{border-color:rgba(96,160,255,0)}.border-accent-light\/10{border-color:rgba(96,160,255,.1)}.border-accent-light\/100{border-color:#60a0ff}.border-accent-light\/15{border-color:rgba(96,160,255,.15)}.border-accent-light\/20{border-color:rgba(96,160,255,.2)}.border-accent-light\/25{border-color:rgba(96,160,255,.25)}.border-accent-light\/30{border-color:rgba(96,160,255,.3)}.border-accent-light\/35{border-color:rgba(96,160,255,.35)}.border-accent-light\/40{border-color:rgba(96,160,255,.4)}.border-accent-light\/45{border-color:rgba(96,160,255,.45)}.border-accent-light\/5{border-color:rgba(96,160,255,.05)}.border-accent-light\/50{border-color:rgba(96,160,255,.5)}.border-accent-light\/55{border-color:rgba(96,160,255,.55)}.border-accent-light\/60{border-color:rgba(96,160,255,.6)}.border-accent-light\/65{border-color:rgba(96,160,255,.65)}.border-accent-light\/70{border-color:rgba(96,160,255,.7)}.border-accent-light\/75{border-color:rgba(96,160,255,.75)}.border-accent-light\/80{border-color:rgba(96,160,255,.8)}.border-accent-light\/85{border-color:rgba(96,160,255,.85)}.border-accent-light\/90{border-color:rgba(96,160,255,.9)}.border-accent-light\/95{border-color:rgba(96,160,255,.95)}.border-accent\/20{border-color:rgba(64,128,224,.2)}.border-accent\/40{border-color:rgba(64,128,224,.4)}.border-amber-100{--tw-border-opacity:1;border-color:rgb(254 243 199/var(--tw-border-opacity,1))}.border-amber-200{--tw-border-opacity:1;border-color:rgb(253 230 138/var(--tw-border-opacity,1))}.border-amber-300{--tw-border-opacity:1;border-color:rgb(252 211 77/var(--tw-border-opacity,1))}.border-amber-400{--tw-border-opacity:1;border-color:rgb(251 191 36/var(--tw-border-opacity,1))}.border-amber-50{--tw-border-opacity:1;border-color:rgb(255 251 235/var(--tw-border-opacity,1))}.border-amber-500{--tw-border-opacity:1;border-color:rgb(245 158 11/var(--tw-border-opacity,1))}.border-amber-500\/30{border-color:rgba(245,158,11,.3)}.border-amber-600{--tw-border-opacity:1;border-color:rgb(217 119 6/var(--tw-border-opacity,1))}.border-amber-700{--tw-border-opacity:1;border-color:rgb(180 83 9/var(--tw-border-opacity,1))}.border-amber-700\/60{border-color:rgba(180,83,9,.6)}.border-amber-800{--tw-border-opacity:1;border-color:rgb(146 64 14/var(--tw-border-opacity,1))}.border-amber-800\/60{border-color:rgba(146,64,14,.6)}.border-amber-900{--tw-border-opacity:1;border-color:rgb(120 53 15/var(--tw-border-opacity,1))}.border-blue-100{--tw-border-opacity:1;border-color:rgb(219 234 254/var(--tw-border-opacity,1))}.border-blue-200{--tw-border-opacity:1;border-color:rgb(191 219 254/var(--tw-border-opacity,1))}.border-blue-300{--tw-border-opacity:1;border-color:rgb(147 197 253/var(--tw-border-opacity,1))}.border-blue-400{--tw-border-opacity:1;border-color:rgb(96 165 250/var(--tw-border-opacity,1))}.border-blue-50{--tw-border-opacity:1;border-color:rgb(239 246 255/var(--tw-border-opacity,1))}.border-blue-500{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity,1))}.border-blue-600{--tw-border-opacity:1;border-color:rgb(37 99 235/var(--tw-border-opacity,1))}.border-blue-700{--tw-border-opacity:1;border-color:rgb(29 78 216/var(--tw-border-opacity,1))}.border-blue-700\/60{border-color:rgba(29,78,216,.6)}.border-blue-800{--tw-border-opacity:1;border-color:rgb(30 64 175/var(--tw-border-opacity,1))}.border-blue-900{--tw-border-opacity:1;border-color:rgb(30 58 138/var(--tw-border-opacity,1))}.border-dark-500{--tw-border-opacity:1;border-color:rgb(30 30 58/var(--tw-border-opacity,1))}.border-dark-500\/0{border-color:rgba(30,30,58,0)}.border-dark-500\/10{border-color:rgba(30,30,58,.1)}.border-dark-500\/100{border-color:#1e1e3a}.border-dark-500\/15{border-color:rgba(30,30,58,.15)}.border-dark-500\/20{border-color:rgba(30,30,58,.2)}.border-dark-500\/25{border-color:rgba(30,30,58,.25)}.border-dark-500\/30{border-color:rgba(30,30,58,.3)}.border-dark-500\/35{border-color:rgba(30,30,58,.35)}.border-dark-500\/40{border-color:rgba(30,30,58,.4)}.border-dark-500\/45{border-color:rgba(30,30,58,.45)}.border-dark-500\/5{border-color:rgba(30,30,58,.05)}.border-dark-500\/50{border-color:rgba(30,30,58,.5)}.border-dark-500\/55{border-color:rgba(30,30,58,.55)}.border-dark-500\/60{border-color:rgba(30,30,58,.6)}.border-dark-500\/65{border-color:rgba(30,30,58,.65)}.border-dark-500\/70{border-color:rgba(30,30,58,.7)}.border-dark-500\/75{border-color:rgba(30,30,58,.75)}.border-dark-500\/80{border-color:rgba(30,30,58,.8)}.border-dark-500\/85{border-color:rgba(30,30,58,.85)}.border-dark-500\/90{border-color:rgba(30,30,58,.9)}.border-dark-500\/95{border-color:rgba(30,30,58,.95)}.border-dark-600{--tw-border-opacity:1;border-color:rgb(24 24 48/var(--tw-border-opacity,1))}.border-dark-600\/0{border-color:rgba(24,24,48,0)}.border-dark-600\/10{border-color:rgba(24,24,48,.1)}.border-dark-600\/100{border-color:#181830}.border-dark-600\/15{border-color:rgba(24,24,48,.15)}.border-dark-600\/20{border-color:rgba(24,24,48,.2)}.border-dark-600\/25{border-color:rgba(24,24,48,.25)}.border-dark-600\/30{border-color:rgba(24,24,48,.3)}.border-dark-600\/35{border-color:rgba(24,24,48,.35)}.border-dark-600\/40{border-color:rgba(24,24,48,.4)}.border-dark-600\/45{border-color:rgba(24,24,48,.45)}.border-dark-600\/5{border-color:rgba(24,24,48,.05)}.border-dark-600\/50{border-color:rgba(24,24,48,.5)}.border-dark-600\/55{border-color:rgba(24,24,48,.55)}.border-dark-600\/60{border-color:rgba(24,24,48,.6)}.border-dark-600\/65{border-color:rgba(24,24,48,.65)}.border-dark-600\/70{border-color:rgba(24,24,48,.7)}.border-dark-600\/75{border-color:rgba(24,24,48,.75)}.border-dark-600\/80{border-color:rgba(24,24,48,.8)}.border-dark-600\/85{border-color:rgba(24,24,48,.85)}.border-dark-600\/90{border-color:rgba(24,24,48,.9)}.border-dark-600\/95{border-color:rgba(24,24,48,.95)}.border-dark-700{--tw-border-opacity:1;border-color:rgb(16 16 30/var(--tw-border-opacity,1))}.border-dark-700\/0{border-color:rgba(16,16,30,0)}.border-dark-700\/10{border-color:rgba(16,16,30,.1)}.border-dark-700\/100{border-color:#10101e}.border-dark-700\/15{border-color:rgba(16,16,30,.15)}.border-dark-700\/20{border-color:rgba(16,16,30,.2)}.border-dark-700\/25{border-color:rgba(16,16,30,.25)}.border-dark-700\/30{border-color:rgba(16,16,30,.3)}.border-dark-700\/35{border-color:rgba(16,16,30,.35)}.border-dark-700\/40{border-color:rgba(16,16,30,.4)}.border-dark-700\/45{border-color:rgba(16,16,30,.45)}.border-dark-700\/5{border-color:rgba(16,16,30,.05)}.border-dark-700\/50{border-color:rgba(16,16,30,.5)}.border-dark-700\/55{border-color:rgba(16,16,30,.55)}.border-dark-700\/60{border-color:rgba(16,16,30,.6)}.border-dark-700\/65{border-color:rgba(16,16,30,.65)}.border-dark-700\/70{border-color:rgba(16,16,30,.7)}.border-dark-700\/75{border-color:rgba(16,16,30,.75)}.border-dark-700\/80{border-color:rgba(16,16,30,.8)}.border-dark-700\/85{border-color:rgba(16,16,30,.85)}.border-dark-700\/90{border-color:rgba(16,16,30,.9)}.border-dark-700\/95{border-color:rgba(16,16,30,.95)}.border-dark-800{--tw-border-opacity:1;border-color:rgb(10 10 18/var(--tw-border-opacity,1))}.border-dark-800\/0{border-color:rgba(10,10,18,0)}.border-dark-800\/10{border-color:rgba(10,10,18,.1)}.border-dark-800\/100{border-color:#0a0a12}.border-dark-800\/15{border-color:rgba(10,10,18,.15)}.border-dark-800\/20{border-color:rgba(10,10,18,.2)}.border-dark-800\/25{border-color:rgba(10,10,18,.25)}.border-dark-800\/30{border-color:rgba(10,10,18,.3)}.border-dark-800\/35{border-color:rgba(10,10,18,.35)}.border-dark-800\/40{border-color:rgba(10,10,18,.4)}.border-dark-800\/45{border-color:rgba(10,10,18,.45)}.border-dark-800\/5{border-color:rgba(10,10,18,.05)}.border-dark-800\/50{border-color:rgba(10,10,18,.5)}.border-dark-800\/55{border-color:rgba(10,10,18,.55)}.border-dark-800\/60{border-color:rgba(10,10,18,.6)}.border-dark-800\/65{border-color:rgba(10,10,18,.65)}.border-dark-800\/70{border-color:rgba(10,10,18,.7)}.border-dark-800\/75{border-color:rgba(10,10,18,.75)}.border-dark-800\/80{border-color:rgba(10,10,18,.8)}.border-dark-800\/85{border-color:rgba(10,10,18,.85)}.border-dark-800\/90{border-color:rgba(10,10,18,.9)}.border-dark-800\/95{border-color:rgba(10,10,18,.95)}.border-dark-900{--tw-border-opacity:1;border-color:rgb(5 5 8/var(--tw-border-opacity,1))}.border-dark-900\/0{border-color:rgba(5,5,8,0)}.border-dark-900\/10{border-color:rgba(5,5,8,.1)}.border-dark-900\/100{border-color:#050508}.border-dark-900\/15{border-color:rgba(5,5,8,.15)}.border-dark-900\/20{border-color:rgba(5,5,8,.2)}.border-dark-900\/25{border-color:rgba(5,5,8,.25)}.border-dark-900\/30{border-color:rgba(5,5,8,.3)}.border-dark-900\/35{border-color:rgba(5,5,8,.35)}.border-dark-900\/40{border-color:rgba(5,5,8,.4)}.border-dark-900\/45{border-color:rgba(5,5,8,.45)}.border-dark-900\/5{border-color:rgba(5,5,8,.05)}.border-dark-900\/50{border-color:rgba(5,5,8,.5)}.border-dark-900\/55{border-color:rgba(5,5,8,.55)}.border-dark-900\/60{border-color:rgba(5,5,8,.6)}.border-dark-900\/65{border-color:rgba(5,5,8,.65)}.border-dark-900\/70{border-color:rgba(5,5,8,.7)}.border-dark-900\/75{border-color:rgba(5,5,8,.75)}.border-dark-900\/80{border-color:rgba(5,5,8,.8)}.border-dark-900\/85{border-color:rgba(5,5,8,.85)}.border-dark-900\/90{border-color:rgba(5,5,8,.9)}.border-dark-900\/95{border-color:rgba(5,5,8,.95)}.border-emerald-700\/60{border-color:rgba(4,120,87,.6)}.border-emerald-800\/60{border-color:rgba(6,95,70,.6)}.border-fb-border{--tw-border-opacity:1;border-color:rgb(51 65 85/var(--tw-border-opacity,1))}.border-fb-border\/30{border-color:rgba(51,65,85,.3)}.border-fb-border\/40{border-color:rgba(51,65,85,.4)}.border-fb-border\/50{border-color:rgba(51,65,85,.5)}.border-fb-border\/60{border-color:rgba(51,65,85,.6)}.border-fb-border\/70{border-color:rgba(51,65,85,.7)}.border-fb-good\/40{border-color:rgba(34,197,94,.4)}.border-fb-good\/50{border-color:rgba(34,197,94,.5)}.border-fb-low\/40{border-color:rgba(239,68,68,.4)}.border-fb-primary{--tw-border-opacity:1;border-color:rgb(14 165 233/var(--tw-border-opacity,1))}.border-fb-primary\/30{border-color:rgba(14,165,233,.3)}.border-fb-primary\/40{border-color:rgba(14,165,233,.4)}.border-fb-primary\/50{border-color:rgba(14,165,233,.5)}.border-fb-primary\/60{border-color:rgba(14,165,233,.6)}.border-fb-textDim{--tw-border-opacity:1;border-color:rgb(148 163 184/var(--tw-border-opacity,1))}.border-gold{--tw-border-opacity:1;border-color:rgb(232 192 64/var(--tw-border-opacity,1))}.border-gold\/20{border-color:rgba(232,192,64,.2)}.border-gold\/40{border-color:rgba(232,192,64,.4)}.border-gold\/50{border-color:rgba(232,192,64,.5)}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity,1))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/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-400{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity,1))}.border-gray-50{--tw-border-opacity:1;border-color:rgb(249 250 251/var(--tw-border-opacity,1))}.border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/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-gray-700\/60{border-color:rgba(55,65,81,.6)}.border-gray-800{--tw-border-opacity:1;border-color:rgb(31 41 55/var(--tw-border-opacity,1))}.border-gray-800\/50{border-color:rgba(31,41,55,.5)}.border-gray-800\/70{border-color:rgba(31,41,55,.7)}.border-gray-900{--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity,1))}.border-green-100{--tw-border-opacity:1;border-color:rgb(220 252 231/var(--tw-border-opacity,1))}.border-green-200{--tw-border-opacity:1;border-color:rgb(187 247 208/var(--tw-border-opacity,1))}.border-green-300{--tw-border-opacity:1;border-color:rgb(134 239 172/var(--tw-border-opacity,1))}.border-green-400{--tw-border-opacity:1;border-color:rgb(74 222 128/var(--tw-border-opacity,1))}.border-green-50{--tw-border-opacity:1;border-color:rgb(240 253 244/var(--tw-border-opacity,1))}.border-green-500{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity,1))}.border-green-600{--tw-border-opacity:1;border-color:rgb(22 163 74/var(--tw-border-opacity,1))}.border-green-700{--tw-border-opacity:1;border-color:rgb(21 128 61/var(--tw-border-opacity,1))}.border-green-800{--tw-border-opacity:1;border-color:rgb(22 101 52/var(--tw-border-opacity,1))}.border-green-900{--tw-border-opacity:1;border-color:rgb(20 83 45/var(--tw-border-opacity,1))}.border-indigo-100{--tw-border-opacity:1;border-color:rgb(224 231 255/var(--tw-border-opacity,1))}.border-indigo-200{--tw-border-opacity:1;border-color:rgb(199 210 254/var(--tw-border-opacity,1))}.border-indigo-300{--tw-border-opacity:1;border-color:rgb(165 180 252/var(--tw-border-opacity,1))}.border-indigo-400{--tw-border-opacity:1;border-color:rgb(129 140 248/var(--tw-border-opacity,1))}.border-indigo-50{--tw-border-opacity:1;border-color:rgb(238 242 255/var(--tw-border-opacity,1))}.border-indigo-500{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.border-indigo-600{--tw-border-opacity:1;border-color:rgb(79 70 229/var(--tw-border-opacity,1))}.border-indigo-700{--tw-border-opacity:1;border-color:rgb(67 56 202/var(--tw-border-opacity,1))}.border-indigo-800{--tw-border-opacity:1;border-color:rgb(55 48 163/var(--tw-border-opacity,1))}.border-indigo-900{--tw-border-opacity:1;border-color:rgb(49 46 129/var(--tw-border-opacity,1))}.border-orange-200{--tw-border-opacity:1;border-color:rgb(254 215 170/var(--tw-border-opacity,1))}.border-pink-100{--tw-border-opacity:1;border-color:rgb(252 231 243/var(--tw-border-opacity,1))}.border-pink-200{--tw-border-opacity:1;border-color:rgb(251 207 232/var(--tw-border-opacity,1))}.border-pink-300{--tw-border-opacity:1;border-color:rgb(249 168 212/var(--tw-border-opacity,1))}.border-pink-400{--tw-border-opacity:1;border-color:rgb(244 114 182/var(--tw-border-opacity,1))}.border-pink-50{--tw-border-opacity:1;border-color:rgb(253 242 248/var(--tw-border-opacity,1))}.border-pink-500{--tw-border-opacity:1;border-color:rgb(236 72 153/var(--tw-border-opacity,1))}.border-pink-600{--tw-border-opacity:1;border-color:rgb(219 39 119/var(--tw-border-opacity,1))}.border-pink-700{--tw-border-opacity:1;border-color:rgb(190 24 93/var(--tw-border-opacity,1))}.border-pink-800{--tw-border-opacity:1;border-color:rgb(157 23 77/var(--tw-border-opacity,1))}.border-pink-900{--tw-border-opacity:1;border-color:rgb(131 24 67/var(--tw-border-opacity,1))}.border-purple-100{--tw-border-opacity:1;border-color:rgb(243 232 255/var(--tw-border-opacity,1))}.border-purple-200{--tw-border-opacity:1;border-color:rgb(233 213 255/var(--tw-border-opacity,1))}.border-purple-300{--tw-border-opacity:1;border-color:rgb(216 180 254/var(--tw-border-opacity,1))}.border-purple-400{--tw-border-opacity:1;border-color:rgb(192 132 252/var(--tw-border-opacity,1))}.border-purple-400\/30{border-color:rgba(192,132,252,.3)}.border-purple-400\/40{border-color:rgba(192,132,252,.4)}.border-purple-50{--tw-border-opacity:1;border-color:rgb(250 245 255/var(--tw-border-opacity,1))}.border-purple-500{--tw-border-opacity:1;border-color:rgb(168 85 247/var(--tw-border-opacity,1))}.border-purple-600{--tw-border-opacity:1;border-color:rgb(147 51 234/var(--tw-border-opacity,1))}.border-purple-700{--tw-border-opacity:1;border-color:rgb(126 34 206/var(--tw-border-opacity,1))}.border-purple-800{--tw-border-opacity:1;border-color:rgb(107 33 168/var(--tw-border-opacity,1))}.border-purple-900{--tw-border-opacity:1;border-color:rgb(88 28 135/var(--tw-border-opacity,1))}.border-red-100{--tw-border-opacity:1;border-color:rgb(254 226 226/var(--tw-border-opacity,1))}.border-red-200{--tw-border-opacity:1;border-color:rgb(254 202 202/var(--tw-border-opacity,1))}.border-red-300{--tw-border-opacity:1;border-color:rgb(252 165 165/var(--tw-border-opacity,1))}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113/var(--tw-border-opacity,1))}.border-red-50{--tw-border-opacity:1;border-color:rgb(254 242 242/var(--tw-border-opacity,1))}.border-red-500{--tw-border-opacity:1;border-color:rgb(239 68 68/var(--tw-border-opacity,1))}.border-red-500\/40{border-color:rgba(239,68,68,.4)}.border-red-600{--tw-border-opacity:1;border-color:rgb(220 38 38/var(--tw-border-opacity,1))}.border-red-700{--tw-border-opacity:1;border-color:rgb(185 28 28/var(--tw-border-opacity,1))}.border-red-700\/60{border-color:rgba(185,28,28,.6)}.border-red-800{--tw-border-opacity:1;border-color:rgb(153 27 27/var(--tw-border-opacity,1))}.border-red-800\/70{border-color:rgba(153,27,27,.7)}.border-red-900{--tw-border-opacity:1;border-color:rgb(127 29 29/var(--tw-border-opacity,1))}.border-red-900\/30{border-color:rgba(127,29,29,.3)}.border-red-900\/50{border-color:rgba(127,29,29,.5)}.border-slate-100{--tw-border-opacity:1;border-color:rgb(241 245 249/var(--tw-border-opacity,1))}.border-slate-200{--tw-border-opacity:1;border-color:rgb(226 232 240/var(--tw-border-opacity,1))}.border-slate-300{--tw-border-opacity:1;border-color:rgb(203 213 225/var(--tw-border-opacity,1))}.border-slate-400{--tw-border-opacity:1;border-color:rgb(148 163 184/var(--tw-border-opacity,1))}.border-slate-50{--tw-border-opacity:1;border-color:rgb(248 250 252/var(--tw-border-opacity,1))}.border-slate-500{--tw-border-opacity:1;border-color:rgb(100 116 139/var(--tw-border-opacity,1))}.border-slate-600{--tw-border-opacity:1;border-color:rgb(71 85 105/var(--tw-border-opacity,1))}.border-slate-700{--tw-border-opacity:1;border-color:rgb(51 65 85/var(--tw-border-opacity,1))}.border-slate-800{--tw-border-opacity:1;border-color:rgb(30 41 59/var(--tw-border-opacity,1))}.border-slate-900{--tw-border-opacity:1;border-color:rgb(15 23 42/var(--tw-border-opacity,1))}.border-teal-700\/60{border-color:rgba(15,118,110,.6)}.border-violet-100{--tw-border-opacity:1;border-color:rgb(237 233 254/var(--tw-border-opacity,1))}.border-yellow-100{--tw-border-opacity:1;border-color:rgb(254 249 195/var(--tw-border-opacity,1))}.border-yellow-200{--tw-border-opacity:1;border-color:rgb(254 240 138/var(--tw-border-opacity,1))}.border-yellow-300{--tw-border-opacity:1;border-color:rgb(253 224 71/var(--tw-border-opacity,1))}.border-yellow-400{--tw-border-opacity:1;border-color:rgb(250 204 21/var(--tw-border-opacity,1))}.border-yellow-400\/40{border-color:rgba(250,204,21,.4)}.border-yellow-50{--tw-border-opacity:1;border-color:rgb(254 252 232/var(--tw-border-opacity,1))}.border-yellow-500{--tw-border-opacity:1;border-color:rgb(234 179 8/var(--tw-border-opacity,1))}.border-yellow-600{--tw-border-opacity:1;border-color:rgb(202 138 4/var(--tw-border-opacity,1))}.border-yellow-700{--tw-border-opacity:1;border-color:rgb(161 98 7/var(--tw-border-opacity,1))}.border-yellow-800{--tw-border-opacity:1;border-color:rgb(133 77 14/var(--tw-border-opacity,1))}.border-yellow-900{--tw-border-opacity:1;border-color:rgb(113 63 18/var(--tw-border-opacity,1))}.\!bg-accent{--tw-bg-opacity:1!important;background-color:rgb(64 128 224/var(--tw-bg-opacity,1))!important}.bg-accent{--tw-bg-opacity:1;background-color:rgb(64 128 224/var(--tw-bg-opacity,1))}.bg-accent-dark{--tw-bg-opacity:1;background-color:rgb(32 96 176/var(--tw-bg-opacity,1))}.bg-accent-dark\/0{background-color:rgba(32,96,176,0)}.bg-accent-dark\/10{background-color:rgba(32,96,176,.1)}.bg-accent-dark\/100{background-color:#2060b0}.bg-accent-dark\/15{background-color:rgba(32,96,176,.15)}.bg-accent-dark\/20{background-color:rgba(32,96,176,.2)}.bg-accent-dark\/25{background-color:rgba(32,96,176,.25)}.bg-accent-dark\/30{background-color:rgba(32,96,176,.3)}.bg-accent-dark\/35{background-color:rgba(32,96,176,.35)}.bg-accent-dark\/40{background-color:rgba(32,96,176,.4)}.bg-accent-dark\/45{background-color:rgba(32,96,176,.45)}.bg-accent-dark\/5{background-color:rgba(32,96,176,.05)}.bg-accent-dark\/50{background-color:rgba(32,96,176,.5)}.bg-accent-dark\/55{background-color:rgba(32,96,176,.55)}.bg-accent-dark\/60{background-color:rgba(32,96,176,.6)}.bg-accent-dark\/65{background-color:rgba(32,96,176,.65)}.bg-accent-dark\/70{background-color:rgba(32,96,176,.7)}.bg-accent-dark\/75{background-color:rgba(32,96,176,.75)}.bg-accent-dark\/80{background-color:rgba(32,96,176,.8)}.bg-accent-dark\/85{background-color:rgba(32,96,176,.85)}.bg-accent-dark\/90{background-color:rgba(32,96,176,.9)}.bg-accent-dark\/95{background-color:rgba(32,96,176,.95)}.bg-accent-light{--tw-bg-opacity:1;background-color:rgb(96 160 255/var(--tw-bg-opacity,1))}.bg-accent-light\/0{background-color:rgba(96,160,255,0)}.bg-accent-light\/10{background-color:rgba(96,160,255,.1)}.bg-accent-light\/100{background-color:#60a0ff}.bg-accent-light\/15{background-color:rgba(96,160,255,.15)}.bg-accent-light\/20{background-color:rgba(96,160,255,.2)}.bg-accent-light\/25{background-color:rgba(96,160,255,.25)}.bg-accent-light\/30{background-color:rgba(96,160,255,.3)}.bg-accent-light\/35{background-color:rgba(96,160,255,.35)}.bg-accent-light\/40{background-color:rgba(96,160,255,.4)}.bg-accent-light\/45{background-color:rgba(96,160,255,.45)}.bg-accent-light\/5{background-color:rgba(96,160,255,.05)}.bg-accent-light\/50{background-color:rgba(96,160,255,.5)}.bg-accent-light\/55{background-color:rgba(96,160,255,.55)}.bg-accent-light\/60{background-color:rgba(96,160,255,.6)}.bg-accent-light\/65{background-color:rgba(96,160,255,.65)}.bg-accent-light\/70{background-color:rgba(96,160,255,.7)}.bg-accent-light\/75{background-color:rgba(96,160,255,.75)}.bg-accent-light\/80{background-color:rgba(96,160,255,.8)}.bg-accent-light\/85{background-color:rgba(96,160,255,.85)}.bg-accent-light\/90{background-color:rgba(96,160,255,.9)}.bg-accent-light\/95{background-color:rgba(96,160,255,.95)}.bg-accent\/10{background-color:rgba(64,128,224,.1)}.bg-accent\/20{background-color:rgba(64,128,224,.2)}.bg-accent\/30{background-color:rgba(64,128,224,.3)}.bg-amber-100{--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity,1))}.bg-amber-200{--tw-bg-opacity:1;background-color:rgb(253 230 138/var(--tw-bg-opacity,1))}.bg-amber-300{--tw-bg-opacity:1;background-color:rgb(252 211 77/var(--tw-bg-opacity,1))}.bg-amber-400{--tw-bg-opacity:1;background-color:rgb(251 191 36/var(--tw-bg-opacity,1))}.bg-amber-50{--tw-bg-opacity:1;background-color:rgb(255 251 235/var(--tw-bg-opacity,1))}.bg-amber-500{--tw-bg-opacity:1;background-color:rgb(245 158 11/var(--tw-bg-opacity,1))}.bg-amber-500\/10{background-color:rgba(245,158,11,.1)}.bg-amber-500\/5{background-color:rgba(245,158,11,.05)}.bg-amber-600{--tw-bg-opacity:1;background-color:rgb(217 119 6/var(--tw-bg-opacity,1))}.bg-amber-700{--tw-bg-opacity:1;background-color:rgb(180 83 9/var(--tw-bg-opacity,1))}.bg-amber-800{--tw-bg-opacity:1;background-color:rgb(146 64 14/var(--tw-bg-opacity,1))}.bg-amber-900{--tw-bg-opacity:1;background-color:rgb(120 53 15/var(--tw-bg-opacity,1))}.bg-amber-900\/30{background-color:rgba(120,53,15,.3)}.bg-amber-900\/60{background-color:rgba(120,53,15,.6)}.bg-amber-900\/80{background-color:rgba(120,53,15,.8)}.bg-black\/30{background-color:rgba(0,0,0,.3)}.bg-black\/40{background-color:rgba(0,0,0,.4)}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-black\/55{background-color:rgba(0,0,0,.55)}.bg-black\/60{background-color:rgba(0,0,0,.6)}.bg-black\/70{background-color:rgba(0,0,0,.7)}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity,1))}.bg-blue-200{--tw-bg-opacity:1;background-color:rgb(191 219 254/var(--tw-bg-opacity,1))}.bg-blue-300{--tw-bg-opacity:1;background-color:rgb(147 197 253/var(--tw-bg-opacity,1))}.bg-blue-400{--tw-bg-opacity:1;background-color:rgb(96 165 250/var(--tw-bg-opacity,1))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity,1))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.bg-blue-500\/10{background-color:rgba(59,130,246,.1)}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.bg-blue-700{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity,1))}.bg-blue-800{--tw-bg-opacity:1;background-color:rgb(30 64 175/var(--tw-bg-opacity,1))}.bg-blue-900{--tw-bg-opacity:1;background-color:rgb(30 58 138/var(--tw-bg-opacity,1))}.bg-blue-900\/40{background-color:rgba(30,58,138,.4)}.bg-current{background-color:currentColor}.bg-cyan-400{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity,1))}.bg-dark-500{--tw-bg-opacity:1;background-color:rgb(30 30 58/var(--tw-bg-opacity,1))}.bg-dark-500\/0{background-color:rgba(30,30,58,0)}.bg-dark-500\/10{background-color:rgba(30,30,58,.1)}.bg-dark-500\/100{background-color:#1e1e3a}.bg-dark-500\/15{background-color:rgba(30,30,58,.15)}.bg-dark-500\/20{background-color:rgba(30,30,58,.2)}.bg-dark-500\/25{background-color:rgba(30,30,58,.25)}.bg-dark-500\/30{background-color:rgba(30,30,58,.3)}.bg-dark-500\/35{background-color:rgba(30,30,58,.35)}.bg-dark-500\/40{background-color:rgba(30,30,58,.4)}.bg-dark-500\/45{background-color:rgba(30,30,58,.45)}.bg-dark-500\/5{background-color:rgba(30,30,58,.05)}.bg-dark-500\/50{background-color:rgba(30,30,58,.5)}.bg-dark-500\/55{background-color:rgba(30,30,58,.55)}.bg-dark-500\/60{background-color:rgba(30,30,58,.6)}.bg-dark-500\/65{background-color:rgba(30,30,58,.65)}.bg-dark-500\/70{background-color:rgba(30,30,58,.7)}.bg-dark-500\/75{background-color:rgba(30,30,58,.75)}.bg-dark-500\/80{background-color:rgba(30,30,58,.8)}.bg-dark-500\/85{background-color:rgba(30,30,58,.85)}.bg-dark-500\/90{background-color:rgba(30,30,58,.9)}.bg-dark-500\/95{background-color:rgba(30,30,58,.95)}.bg-dark-600{--tw-bg-opacity:1;background-color:rgb(24 24 48/var(--tw-bg-opacity,1))}.bg-dark-600\/0{background-color:rgba(24,24,48,0)}.bg-dark-600\/10{background-color:rgba(24,24,48,.1)}.bg-dark-600\/100{background-color:#181830}.bg-dark-600\/15{background-color:rgba(24,24,48,.15)}.bg-dark-600\/20{background-color:rgba(24,24,48,.2)}.bg-dark-600\/25{background-color:rgba(24,24,48,.25)}.bg-dark-600\/30{background-color:rgba(24,24,48,.3)}.bg-dark-600\/35{background-color:rgba(24,24,48,.35)}.bg-dark-600\/40{background-color:rgba(24,24,48,.4)}.bg-dark-600\/45{background-color:rgba(24,24,48,.45)}.bg-dark-600\/5{background-color:rgba(24,24,48,.05)}.bg-dark-600\/50{background-color:rgba(24,24,48,.5)}.bg-dark-600\/55{background-color:rgba(24,24,48,.55)}.bg-dark-600\/60{background-color:rgba(24,24,48,.6)}.bg-dark-600\/65{background-color:rgba(24,24,48,.65)}.bg-dark-600\/70{background-color:rgba(24,24,48,.7)}.bg-dark-600\/75{background-color:rgba(24,24,48,.75)}.bg-dark-600\/80{background-color:rgba(24,24,48,.8)}.bg-dark-600\/85{background-color:rgba(24,24,48,.85)}.bg-dark-600\/90{background-color:rgba(24,24,48,.9)}.bg-dark-600\/95{background-color:rgba(24,24,48,.95)}.bg-dark-700{--tw-bg-opacity:1;background-color:rgb(16 16 30/var(--tw-bg-opacity,1))}.bg-dark-700\/0{background-color:rgba(16,16,30,0)}.bg-dark-700\/10{background-color:rgba(16,16,30,.1)}.bg-dark-700\/100{background-color:#10101e}.bg-dark-700\/15{background-color:rgba(16,16,30,.15)}.bg-dark-700\/20{background-color:rgba(16,16,30,.2)}.bg-dark-700\/25{background-color:rgba(16,16,30,.25)}.bg-dark-700\/30{background-color:rgba(16,16,30,.3)}.bg-dark-700\/35{background-color:rgba(16,16,30,.35)}.bg-dark-700\/40{background-color:rgba(16,16,30,.4)}.bg-dark-700\/45{background-color:rgba(16,16,30,.45)}.bg-dark-700\/5{background-color:rgba(16,16,30,.05)}.bg-dark-700\/50{background-color:rgba(16,16,30,.5)}.bg-dark-700\/55{background-color:rgba(16,16,30,.55)}.bg-dark-700\/60{background-color:rgba(16,16,30,.6)}.bg-dark-700\/65{background-color:rgba(16,16,30,.65)}.bg-dark-700\/70{background-color:rgba(16,16,30,.7)}.bg-dark-700\/75{background-color:rgba(16,16,30,.75)}.bg-dark-700\/80{background-color:rgba(16,16,30,.8)}.bg-dark-700\/85{background-color:rgba(16,16,30,.85)}.bg-dark-700\/90{background-color:rgba(16,16,30,.9)}.bg-dark-700\/95{background-color:rgba(16,16,30,.95)}.bg-dark-800{--tw-bg-opacity:1;background-color:rgb(10 10 18/var(--tw-bg-opacity,1))}.bg-dark-800\/0{background-color:rgba(10,10,18,0)}.bg-dark-800\/10{background-color:rgba(10,10,18,.1)}.bg-dark-800\/100{background-color:#0a0a12}.bg-dark-800\/15{background-color:rgba(10,10,18,.15)}.bg-dark-800\/20{background-color:rgba(10,10,18,.2)}.bg-dark-800\/25{background-color:rgba(10,10,18,.25)}.bg-dark-800\/30{background-color:rgba(10,10,18,.3)}.bg-dark-800\/35{background-color:rgba(10,10,18,.35)}.bg-dark-800\/40{background-color:rgba(10,10,18,.4)}.bg-dark-800\/45{background-color:rgba(10,10,18,.45)}.bg-dark-800\/5{background-color:rgba(10,10,18,.05)}.bg-dark-800\/50{background-color:rgba(10,10,18,.5)}.bg-dark-800\/55{background-color:rgba(10,10,18,.55)}.bg-dark-800\/60{background-color:rgba(10,10,18,.6)}.bg-dark-800\/65{background-color:rgba(10,10,18,.65)}.bg-dark-800\/70{background-color:rgba(10,10,18,.7)}.bg-dark-800\/75{background-color:rgba(10,10,18,.75)}.bg-dark-800\/80{background-color:rgba(10,10,18,.8)}.bg-dark-800\/85{background-color:rgba(10,10,18,.85)}.bg-dark-800\/90{background-color:rgba(10,10,18,.9)}.bg-dark-800\/95{background-color:rgba(10,10,18,.95)}.bg-dark-900{--tw-bg-opacity:1;background-color:rgb(5 5 8/var(--tw-bg-opacity,1))}.bg-dark-900\/0{background-color:rgba(5,5,8,0)}.bg-dark-900\/10{background-color:rgba(5,5,8,.1)}.bg-dark-900\/100{background-color:#050508}.bg-dark-900\/15{background-color:rgba(5,5,8,.15)}.bg-dark-900\/20{background-color:rgba(5,5,8,.2)}.bg-dark-900\/25{background-color:rgba(5,5,8,.25)}.bg-dark-900\/30{background-color:rgba(5,5,8,.3)}.bg-dark-900\/35{background-color:rgba(5,5,8,.35)}.bg-dark-900\/40{background-color:rgba(5,5,8,.4)}.bg-dark-900\/45{background-color:rgba(5,5,8,.45)}.bg-dark-900\/5{background-color:rgba(5,5,8,.05)}.bg-dark-900\/50{background-color:rgba(5,5,8,.5)}.bg-dark-900\/55{background-color:rgba(5,5,8,.55)}.bg-dark-900\/60{background-color:rgba(5,5,8,.6)}.bg-dark-900\/65{background-color:rgba(5,5,8,.65)}.bg-dark-900\/70{background-color:rgba(5,5,8,.7)}.bg-dark-900\/75{background-color:rgba(5,5,8,.75)}.bg-dark-900\/80{background-color:rgba(5,5,8,.8)}.bg-dark-900\/85{background-color:rgba(5,5,8,.85)}.bg-dark-900\/90{background-color:rgba(5,5,8,.9)}.bg-dark-900\/95{background-color:rgba(5,5,8,.95)}.bg-emerald-400{--tw-bg-opacity:1;background-color:rgb(52 211 153/var(--tw-bg-opacity,1))}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-emerald-500\/10{background-color:rgba(16,185,129,.1)}.bg-emerald-500\/5{background-color:rgba(16,185,129,.05)}.bg-emerald-700{--tw-bg-opacity:1;background-color:rgb(4 120 87/var(--tw-bg-opacity,1))}.bg-fb-bg{--tw-bg-opacity:1;background-color:rgb(15 23 42/var(--tw-bg-opacity,1))}.bg-fb-bg\/40{background-color:rgba(15,23,42,.4)}.bg-fb-bg\/50{background-color:rgba(15,23,42,.5)}.bg-fb-bg\/60{background-color:rgba(15,23,42,.6)}.bg-fb-bg\/95{background-color:rgba(15,23,42,.95)}.bg-fb-border{--tw-bg-opacity:1;background-color:rgb(51 65 85/var(--tw-bg-opacity,1))}.bg-fb-card{--tw-bg-opacity:1;background-color:rgb(30 41 59/var(--tw-bg-opacity,1))}.bg-fb-card\/50{background-color:rgba(30,41,59,.5)}.bg-fb-card\/60{background-color:rgba(30,41,59,.6)}.bg-fb-card\/80{background-color:rgba(30,41,59,.8)}.bg-fb-cardMuted{--tw-bg-opacity:1;background-color:rgb(11 18 32/var(--tw-bg-opacity,1))}.bg-fb-good{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-fb-good\/30{background-color:rgba(34,197,94,.3)}.bg-fb-low{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-fb-low\/15{background-color:rgba(239,68,68,.15)}.bg-fb-low\/30{background-color:rgba(239,68,68,.3)}.bg-fb-mid{--tw-bg-opacity:1;background-color:rgb(234 179 8/var(--tw-bg-opacity,1))}.bg-fb-primary{--tw-bg-opacity:1;background-color:rgb(14 165 233/var(--tw-bg-opacity,1))}.bg-fb-primary\/15{background-color:rgba(14,165,233,.15)}.bg-fb-primary\/20{background-color:rgba(14,165,233,.2)}.bg-fb-primary\/5{background-color:rgba(14,165,233,.05)}.bg-fb-sidebar{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.bg-fb-sidebar\/80{background-color:rgba(17,24,39,.8)}.bg-fb-sidebar\/95{background-color:rgba(17,24,39,.95)}.bg-gold{--tw-bg-opacity:1;background-color:rgb(232 192 64/var(--tw-bg-opacity,1))}.bg-gold\/10{background-color:rgba(232,192,64,.1)}.bg-gold\/5{background-color:rgba(232,192,64,.05)}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/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-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity,1))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity,1))}.bg-gray-500\/40{background-color:hsla(220,9%,46%,.4)}.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-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.bg-gray-800\/50{background-color:rgba(31,41,55,.5)}.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{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(220 252 231/var(--tw-bg-opacity,1))}.bg-green-200{--tw-bg-opacity:1;background-color:rgb(187 247 208/var(--tw-bg-opacity,1))}.bg-green-300{--tw-bg-opacity:1;background-color:rgb(134 239 172/var(--tw-bg-opacity,1))}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(74 222 128/var(--tw-bg-opacity,1))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity,1))}.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-green-700{--tw-bg-opacity:1;background-color:rgb(21 128 61/var(--tw-bg-opacity,1))}.bg-green-800{--tw-bg-opacity:1;background-color:rgb(22 101 52/var(--tw-bg-opacity,1))}.bg-green-900{--tw-bg-opacity:1;background-color:rgb(20 83 45/var(--tw-bg-opacity,1))}.bg-green-900\/30{background-color:rgba(20,83,45,.3)}.bg-green-900\/40{background-color:rgba(20,83,45,.4)}.bg-green-900\/50{background-color:rgba(20,83,45,.5)}.bg-green-900\/60{background-color:rgba(20,83,45,.6)}.bg-green-900\/80{background-color:rgba(20,83,45,.8)}.bg-indigo-100{--tw-bg-opacity:1;background-color:rgb(224 231 255/var(--tw-bg-opacity,1))}.bg-indigo-200{--tw-bg-opacity:1;background-color:rgb(199 210 254/var(--tw-bg-opacity,1))}.bg-indigo-300{--tw-bg-opacity:1;background-color:rgb(165 180 252/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-50{--tw-bg-opacity:1;background-color:rgb(238 242 255/var(--tw-bg-opacity,1))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity,1))}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity,1))}.bg-indigo-700{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity,1))}.bg-indigo-800{--tw-bg-opacity:1;background-color:rgb(55 48 163/var(--tw-bg-opacity,1))}.bg-indigo-900{--tw-bg-opacity:1;background-color:rgb(49 46 129/var(--tw-bg-opacity,1))}.bg-orange-400{--tw-bg-opacity:1;background-color:rgb(251 146 60/var(--tw-bg-opacity,1))}.bg-orange-500{--tw-bg-opacity:1;background-color:rgb(249 115 22/var(--tw-bg-opacity,1))}.bg-orange-600{--tw-bg-opacity:1;background-color:rgb(234 88 12/var(--tw-bg-opacity,1))}.bg-pink-100{--tw-bg-opacity:1;background-color:rgb(252 231 243/var(--tw-bg-opacity,1))}.bg-pink-200{--tw-bg-opacity:1;background-color:rgb(251 207 232/var(--tw-bg-opacity,1))}.bg-pink-300{--tw-bg-opacity:1;background-color:rgb(249 168 212/var(--tw-bg-opacity,1))}.bg-pink-400{--tw-bg-opacity:1;background-color:rgb(244 114 182/var(--tw-bg-opacity,1))}.bg-pink-50{--tw-bg-opacity:1;background-color:rgb(253 242 248/var(--tw-bg-opacity,1))}.bg-pink-500{--tw-bg-opacity:1;background-color:rgb(236 72 153/var(--tw-bg-opacity,1))}.bg-pink-600{--tw-bg-opacity:1;background-color:rgb(219 39 119/var(--tw-bg-opacity,1))}.bg-pink-700{--tw-bg-opacity:1;background-color:rgb(190 24 93/var(--tw-bg-opacity,1))}.bg-pink-800{--tw-bg-opacity:1;background-color:rgb(157 23 77/var(--tw-bg-opacity,1))}.bg-pink-900{--tw-bg-opacity:1;background-color:rgb(131 24 67/var(--tw-bg-opacity,1))}.bg-purple-100{--tw-bg-opacity:1;background-color:rgb(243 232 255/var(--tw-bg-opacity,1))}.bg-purple-200{--tw-bg-opacity:1;background-color:rgb(233 213 255/var(--tw-bg-opacity,1))}.bg-purple-300{--tw-bg-opacity:1;background-color:rgb(216 180 254/var(--tw-bg-opacity,1))}.bg-purple-400{--tw-bg-opacity:1;background-color:rgb(192 132 252/var(--tw-bg-opacity,1))}.bg-purple-50{--tw-bg-opacity:1;background-color:rgb(250 245 255/var(--tw-bg-opacity,1))}.bg-purple-500{--tw-bg-opacity:1;background-color:rgb(168 85 247/var(--tw-bg-opacity,1))}.bg-purple-500\/10{background-color:rgba(168,85,247,.1)}.bg-purple-500\/20{background-color:rgba(168,85,247,.2)}.bg-purple-500\/40{background-color:rgba(168,85,247,.4)}.bg-purple-600{--tw-bg-opacity:1;background-color:rgb(147 51 234/var(--tw-bg-opacity,1))}.bg-purple-700{--tw-bg-opacity:1;background-color:rgb(126 34 206/var(--tw-bg-opacity,1))}.bg-purple-800{--tw-bg-opacity:1;background-color:rgb(107 33 168/var(--tw-bg-opacity,1))}.bg-purple-900{--tw-bg-opacity:1;background-color:rgb(88 28 135/var(--tw-bg-opacity,1))}.bg-purple-900\/30{background-color:rgba(88,28,135,.3)}.bg-purple-900\/40{background-color:rgba(88,28,135,.4)}.bg-purple-900\/60{background-color:rgba(88,28,135,.6)}.bg-purple-900\/80{background-color:rgba(88,28,135,.8)}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity,1))}.bg-red-200{--tw-bg-opacity:1;background-color:rgb(254 202 202/var(--tw-bg-opacity,1))}.bg-red-300{--tw-bg-opacity:1;background-color:rgb(252 165 165/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-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/10{background-color:rgba(239,68,68,.1)}.bg-red-500\/5{background-color:rgba(239,68,68,.05)}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity,1))}.bg-red-600\/80{background-color:rgba(220,38,38,.8)}.bg-red-700{--tw-bg-opacity:1;background-color:rgb(185 28 28/var(--tw-bg-opacity,1))}.bg-red-800{--tw-bg-opacity:1;background-color:rgb(153 27 27/var(--tw-bg-opacity,1))}.bg-red-900{--tw-bg-opacity:1;background-color:rgb(127 29 29/var(--tw-bg-opacity,1))}.bg-red-900\/10{background-color:rgba(127,29,29,.1)}.bg-red-900\/30{background-color:rgba(127,29,29,.3)}.bg-red-900\/40{background-color:rgba(127,29,29,.4)}.bg-red-900\/95{background-color:rgba(127,29,29,.95)}.bg-slate-100{--tw-bg-opacity:1;background-color:rgb(241 245 249/var(--tw-bg-opacity,1))}.bg-slate-200{--tw-bg-opacity:1;background-color:rgb(226 232 240/var(--tw-bg-opacity,1))}.bg-slate-300{--tw-bg-opacity:1;background-color:rgb(203 213 225/var(--tw-bg-opacity,1))}.bg-slate-400{--tw-bg-opacity:1;background-color:rgb(148 163 184/var(--tw-bg-opacity,1))}.bg-slate-50{--tw-bg-opacity:1;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.bg-slate-500{--tw-bg-opacity:1;background-color:rgb(100 116 139/var(--tw-bg-opacity,1))}.bg-slate-600{--tw-bg-opacity:1;background-color:rgb(71 85 105/var(--tw-bg-opacity,1))}.bg-slate-700{--tw-bg-opacity:1;background-color:rgb(51 65 85/var(--tw-bg-opacity,1))}.bg-slate-800{--tw-bg-opacity:1;background-color:rgb(30 41 59/var(--tw-bg-opacity,1))}.bg-slate-900{--tw-bg-opacity:1;background-color:rgb(15 23 42/var(--tw-bg-opacity,1))}.bg-teal-500\/10{background-color:rgba(20,184,166,.1)}.bg-transparent{background-color:transparent}.bg-violet-200{--tw-bg-opacity:1;background-color:rgb(221 214 254/var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity,1))}.bg-yellow-200{--tw-bg-opacity:1;background-color:rgb(254 240 138/var(--tw-bg-opacity,1))}.bg-yellow-300{--tw-bg-opacity:1;background-color:rgb(253 224 71/var(--tw-bg-opacity,1))}.bg-yellow-400{--tw-bg-opacity:1;background-color:rgb(250 204 21/var(--tw-bg-opacity,1))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity,1))}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgb(234 179 8/var(--tw-bg-opacity,1))}.bg-yellow-500\/10{background-color:rgba(234,179,8,.1)}.bg-yellow-600{--tw-bg-opacity:1;background-color:rgb(202 138 4/var(--tw-bg-opacity,1))}.bg-yellow-700{--tw-bg-opacity:1;background-color:rgb(161 98 7/var(--tw-bg-opacity,1))}.bg-yellow-800{--tw-bg-opacity:1;background-color:rgb(133 77 14/var(--tw-bg-opacity,1))}.bg-yellow-900{--tw-bg-opacity:1;background-color:rgb(113 63 18/var(--tw-bg-opacity,1))}.bg-yellow-900\/30{background-color:rgba(113,63,18,.3)}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-accent-light{--tw-gradient-from:#60a0ff var(--tw-gradient-from-position);--tw-gradient-to:rgba(96,160,255,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.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-black\/80{--tw-gradient-from:rgba(0,0,0,.8) 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-black\/90{--tw-gradient-from:rgba(0,0,0,.9) 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)}.via-black\/40{--tw-gradient-to:transparent var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(0,0,0,.4) var(--tw-gradient-via-position),var(--tw-gradient-to)}.to-purple-400{--tw-gradient-to:#c084fc var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.object-cover{-o-object-fit:cover;object-fit:cover}.object-center{-o-object-position:center;object-position:center}.object-right{-o-object-position:right;object-position:right}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-10{padding:2.5rem}.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-0\.5{padding-left:.125rem;padding-right:.125rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.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}.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-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-24{padding-top:6rem;padding-bottom:6rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-px{padding-top:1px;padding-bottom:1px}.pb-1{padding-bottom:.25rem}.pb-10{padding-bottom:2.5rem}.pb-16{padding-bottom:4rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pl-0{padding-left:0}.pl-10{padding-left:2.5rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pl-8{padding-left:2rem}.pr-0{padding-right:0}.pr-1{padding-right:.25rem}.pr-3{padding-right:.75rem}.pr-4{padding-right:1rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pt-24{padding-top:6rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.align-middle{vertical-align:middle}.font-display{font-family:Inter,system-ui,sans-serif}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.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-6xl{font-size:3.75rem;line-height:1}.text-9xl{font-size:8rem;line-height:1}.text-\[0\.625rem\]{font-size:.625rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[9px\]{font-size:9px}.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-black{font-weight:900}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.normal-case{text-transform:none}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-5{line-height:1.25rem}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-tight{letter-spacing:-.025em}.tracking-tighter{letter-spacing:-.05em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.\!text-white{--tw-text-opacity:1!important;color:rgb(255 255 255/var(--tw-text-opacity,1))!important}.text-\[\#FF4B4B\]{--tw-text-opacity:1;color:rgb(255 75 75/var(--tw-text-opacity,1))}.text-accent{--tw-text-opacity:1;color:rgb(64 128 224/var(--tw-text-opacity,1))}.text-accent-dark{--tw-text-opacity:1;color:rgb(32 96 176/var(--tw-text-opacity,1))}.text-accent-dark\/0{color:rgba(32,96,176,0)}.text-accent-dark\/10{color:rgba(32,96,176,.1)}.text-accent-dark\/100{color:#2060b0}.text-accent-dark\/15{color:rgba(32,96,176,.15)}.text-accent-dark\/20{color:rgba(32,96,176,.2)}.text-accent-dark\/25{color:rgba(32,96,176,.25)}.text-accent-dark\/30{color:rgba(32,96,176,.3)}.text-accent-dark\/35{color:rgba(32,96,176,.35)}.text-accent-dark\/40{color:rgba(32,96,176,.4)}.text-accent-dark\/45{color:rgba(32,96,176,.45)}.text-accent-dark\/5{color:rgba(32,96,176,.05)}.text-accent-dark\/50{color:rgba(32,96,176,.5)}.text-accent-dark\/55{color:rgba(32,96,176,.55)}.text-accent-dark\/60{color:rgba(32,96,176,.6)}.text-accent-dark\/65{color:rgba(32,96,176,.65)}.text-accent-dark\/70{color:rgba(32,96,176,.7)}.text-accent-dark\/75{color:rgba(32,96,176,.75)}.text-accent-dark\/80{color:rgba(32,96,176,.8)}.text-accent-dark\/85{color:rgba(32,96,176,.85)}.text-accent-dark\/90{color:rgba(32,96,176,.9)}.text-accent-dark\/95{color:rgba(32,96,176,.95)}.text-accent-light{--tw-text-opacity:1;color:rgb(96 160 255/var(--tw-text-opacity,1))}.text-accent-light\/0{color:rgba(96,160,255,0)}.text-accent-light\/10{color:rgba(96,160,255,.1)}.text-accent-light\/100{color:#60a0ff}.text-accent-light\/15{color:rgba(96,160,255,.15)}.text-accent-light\/20{color:rgba(96,160,255,.2)}.text-accent-light\/25{color:rgba(96,160,255,.25)}.text-accent-light\/30{color:rgba(96,160,255,.3)}.text-accent-light\/35{color:rgba(96,160,255,.35)}.text-accent-light\/40{color:rgba(96,160,255,.4)}.text-accent-light\/45{color:rgba(96,160,255,.45)}.text-accent-light\/5{color:rgba(96,160,255,.05)}.text-accent-light\/50{color:rgba(96,160,255,.5)}.text-accent-light\/55{color:rgba(96,160,255,.55)}.text-accent-light\/60{color:rgba(96,160,255,.6)}.text-accent-light\/65{color:rgba(96,160,255,.65)}.text-accent-light\/70{color:rgba(96,160,255,.7)}.text-accent-light\/75{color:rgba(96,160,255,.75)}.text-accent-light\/80{color:rgba(96,160,255,.8)}.text-accent-light\/85{color:rgba(96,160,255,.85)}.text-accent-light\/90{color:rgba(96,160,255,.9)}.text-accent-light\/95{color:rgba(96,160,255,.95)}.text-accent\/70{color:rgba(64,128,224,.7)}.text-amber-100{--tw-text-opacity:1;color:rgb(254 243 199/var(--tw-text-opacity,1))}.text-amber-200{--tw-text-opacity:1;color:rgb(253 230 138/var(--tw-text-opacity,1))}.text-amber-300{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.text-amber-400\/90{color:rgba(251,191,36,.9)}.text-amber-50{--tw-text-opacity:1;color:rgb(255 251 235/var(--tw-text-opacity,1))}.text-amber-500{--tw-text-opacity:1;color:rgb(245 158 11/var(--tw-text-opacity,1))}.text-amber-600{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.text-amber-700{--tw-text-opacity:1;color:rgb(180 83 9/var(--tw-text-opacity,1))}.text-amber-800{--tw-text-opacity:1;color:rgb(146 64 14/var(--tw-text-opacity,1))}.text-amber-900{--tw-text-opacity:1;color:rgb(120 53 15/var(--tw-text-opacity,1))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.text-blue-100{--tw-text-opacity:1;color:rgb(219 234 254/var(--tw-text-opacity,1))}.text-blue-200{--tw-text-opacity:1;color:rgb(191 219 254/var(--tw-text-opacity,1))}.text-blue-300{--tw-text-opacity:1;color:rgb(147 197 253/var(--tw-text-opacity,1))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.text-blue-400\/70{color:rgba(96,165,250,.7)}.text-blue-50{--tw-text-opacity:1;color:rgb(239 246 255/var(--tw-text-opacity,1))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity,1))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity,1))}.text-blue-800{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity,1))}.text-blue-900{--tw-text-opacity:1;color:rgb(30 58 138/var(--tw-text-opacity,1))}.text-cyan-300{--tw-text-opacity:1;color:rgb(103 232 249/var(--tw-text-opacity,1))}.text-cyan-300\/90{color:rgba(103,232,249,.9)}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.text-dark-500{--tw-text-opacity:1;color:rgb(30 30 58/var(--tw-text-opacity,1))}.text-dark-500\/0{color:rgba(30,30,58,0)}.text-dark-500\/10{color:rgba(30,30,58,.1)}.text-dark-500\/100{color:#1e1e3a}.text-dark-500\/15{color:rgba(30,30,58,.15)}.text-dark-500\/20{color:rgba(30,30,58,.2)}.text-dark-500\/25{color:rgba(30,30,58,.25)}.text-dark-500\/30{color:rgba(30,30,58,.3)}.text-dark-500\/35{color:rgba(30,30,58,.35)}.text-dark-500\/40{color:rgba(30,30,58,.4)}.text-dark-500\/45{color:rgba(30,30,58,.45)}.text-dark-500\/5{color:rgba(30,30,58,.05)}.text-dark-500\/50{color:rgba(30,30,58,.5)}.text-dark-500\/55{color:rgba(30,30,58,.55)}.text-dark-500\/60{color:rgba(30,30,58,.6)}.text-dark-500\/65{color:rgba(30,30,58,.65)}.text-dark-500\/70{color:rgba(30,30,58,.7)}.text-dark-500\/75{color:rgba(30,30,58,.75)}.text-dark-500\/80{color:rgba(30,30,58,.8)}.text-dark-500\/85{color:rgba(30,30,58,.85)}.text-dark-500\/90{color:rgba(30,30,58,.9)}.text-dark-500\/95{color:rgba(30,30,58,.95)}.text-dark-600{--tw-text-opacity:1;color:rgb(24 24 48/var(--tw-text-opacity,1))}.text-dark-600\/0{color:rgba(24,24,48,0)}.text-dark-600\/10{color:rgba(24,24,48,.1)}.text-dark-600\/100{color:#181830}.text-dark-600\/15{color:rgba(24,24,48,.15)}.text-dark-600\/20{color:rgba(24,24,48,.2)}.text-dark-600\/25{color:rgba(24,24,48,.25)}.text-dark-600\/30{color:rgba(24,24,48,.3)}.text-dark-600\/35{color:rgba(24,24,48,.35)}.text-dark-600\/40{color:rgba(24,24,48,.4)}.text-dark-600\/45{color:rgba(24,24,48,.45)}.text-dark-600\/5{color:rgba(24,24,48,.05)}.text-dark-600\/50{color:rgba(24,24,48,.5)}.text-dark-600\/55{color:rgba(24,24,48,.55)}.text-dark-600\/60{color:rgba(24,24,48,.6)}.text-dark-600\/65{color:rgba(24,24,48,.65)}.text-dark-600\/70{color:rgba(24,24,48,.7)}.text-dark-600\/75{color:rgba(24,24,48,.75)}.text-dark-600\/80{color:rgba(24,24,48,.8)}.text-dark-600\/85{color:rgba(24,24,48,.85)}.text-dark-600\/90{color:rgba(24,24,48,.9)}.text-dark-600\/95{color:rgba(24,24,48,.95)}.text-dark-700{--tw-text-opacity:1;color:rgb(16 16 30/var(--tw-text-opacity,1))}.text-dark-700\/0{color:rgba(16,16,30,0)}.text-dark-700\/10{color:rgba(16,16,30,.1)}.text-dark-700\/100{color:#10101e}.text-dark-700\/15{color:rgba(16,16,30,.15)}.text-dark-700\/20{color:rgba(16,16,30,.2)}.text-dark-700\/25{color:rgba(16,16,30,.25)}.text-dark-700\/30{color:rgba(16,16,30,.3)}.text-dark-700\/35{color:rgba(16,16,30,.35)}.text-dark-700\/40{color:rgba(16,16,30,.4)}.text-dark-700\/45{color:rgba(16,16,30,.45)}.text-dark-700\/5{color:rgba(16,16,30,.05)}.text-dark-700\/50{color:rgba(16,16,30,.5)}.text-dark-700\/55{color:rgba(16,16,30,.55)}.text-dark-700\/60{color:rgba(16,16,30,.6)}.text-dark-700\/65{color:rgba(16,16,30,.65)}.text-dark-700\/70{color:rgba(16,16,30,.7)}.text-dark-700\/75{color:rgba(16,16,30,.75)}.text-dark-700\/80{color:rgba(16,16,30,.8)}.text-dark-700\/85{color:rgba(16,16,30,.85)}.text-dark-700\/90{color:rgba(16,16,30,.9)}.text-dark-700\/95{color:rgba(16,16,30,.95)}.text-dark-800{--tw-text-opacity:1;color:rgb(10 10 18/var(--tw-text-opacity,1))}.text-dark-800\/0{color:rgba(10,10,18,0)}.text-dark-800\/10{color:rgba(10,10,18,.1)}.text-dark-800\/100{color:#0a0a12}.text-dark-800\/15{color:rgba(10,10,18,.15)}.text-dark-800\/20{color:rgba(10,10,18,.2)}.text-dark-800\/25{color:rgba(10,10,18,.25)}.text-dark-800\/30{color:rgba(10,10,18,.3)}.text-dark-800\/35{color:rgba(10,10,18,.35)}.text-dark-800\/40{color:rgba(10,10,18,.4)}.text-dark-800\/45{color:rgba(10,10,18,.45)}.text-dark-800\/5{color:rgba(10,10,18,.05)}.text-dark-800\/50{color:rgba(10,10,18,.5)}.text-dark-800\/55{color:rgba(10,10,18,.55)}.text-dark-800\/60{color:rgba(10,10,18,.6)}.text-dark-800\/65{color:rgba(10,10,18,.65)}.text-dark-800\/70{color:rgba(10,10,18,.7)}.text-dark-800\/75{color:rgba(10,10,18,.75)}.text-dark-800\/80{color:rgba(10,10,18,.8)}.text-dark-800\/85{color:rgba(10,10,18,.85)}.text-dark-800\/90{color:rgba(10,10,18,.9)}.text-dark-800\/95{color:rgba(10,10,18,.95)}.text-dark-900{--tw-text-opacity:1;color:rgb(5 5 8/var(--tw-text-opacity,1))}.text-dark-900\/0{color:rgba(5,5,8,0)}.text-dark-900\/10{color:rgba(5,5,8,.1)}.text-dark-900\/100{color:#050508}.text-dark-900\/15{color:rgba(5,5,8,.15)}.text-dark-900\/20{color:rgba(5,5,8,.2)}.text-dark-900\/25{color:rgba(5,5,8,.25)}.text-dark-900\/30{color:rgba(5,5,8,.3)}.text-dark-900\/35{color:rgba(5,5,8,.35)}.text-dark-900\/40{color:rgba(5,5,8,.4)}.text-dark-900\/45{color:rgba(5,5,8,.45)}.text-dark-900\/5{color:rgba(5,5,8,.05)}.text-dark-900\/50{color:rgba(5,5,8,.5)}.text-dark-900\/55{color:rgba(5,5,8,.55)}.text-dark-900\/60{color:rgba(5,5,8,.6)}.text-dark-900\/65{color:rgba(5,5,8,.65)}.text-dark-900\/70{color:rgba(5,5,8,.7)}.text-dark-900\/75{color:rgba(5,5,8,.75)}.text-dark-900\/80{color:rgba(5,5,8,.8)}.text-dark-900\/85{color:rgba(5,5,8,.85)}.text-dark-900\/90{color:rgba(5,5,8,.9)}.text-dark-900\/95{color:rgba(5,5,8,.95)}.text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.text-emerald-400{--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.text-fb-accent{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-fb-gold{--tw-text-opacity:1;color:rgb(232 192 64/var(--tw-text-opacity,1))}.text-fb-good{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity,1))}.text-fb-low{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-fb-mid{--tw-text-opacity:1;color:rgb(234 179 8/var(--tw-text-opacity,1))}.text-fb-primary{--tw-text-opacity:1;color:rgb(14 165 233/var(--tw-text-opacity,1))}.text-fb-primary\/90{color:rgba(14,165,233,.9)}.text-fb-text{--tw-text-opacity:1;color:rgb(248 250 252/var(--tw-text-opacity,1))}.text-fb-text\/80{color:rgba(248,250,252,.8)}.text-fb-textDim{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity,1))}.text-fb-textDim\/40{color:rgba(148,163,184,.4)}.text-fb-textDim\/60{color:rgba(148,163,184,.6)}.text-fb-textDim\/70{color:rgba(148,163,184,.7)}.text-gold{--tw-text-opacity:1;color:rgb(232 192 64/var(--tw-text-opacity,1))}.text-gray-100{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity,1))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-50{--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/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-green-100{--tw-text-opacity:1;color:rgb(220 252 231/var(--tw-text-opacity,1))}.text-green-200{--tw-text-opacity:1;color:rgb(187 247 208/var(--tw-text-opacity,1))}.text-green-300{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.text-green-50{--tw-text-opacity:1;color:rgb(240 253 244/var(--tw-text-opacity,1))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity,1))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity,1))}.text-green-700{--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity,1))}.text-green-800{--tw-text-opacity:1;color:rgb(22 101 52/var(--tw-text-opacity,1))}.text-green-900{--tw-text-opacity:1;color:rgb(20 83 45/var(--tw-text-opacity,1))}.text-indigo-100{--tw-text-opacity:1;color:rgb(224 231 255/var(--tw-text-opacity,1))}.text-indigo-200{--tw-text-opacity:1;color:rgb(199 210 254/var(--tw-text-opacity,1))}.text-indigo-300{--tw-text-opacity:1;color:rgb(165 180 252/var(--tw-text-opacity,1))}.text-indigo-400{--tw-text-opacity:1;color:rgb(129 140 248/var(--tw-text-opacity,1))}.text-indigo-50{--tw-text-opacity:1;color:rgb(238 242 255/var(--tw-text-opacity,1))}.text-indigo-500{--tw-text-opacity:1;color:rgb(99 102 241/var(--tw-text-opacity,1))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity,1))}.text-indigo-700{--tw-text-opacity:1;color:rgb(67 56 202/var(--tw-text-opacity,1))}.text-indigo-800{--tw-text-opacity:1;color:rgb(55 48 163/var(--tw-text-opacity,1))}.text-indigo-900{--tw-text-opacity:1;color:rgb(49 46 129/var(--tw-text-opacity,1))}.text-pink-100{--tw-text-opacity:1;color:rgb(252 231 243/var(--tw-text-opacity,1))}.text-pink-200{--tw-text-opacity:1;color:rgb(251 207 232/var(--tw-text-opacity,1))}.text-pink-300{--tw-text-opacity:1;color:rgb(249 168 212/var(--tw-text-opacity,1))}.text-pink-400{--tw-text-opacity:1;color:rgb(244 114 182/var(--tw-text-opacity,1))}.text-pink-50{--tw-text-opacity:1;color:rgb(253 242 248/var(--tw-text-opacity,1))}.text-pink-500{--tw-text-opacity:1;color:rgb(236 72 153/var(--tw-text-opacity,1))}.text-pink-600{--tw-text-opacity:1;color:rgb(219 39 119/var(--tw-text-opacity,1))}.text-pink-700{--tw-text-opacity:1;color:rgb(190 24 93/var(--tw-text-opacity,1))}.text-pink-800{--tw-text-opacity:1;color:rgb(157 23 77/var(--tw-text-opacity,1))}.text-pink-900{--tw-text-opacity:1;color:rgb(131 24 67/var(--tw-text-opacity,1))}.text-purple-100{--tw-text-opacity:1;color:rgb(243 232 255/var(--tw-text-opacity,1))}.text-purple-200{--tw-text-opacity:1;color:rgb(233 213 255/var(--tw-text-opacity,1))}.text-purple-300{--tw-text-opacity:1;color:rgb(216 180 254/var(--tw-text-opacity,1))}.text-purple-400{--tw-text-opacity:1;color:rgb(192 132 252/var(--tw-text-opacity,1))}.text-purple-50{--tw-text-opacity:1;color:rgb(250 245 255/var(--tw-text-opacity,1))}.text-purple-500{--tw-text-opacity:1;color:rgb(168 85 247/var(--tw-text-opacity,1))}.text-purple-600{--tw-text-opacity:1;color:rgb(147 51 234/var(--tw-text-opacity,1))}.text-purple-700{--tw-text-opacity:1;color:rgb(126 34 206/var(--tw-text-opacity,1))}.text-purple-800{--tw-text-opacity:1;color:rgb(107 33 168/var(--tw-text-opacity,1))}.text-purple-900{--tw-text-opacity:1;color:rgb(88 28 135/var(--tw-text-opacity,1))}.text-red-100{--tw-text-opacity:1;color:rgb(254 226 226/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-400\/90{color:hsla(0,91%,71%,.9)}.text-red-50{--tw-text-opacity:1;color:rgb(254 242 242/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.text-red-900{--tw-text-opacity:1;color:rgb(127 29 29/var(--tw-text-opacity,1))}.text-sky-500{--tw-text-opacity:1;color:rgb(14 165 233/var(--tw-text-opacity,1))}.text-slate-100{--tw-text-opacity:1;color:rgb(241 245 249/var(--tw-text-opacity,1))}.text-slate-200{--tw-text-opacity:1;color:rgb(226 232 240/var(--tw-text-opacity,1))}.text-slate-300{--tw-text-opacity:1;color:rgb(203 213 225/var(--tw-text-opacity,1))}.text-slate-400{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity,1))}.text-slate-50{--tw-text-opacity:1;color:rgb(248 250 252/var(--tw-text-opacity,1))}.text-slate-500{--tw-text-opacity:1;color:rgb(100 116 139/var(--tw-text-opacity,1))}.text-slate-600{--tw-text-opacity:1;color:rgb(71 85 105/var(--tw-text-opacity,1))}.text-slate-700{--tw-text-opacity:1;color:rgb(51 65 85/var(--tw-text-opacity,1))}.text-slate-800{--tw-text-opacity:1;color:rgb(30 41 59/var(--tw-text-opacity,1))}.text-slate-900{--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity,1))}.text-teal-300{--tw-text-opacity:1;color:rgb(94 234 212/var(--tw-text-opacity,1))}.text-transparent{color:transparent}.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-yellow-100{--tw-text-opacity:1;color:rgb(254 249 195/var(--tw-text-opacity,1))}.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-300\/80{color:rgba(253,224,71,.8)}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.text-yellow-50{--tw-text-opacity:1;color:rgb(254 252 232/var(--tw-text-opacity,1))}.text-yellow-500{--tw-text-opacity:1;color:rgb(234 179 8/var(--tw-text-opacity,1))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity,1))}.text-yellow-700{--tw-text-opacity:1;color:rgb(161 98 7/var(--tw-text-opacity,1))}.text-yellow-800{--tw-text-opacity:1;color:rgb(133 77 14/var(--tw-text-opacity,1))}.text-yellow-900{--tw-text-opacity:1;color:rgb(113 63 18/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.overline{text-decoration-line:overline}.line-through{text-decoration-line:line-through}.placeholder-fb-textDim::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(148 163 184/var(--tw-placeholder-opacity,1))}.placeholder-fb-textDim::placeholder{--tw-placeholder-opacity:1;color:rgb(148 163 184/var(--tw-placeholder-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))}.placeholder-gray-600::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(75 85 99/var(--tw-placeholder-opacity,1))}.placeholder-gray-600::placeholder{--tw-placeholder-opacity:1;color:rgb(75 85 99/var(--tw-placeholder-opacity,1))}.accent-accent{accent-color:#4080e0}.accent-fb-primary{accent-color:#0ea5e9}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-20{opacity:.2}.opacity-40{opacity:.4}.opacity-5{opacity:.05}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-2xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.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-\[0_0_10px_3px_rgba\(52\2c 211\2c 153\2c 0\.95\)\]{--tw-shadow:0 0 10px 3px rgba(52,211,153,.95);--tw-shadow-colored:0 0 10px 3px 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-\[0_0_12px_rgba\(192\2c 132\2c 252\2c 0\.55\)\]{--tw-shadow:0 0 12px rgba(192,132,252,.55);--tw-shadow-colored:0 0 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-\[0_0_12px_rgba\(192\2c 132\2c 252\2c 0\.65\)\]{--tw-shadow:0 0 12px rgba(192,132,252,.65);--tw-shadow-colored:0 0 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-\[0_0_12px_rgba\(216\2c 180\2c 254\2c 0\.65\)\]{--tw-shadow:0 0 12px rgba(216,180,254,.65);--tw-shadow-colored:0 0 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-\[0_0_12px_rgba\(221\2c 214\2c 254\2c 0\.6\)\]{--tw-shadow:0 0 12px rgba(221,214,254,.6);--tw-shadow-colored:0 0 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-\[0_0_12px_rgba\(251\2c 146\2c 60\2c 0\.55\)\]{--tw-shadow:0 0 12px rgba(251,146,60,.55);--tw-shadow-colored:0 0 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-\[0_0_12px_rgba\(251\2c 146\2c 60\2c 0\.65\)\]{--tw-shadow:0 0 12px rgba(251,146,60,.65);--tw-shadow-colored:0 0 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-\[0_0_12px_rgba\(96\2c 165\2c 250\2c 0\.55\)\]{--tw-shadow:0 0 12px rgba(96,165,250,.55);--tw-shadow-colored:0 0 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-\[0_0_12px_rgba\(96\2c 165\2c 250\2c 0\.65\)\]{--tw-shadow:0 0 12px rgba(96,165,250,.65);--tw-shadow-colored:0 0 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-\[0_0_4px_1px_rgba\(185\2c 28\2c 28\2c 0\.7\)\]{--tw-shadow:0 0 4px 1px rgba(185,28,28,.7);--tw-shadow-colored:0 0 4px 1px 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-\[0_0_6px_2px_rgba\(239\2c 68\2c 68\2c 0\.8\)\]{--tw-shadow:0 0 6px 2px rgba(239,68,68,.8);--tw-shadow-colored:0 0 6px 2px 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-\[0_0_6px_2px_rgba\(249\2c 115\2c 22\2c 0\.85\)\]{--tw-shadow:0 0 6px 2px rgba(249,115,22,.85);--tw-shadow-colored:0 0 6px 2px 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-\[0_0_8px_2px_rgba\(234\2c 179\2c 8\2c 0\.9\)\]{--tw-shadow:0 0 8px 2px rgba(234,179,8,.9);--tw-shadow-colored:0 0 8px 2px 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-\[0_0_8px_2px_rgba\(52\2c 211\2c 153\2c 0\.85\)\]{--tw-shadow:0 0 8px 2px rgba(52,211,153,.85);--tw-shadow-colored:0 0 8px 2px 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-\[0_0_8px_rgba\(255\2c 255\2c 255\2c 0\.5\)\]{--tw-shadow:0 0 8px hsla(0,0%,100%,.5);--tw-shadow-colored:0 0 8px 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-\[0_0_8px_rgba\(74\2c 222\2c 128\2c 0\.5\)\]{--tw-shadow:0 0 8px rgba(74,222,128,.5);--tw-shadow-colored:0 0 8px var(--tw-shadow-color)}.shadow-\[0_0_8px_rgba\(74\2c 222\2c 128\2c 0\.5\)\],.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-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);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-fb-primary\/20{--tw-shadow-color:rgba(14,165,233,.2);--tw-shadow:var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.\!ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)!important;--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)!important;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)!important}.ring{--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(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-1{--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)}.ring-2{--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(2px + 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)}.ring-amber-100{--tw-ring-opacity:1;--tw-ring-color:rgb(254 243 199/var(--tw-ring-opacity,1))}.ring-amber-200{--tw-ring-opacity:1;--tw-ring-color:rgb(253 230 138/var(--tw-ring-opacity,1))}.ring-amber-300{--tw-ring-opacity:1;--tw-ring-color:rgb(252 211 77/var(--tw-ring-opacity,1))}.ring-amber-400{--tw-ring-opacity:1;--tw-ring-color:rgb(251 191 36/var(--tw-ring-opacity,1))}.ring-amber-50{--tw-ring-opacity:1;--tw-ring-color:rgb(255 251 235/var(--tw-ring-opacity,1))}.ring-amber-500{--tw-ring-opacity:1;--tw-ring-color:rgb(245 158 11/var(--tw-ring-opacity,1))}.ring-amber-600{--tw-ring-opacity:1;--tw-ring-color:rgb(217 119 6/var(--tw-ring-opacity,1))}.ring-amber-700{--tw-ring-opacity:1;--tw-ring-color:rgb(180 83 9/var(--tw-ring-opacity,1))}.ring-amber-800{--tw-ring-opacity:1;--tw-ring-color:rgb(146 64 14/var(--tw-ring-opacity,1))}.ring-amber-900{--tw-ring-opacity:1;--tw-ring-color:rgb(120 53 15/var(--tw-ring-opacity,1))}.ring-blue-100{--tw-ring-opacity:1;--tw-ring-color:rgb(219 234 254/var(--tw-ring-opacity,1))}.ring-blue-200{--tw-ring-opacity:1;--tw-ring-color:rgb(191 219 254/var(--tw-ring-opacity,1))}.ring-blue-300{--tw-ring-opacity:1;--tw-ring-color:rgb(147 197 253/var(--tw-ring-opacity,1))}.ring-blue-400{--tw-ring-opacity:1;--tw-ring-color:rgb(96 165 250/var(--tw-ring-opacity,1))}.ring-blue-50{--tw-ring-opacity:1;--tw-ring-color:rgb(239 246 255/var(--tw-ring-opacity,1))}.ring-blue-500{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246/var(--tw-ring-opacity,1))}.ring-blue-600{--tw-ring-opacity:1;--tw-ring-color:rgb(37 99 235/var(--tw-ring-opacity,1))}.ring-blue-700{--tw-ring-opacity:1;--tw-ring-color:rgb(29 78 216/var(--tw-ring-opacity,1))}.ring-blue-800{--tw-ring-opacity:1;--tw-ring-color:rgb(30 64 175/var(--tw-ring-opacity,1))}.ring-blue-900{--tw-ring-opacity:1;--tw-ring-color:rgb(30 58 138/var(--tw-ring-opacity,1))}.ring-cyan-500\/30{--tw-ring-color:rgba(6,182,212,.3)}.ring-emerald-400{--tw-ring-opacity:1;--tw-ring-color:rgb(52 211 153/var(--tw-ring-opacity,1))}.ring-fb-primary{--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity,1))}.ring-gray-100{--tw-ring-opacity:1;--tw-ring-color:rgb(243 244 246/var(--tw-ring-opacity,1))}.ring-gray-200{--tw-ring-opacity:1;--tw-ring-color:rgb(229 231 235/var(--tw-ring-opacity,1))}.ring-gray-300{--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity,1))}.ring-gray-400{--tw-ring-opacity:1;--tw-ring-color:rgb(156 163 175/var(--tw-ring-opacity,1))}.ring-gray-50{--tw-ring-opacity:1;--tw-ring-color:rgb(249 250 251/var(--tw-ring-opacity,1))}.ring-gray-500{--tw-ring-opacity:1;--tw-ring-color:rgb(107 114 128/var(--tw-ring-opacity,1))}.ring-gray-600{--tw-ring-opacity:1;--tw-ring-color:rgb(75 85 99/var(--tw-ring-opacity,1))}.ring-gray-700{--tw-ring-opacity:1;--tw-ring-color:rgb(55 65 81/var(--tw-ring-opacity,1))}.ring-gray-800{--tw-ring-opacity:1;--tw-ring-color:rgb(31 41 55/var(--tw-ring-opacity,1))}.ring-gray-900{--tw-ring-opacity:1;--tw-ring-color:rgb(17 24 39/var(--tw-ring-opacity,1))}.ring-green-100{--tw-ring-opacity:1;--tw-ring-color:rgb(220 252 231/var(--tw-ring-opacity,1))}.ring-green-200{--tw-ring-opacity:1;--tw-ring-color:rgb(187 247 208/var(--tw-ring-opacity,1))}.ring-green-300{--tw-ring-opacity:1;--tw-ring-color:rgb(134 239 172/var(--tw-ring-opacity,1))}.ring-green-400{--tw-ring-opacity:1;--tw-ring-color:rgb(74 222 128/var(--tw-ring-opacity,1))}.ring-green-50{--tw-ring-opacity:1;--tw-ring-color:rgb(240 253 244/var(--tw-ring-opacity,1))}.ring-green-500{--tw-ring-opacity:1;--tw-ring-color:rgb(34 197 94/var(--tw-ring-opacity,1))}.ring-green-600{--tw-ring-opacity:1;--tw-ring-color:rgb(22 163 74/var(--tw-ring-opacity,1))}.ring-green-700{--tw-ring-opacity:1;--tw-ring-color:rgb(21 128 61/var(--tw-ring-opacity,1))}.ring-green-800{--tw-ring-opacity:1;--tw-ring-color:rgb(22 101 52/var(--tw-ring-opacity,1))}.ring-green-900{--tw-ring-opacity:1;--tw-ring-color:rgb(20 83 45/var(--tw-ring-opacity,1))}.ring-indigo-100{--tw-ring-opacity:1;--tw-ring-color:rgb(224 231 255/var(--tw-ring-opacity,1))}.ring-indigo-200{--tw-ring-opacity:1;--tw-ring-color:rgb(199 210 254/var(--tw-ring-opacity,1))}.ring-indigo-300{--tw-ring-opacity:1;--tw-ring-color:rgb(165 180 252/var(--tw-ring-opacity,1))}.ring-indigo-400{--tw-ring-opacity:1;--tw-ring-color:rgb(129 140 248/var(--tw-ring-opacity,1))}.ring-indigo-50{--tw-ring-opacity:1;--tw-ring-color:rgb(238 242 255/var(--tw-ring-opacity,1))}.ring-indigo-500{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity,1))}.ring-indigo-600{--tw-ring-opacity:1;--tw-ring-color:rgb(79 70 229/var(--tw-ring-opacity,1))}.ring-indigo-700{--tw-ring-opacity:1;--tw-ring-color:rgb(67 56 202/var(--tw-ring-opacity,1))}.ring-indigo-800{--tw-ring-opacity:1;--tw-ring-color:rgb(55 48 163/var(--tw-ring-opacity,1))}.ring-indigo-900{--tw-ring-opacity:1;--tw-ring-color:rgb(49 46 129/var(--tw-ring-opacity,1))}.ring-pink-100{--tw-ring-opacity:1;--tw-ring-color:rgb(252 231 243/var(--tw-ring-opacity,1))}.ring-pink-200{--tw-ring-opacity:1;--tw-ring-color:rgb(251 207 232/var(--tw-ring-opacity,1))}.ring-pink-300{--tw-ring-opacity:1;--tw-ring-color:rgb(249 168 212/var(--tw-ring-opacity,1))}.ring-pink-400{--tw-ring-opacity:1;--tw-ring-color:rgb(244 114 182/var(--tw-ring-opacity,1))}.ring-pink-50{--tw-ring-opacity:1;--tw-ring-color:rgb(253 242 248/var(--tw-ring-opacity,1))}.ring-pink-500{--tw-ring-opacity:1;--tw-ring-color:rgb(236 72 153/var(--tw-ring-opacity,1))}.ring-pink-600{--tw-ring-opacity:1;--tw-ring-color:rgb(219 39 119/var(--tw-ring-opacity,1))}.ring-pink-700{--tw-ring-opacity:1;--tw-ring-color:rgb(190 24 93/var(--tw-ring-opacity,1))}.ring-pink-800{--tw-ring-opacity:1;--tw-ring-color:rgb(157 23 77/var(--tw-ring-opacity,1))}.ring-pink-900{--tw-ring-opacity:1;--tw-ring-color:rgb(131 24 67/var(--tw-ring-opacity,1))}.ring-purple-100{--tw-ring-opacity:1;--tw-ring-color:rgb(243 232 255/var(--tw-ring-opacity,1))}.ring-purple-200{--tw-ring-opacity:1;--tw-ring-color:rgb(233 213 255/var(--tw-ring-opacity,1))}.ring-purple-300{--tw-ring-opacity:1;--tw-ring-color:rgb(216 180 254/var(--tw-ring-opacity,1))}.ring-purple-400{--tw-ring-opacity:1;--tw-ring-color:rgb(192 132 252/var(--tw-ring-opacity,1))}.ring-purple-50{--tw-ring-opacity:1;--tw-ring-color:rgb(250 245 255/var(--tw-ring-opacity,1))}.ring-purple-500{--tw-ring-opacity:1;--tw-ring-color:rgb(168 85 247/var(--tw-ring-opacity,1))}.ring-purple-600{--tw-ring-opacity:1;--tw-ring-color:rgb(147 51 234/var(--tw-ring-opacity,1))}.ring-purple-700{--tw-ring-opacity:1;--tw-ring-color:rgb(126 34 206/var(--tw-ring-opacity,1))}.ring-purple-800{--tw-ring-opacity:1;--tw-ring-color:rgb(107 33 168/var(--tw-ring-opacity,1))}.ring-purple-900{--tw-ring-opacity:1;--tw-ring-color:rgb(88 28 135/var(--tw-ring-opacity,1))}.ring-red-100{--tw-ring-opacity:1;--tw-ring-color:rgb(254 226 226/var(--tw-ring-opacity,1))}.ring-red-200{--tw-ring-opacity:1;--tw-ring-color:rgb(254 202 202/var(--tw-ring-opacity,1))}.ring-red-300{--tw-ring-opacity:1;--tw-ring-color:rgb(252 165 165/var(--tw-ring-opacity,1))}.ring-red-400{--tw-ring-opacity:1;--tw-ring-color:rgb(248 113 113/var(--tw-ring-opacity,1))}.ring-red-50{--tw-ring-opacity:1;--tw-ring-color:rgb(254 242 242/var(--tw-ring-opacity,1))}.ring-red-500{--tw-ring-opacity:1;--tw-ring-color:rgb(239 68 68/var(--tw-ring-opacity,1))}.ring-red-600{--tw-ring-opacity:1;--tw-ring-color:rgb(220 38 38/var(--tw-ring-opacity,1))}.ring-red-700{--tw-ring-opacity:1;--tw-ring-color:rgb(185 28 28/var(--tw-ring-opacity,1))}.ring-red-800{--tw-ring-opacity:1;--tw-ring-color:rgb(153 27 27/var(--tw-ring-opacity,1))}.ring-red-900{--tw-ring-opacity:1;--tw-ring-color:rgb(127 29 29/var(--tw-ring-opacity,1))}.ring-slate-100{--tw-ring-opacity:1;--tw-ring-color:rgb(241 245 249/var(--tw-ring-opacity,1))}.ring-slate-200{--tw-ring-opacity:1;--tw-ring-color:rgb(226 232 240/var(--tw-ring-opacity,1))}.ring-slate-300{--tw-ring-opacity:1;--tw-ring-color:rgb(203 213 225/var(--tw-ring-opacity,1))}.ring-slate-400{--tw-ring-opacity:1;--tw-ring-color:rgb(148 163 184/var(--tw-ring-opacity,1))}.ring-slate-50{--tw-ring-opacity:1;--tw-ring-color:rgb(248 250 252/var(--tw-ring-opacity,1))}.ring-slate-500{--tw-ring-opacity:1;--tw-ring-color:rgb(100 116 139/var(--tw-ring-opacity,1))}.ring-slate-600{--tw-ring-opacity:1;--tw-ring-color:rgb(71 85 105/var(--tw-ring-opacity,1))}.ring-slate-700{--tw-ring-opacity:1;--tw-ring-color:rgb(51 65 85/var(--tw-ring-opacity,1))}.ring-slate-800{--tw-ring-opacity:1;--tw-ring-color:rgb(30 41 59/var(--tw-ring-opacity,1))}.ring-slate-900{--tw-ring-opacity:1;--tw-ring-color:rgb(15 23 42/var(--tw-ring-opacity,1))}.ring-yellow-100{--tw-ring-opacity:1;--tw-ring-color:rgb(254 249 195/var(--tw-ring-opacity,1))}.ring-yellow-200{--tw-ring-opacity:1;--tw-ring-color:rgb(254 240 138/var(--tw-ring-opacity,1))}.ring-yellow-300{--tw-ring-opacity:1;--tw-ring-color:rgb(253 224 71/var(--tw-ring-opacity,1))}.ring-yellow-400{--tw-ring-opacity:1;--tw-ring-color:rgb(250 204 21/var(--tw-ring-opacity,1))}.ring-yellow-50{--tw-ring-opacity:1;--tw-ring-color:rgb(254 252 232/var(--tw-ring-opacity,1))}.ring-yellow-500{--tw-ring-opacity:1;--tw-ring-color:rgb(234 179 8/var(--tw-ring-opacity,1))}.ring-yellow-600{--tw-ring-opacity:1;--tw-ring-color:rgb(202 138 4/var(--tw-ring-opacity,1))}.ring-yellow-700{--tw-ring-opacity:1;--tw-ring-color:rgb(161 98 7/var(--tw-ring-opacity,1))}.ring-yellow-800{--tw-ring-opacity:1;--tw-ring-color:rgb(133 77 14/var(--tw-ring-opacity,1))}.ring-yellow-900{--tw-ring-opacity:1;--tw-ring-color:rgb(113 63 18/var(--tw-ring-opacity,1))}.blur{--tw-blur:blur(8px)}.blur,.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))}.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)!important}.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{--tw-backdrop-blur:blur(8px)}.backdrop-blur,.backdrop-blur-lg{-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-lg{--tw-backdrop-blur:blur(16px)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px)}.backdrop-blur-md,.backdrop-blur-sm{-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)}.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-100{transition-duration:.1s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:left-\[2px\]:after{content:var(--tw-content);left:2px}.after\:top-\[2px\]:after{content:var(--tw-content);top:2px}.after\:h-4:after{content:var(--tw-content);height:1rem}.after\:w-4:after{content:var(--tw-content);width:1rem}.after\:rounded-full:after{content:var(--tw-content);border-radius:9999px}.after\:border:after{content:var(--tw-content);border-width:1px}.after\:border-gray-300:after{content:var(--tw-content);--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.after\:bg-white:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.after\:transition-all:after{content:var(--tw-content);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.after\:content-\[\'\'\]:after{--tw-content:"";content:var(--tw-content)}.first\:border-t-0:first-child{border-top-width:0}.first\:pt-0:first-child{padding-top:0}.last\:mb-0:last-child{margin-bottom:0}.last\:border-0:last-child{border-width:0}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05;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-accent\/40:hover{border-color:rgba(64,128,224,.4)}.hover\:border-accent\/50:hover{border-color:rgba(64,128,224,.5)}.hover\:border-fb-accent\/50:hover{border-color:rgba(239,68,68,.5)}.hover\:border-fb-border:hover{--tw-border-opacity:1;border-color:rgb(51 65 85/var(--tw-border-opacity,1))}.hover\:border-fb-border\/70:hover{border-color:rgba(51,65,85,.7)}.hover\:border-fb-primary:hover{--tw-border-opacity:1;border-color:rgb(14 165 233/var(--tw-border-opacity,1))}.hover\:border-fb-primary\/40:hover{border-color:rgba(14,165,233,.4)}.hover\:border-fb-primary\/50:hover{border-color:rgba(14,165,233,.5)}.hover\:border-gray-600:hover{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity,1))}.hover\:border-red-700:hover{--tw-border-opacity:1;border-color:rgb(185 28 28/var(--tw-border-opacity,1))}.hover\:border-white:hover{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity,1))}.hover\:bg-accent-light:hover{--tw-bg-opacity:1;background-color:rgb(96 160 255/var(--tw-bg-opacity,1))}.hover\:bg-accent\/30:hover{background-color:rgba(64,128,224,.3)}.hover\:bg-black\/70:hover{background-color:rgba(0,0,0,.7)}.hover\:bg-black\/75:hover{background-color:rgba(0,0,0,.75)}.hover\:bg-dark-500:hover{--tw-bg-opacity:1;background-color:rgb(30 30 58/var(--tw-bg-opacity,1))}.hover\:bg-dark-700:hover{--tw-bg-opacity:1;background-color:rgb(16 16 30/var(--tw-bg-opacity,1))}.hover\:bg-dark-700\/70:hover{background-color:rgba(16,16,30,.7)}.hover\:bg-emerald-600:hover{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.hover\:bg-fb-border:hover{--tw-bg-opacity:1;background-color:rgb(51 65 85/var(--tw-bg-opacity,1))}.hover\:bg-fb-card:hover{--tw-bg-opacity:1;background-color:rgb(30 41 59/var(--tw-bg-opacity,1))}.hover\:bg-fb-card\/50:hover{background-color:rgba(30,41,59,.5)}.hover\:bg-fb-card\/60:hover{background-color:rgba(30,41,59,.6)}.hover\:bg-fb-card\/70:hover{background-color:rgba(30,41,59,.7)}.hover\:bg-fb-low\/30:hover{background-color:rgba(239,68,68,.3)}.hover\:bg-fb-primary:hover{--tw-bg-opacity:1;background-color:rgb(14 165 233/var(--tw-bg-opacity,1))}.hover\:bg-fb-primaryHi:hover{--tw-bg-opacity:1;background-color:rgb(56 189 248/var(--tw-bg-opacity,1))}.hover\:bg-gold\/20:hover{background-color:rgba(232,192,64,.2)}.hover\:bg-green-900\/50:hover{background-color:rgba(20,83,45,.5)}.hover\:bg-purple-900\/60:hover{background-color:rgba(88,28,135,.6)}.hover\:bg-red-500:hover{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.hover\:bg-red-500\/10:hover{background-color:rgba(239,68,68,.1)}.hover\:bg-red-900\/30:hover{background-color:rgba(127,29,29,.3)}.hover\:bg-red-900\/50:hover{background-color:rgba(127,29,29,.5)}.hover\:bg-red-900\/60:hover{background-color:rgba(127,29,29,.6)}.hover\:bg-white\/5:hover{background-color:hsla(0,0%,100%,.05)}.hover\:text-accent:hover{--tw-text-opacity:1;color:rgb(64 128 224/var(--tw-text-opacity,1))}.hover\:text-accent-light:hover{--tw-text-opacity:1;color:rgb(96 160 255/var(--tw-text-opacity,1))}.hover\:text-fb-accent:hover{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.hover\:text-fb-primaryHi:hover{--tw-text-opacity:1;color:rgb(56 189 248/var(--tw-text-opacity,1))}.hover\:text-fb-text:hover{--tw-text-opacity:1;color:rgb(248 250 252/var(--tw-text-opacity,1))}.hover\:text-gray-200:hover{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.hover\:text-gray-400:hover{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.hover\:text-red-100:hover{--tw-text-opacity:1;color:rgb(254 226 226/var(--tw-text-opacity,1))}.hover\:text-red-200:hover{--tw-text-opacity:1;color:rgb(254 202 202/var(--tw-text-opacity,1))}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-red-400:hover{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:ring-1:hover{--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)}.hover\:ring-1:hover,.hover\:ring-2:hover{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.hover\:ring-2:hover{--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(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.hover\:ring-fb-primary\/40:hover{--tw-ring-color:rgba(14,165,233,.4)}.hover\:ring-fb-primary\/50:hover{--tw-ring-color:rgba(14,165,233,.5)}.focus\:border-accent:focus{--tw-border-opacity:1;border-color:rgb(64 128 224/var(--tw-border-opacity,1))}.focus\:border-accent\/50:focus{border-color:rgba(64,128,224,.5)}.focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity,1))}.focus\:border-fb-primary:focus{--tw-border-opacity:1;border-color:rgb(14 165 233/var(--tw-border-opacity,1))}.focus\:border-fb-primary\/60:focus{border-color:rgba(14,165,233,.6)}.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)}.focus\:ring-1:focus,.focus\:ring-2:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-2: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(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-accent\/30:focus{--tw-ring-color:rgba(64,128,224,.3)}.focus\:ring-accent\/40:focus{--tw-ring-color:rgba(64,128,224,.4)}.focus\:ring-accent\/60:focus{--tw-ring-color:rgba(64,128,224,.6)}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246/var(--tw-ring-opacity,1))}.focus\:ring-fb-primary:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity,1))}.focus\:ring-gray-500\/40:focus{--tw-ring-color:hsla(220,9%,46%,.4)}.focus\:ring-red-400\/60:focus{--tw-ring-color:hsla(0,91%,71%,.6)}.focus\:ring-sky-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity,1))}.active\:scale-95:active{--tw-scale-x:.95;--tw-scale-y:.95;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))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group[open] .group-open\:rotate-180{--tw-rotate:180deg}.group:hover .group-hover\:scale-105,.group[open] .group-open\:rotate-180{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\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05}.group:hover .group-hover\:text-accent-light{--tw-text-opacity:1;color:rgb(96 160 255/var(--tw-text-opacity,1))}.group:hover .group-hover\:text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.group:hover .group-hover\:text-fb-text{--tw-text-opacity:1;color:rgb(248 250 252/var(--tw-text-opacity,1))}.group:hover .group-hover\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.group:hover .group-hover\:opacity-0{opacity:0}.group:hover .group-hover\:opacity-100{opacity:1}.group:hover .group-hover\:opacity-70{opacity:.7}.peer:checked~.peer-checked\:bg-accent{--tw-bg-opacity:1;background-color:rgb(64 128 224/var(--tw-bg-opacity,1))}.peer:checked~.peer-checked\:after\:translate-x-full:after{content:var(--tw-content);--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))}.peer:checked~.peer-checked\:after\:border-white:after{content:var(--tw-content);--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity,1))}.peer:focus~.peer-focus\:outline-none{outline:2px solid transparent;outline-offset:2px}@media (min-width:640px){.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:w-96{width:24rem}.sm\:w-\[26rem\]{width:26rem}.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\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:text-left{text-align:left}.sm\:text-right{text-align:right}}@media (min-width:768px){.md\:col-span-2{grid-column:span 2/span 2}.md\:-mx-8{margin-left:-2rem;margin-right:-2rem}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-60{width:15rem}.md\:w-auto{width:auto}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-end{align-items:flex-end}.md\:items-center{align-items:center}.md\:p-8{padding:2rem}.md\:px-8{padding-left:2rem;padding-right:2rem}.md\:text-3xl{font-size:1.875rem;line-height:2.25rem}}@media (min-width:1024px){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:grid{display:grid}.lg\:w-96{width:24rem}.lg\:max-w-96{max-width:24rem}.lg\:flex-none{flex:none}.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\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:grid-cols-\[minmax\(9rem\2c 0\.9fr\)_minmax\(12rem\2c 1fr\)_4rem_minmax\(12rem\2c 1fr\)\]{grid-template-columns:minmax(9rem,.9fr) minmax(12rem,1fr) 4rem minmax(12rem,1fr)}.lg\:items-center{align-items:center}.lg\:items-stretch{align-items:stretch}.lg\:justify-end{justify-content:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:gap-16{gap:4rem}}@media (min-width:1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:gap-24{gap:6rem}} \ No newline at end of file +/*! 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%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.-left-\[1\.4375rem\]{left:-1.4375rem}.-right-\[1\.4375rem\]{right:-1.4375rem}.bottom-0{bottom:0}.bottom-1{bottom:.25rem}.bottom-1\.5{bottom:.375rem}.bottom-4{bottom:1rem}.bottom-5{bottom:1.25rem}.bottom-6{bottom:1.5rem}.bottom-\[30\%\]{bottom:30%}.bottom-full{bottom:100%}.left-0{left:0}.left-0\.5{left:.125rem}.left-1\.5{left:.375rem}.left-1\/2{left:50%}.left-2{left:.5rem}.left-2\.5{left:.625rem}.left-3{left:.75rem}.left-9{left:2.25rem}.left-\[30\%\]{left:30%}.right-0{right:0}.right-0\.5{right:.125rem}.right-1\.5{right:.375rem}.right-2{right:.5rem}.right-3{right:.75rem}.right-4{right:1rem}.right-5{right:1.25rem}.right-\[30\%\]{right:30%}.top-0{top:0}.top-1{top:.25rem}.top-1\.5{top:.375rem}.top-1\/2{top:50%}.top-10{top:2.5rem}.top-2{top:.5rem}.top-3{top:.75rem}.top-4{top:1rem}.top-\[30\%\]{top:30%}.top-full{top:100%}.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}.z-\[1000\]{z-index:1000}.z-\[1001\]{z-index:1001}.z-\[100\]{z-index:100}.z-\[120\]{z-index:120}.z-\[200\]{z-index:200}.z-\[210\]{z-index:210}.z-\[250\]{z-index:250}.z-\[300\]{z-index:300}.z-\[60\]{z-index:60}.z-\[61\]{z-index:61}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.m-1{margin:.25rem}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.-mx-6{margin-left:-1.5rem;margin-right:-1.5rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-10{margin-bottom:2.5rem}.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-3{margin-left:.75rem}.ml-5{margin-left:1.25rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-10{margin-top:2.5rem}.mt-2{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-auto{margin-top:auto}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.\!block{display:block!important}.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!important}.grid{display:grid}.contents{display:contents}.hidden{display:none}.aspect-\[3\/1\]{aspect-ratio:3/1}.aspect-square{aspect-ratio:1/1}.aspect-video{aspect-ratio:16/9}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-1\/2{height:50%}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-2\/3{height:66.666667%}.h-20{height:5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-32{height:8rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[3px\]{height:3px}.h-\[92px\]{height:92px}.h-\[calc\(50\%-1\.5px\)\]{height:calc(50% - 1.5px)}.h-\[calc\(50\%-2\.5px\)\]{height:calc(50% - 2.5px)}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-32{max-height:8rem}.max-h-64{max-height:16rem}.max-h-72{max-height:18rem}.max-h-96{max-height:24rem}.max-h-\[80vh\]{max-height:80vh}.max-h-\[85vh\]{max-height:85vh}.min-h-5{min-height:1.25rem}.min-h-6{min-height:1.5rem}.min-h-\[18rem\]{min-height:18rem}.min-h-\[2\.5em\]{min-height:2.5em}.min-h-\[480px\]{min-height:480px}.min-h-\[6rem\]{min-height:6rem}.w-0\.5{width:.125rem}.w-1{width:.25rem}.w-1\/2{width:50%}.w-10{width:2.5rem}.w-12{width:3rem}.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{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-60{width:15rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-\[96px\]{width:96px}.w-\[calc\(50\%-1px\)\]{width:calc(50% - 1px)}.w-\[calc\(50\%-2px\)\]{width:calc(50% - 2px)}.w-full{width:100%}.min-w-0{min-width:0}.min-w-40{min-width:10rem}.min-w-\[1\.25rem\]{min-width:1.25rem}.min-w-\[1\.75rem\]{min-width:1.75rem}.min-w-\[10rem\]{min-width:10rem}.min-w-\[180px\]{min-width:180px}.min-w-full{min-width:100%}.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-7xl{max-width:80rem}.max-w-\[11rem\]{max-width:11rem}.max-w-\[130px\]{max-width:130px}.max-w-\[160px\]{max-width:160px}.max-w-\[210px\]{max-width:210px}.max-w-\[5\.5rem\]{max-width:5.5rem}.max-w-\[90\%\]{max-width:90%}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-y-1\/2{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%}.translate-x-full{--tw-translate-x:100%}.-rotate-12,.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))}.-rotate-12{--tw-rotate:-12deg}.rotate-12{--tw-rotate:12deg}.rotate-12,.rotate-45{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))}.rotate-45{--tw-rotate:45deg}.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))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-default{cursor:default}.cursor-grab{cursor:grab}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.resize{resize:both}.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-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-10{gap:2.5rem}.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}.gap-\[3px\]{gap:3px}.gap-px{gap:1px}.space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.125rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem*var(--tw-space-y-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-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem*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-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*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-fb-border\/30>:not([hidden])~:not([hidden]){border-color:rgba(51,65,85,.3)}.self-start{align-self:flex-start}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.scroll-smooth{scroll-behavior:smooth}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.text-wrap{text-wrap:wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.rounded-bl-md{border-bottom-left-radius:.375rem}.rounded-br-md{border-bottom-right-radius:.375rem}.rounded-tl-md{border-top-left-radius:.375rem}.rounded-tr-md{border-top-right-radius:.375rem}.border{border-width:1px}.border-0{border-width:0}.border-2{border-width:2px}.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-none{border-style:none}.border-accent{--tw-border-opacity:1;border-color:rgb(64 128 224/var(--tw-border-opacity,1))}.border-accent-dark{--tw-border-opacity:1;border-color:rgb(32 96 176/var(--tw-border-opacity,1))}.border-accent-dark\/0{border-color:rgba(32,96,176,0)}.border-accent-dark\/10{border-color:rgba(32,96,176,.1)}.border-accent-dark\/100{border-color:#2060b0}.border-accent-dark\/15{border-color:rgba(32,96,176,.15)}.border-accent-dark\/20{border-color:rgba(32,96,176,.2)}.border-accent-dark\/25{border-color:rgba(32,96,176,.25)}.border-accent-dark\/30{border-color:rgba(32,96,176,.3)}.border-accent-dark\/35{border-color:rgba(32,96,176,.35)}.border-accent-dark\/40{border-color:rgba(32,96,176,.4)}.border-accent-dark\/45{border-color:rgba(32,96,176,.45)}.border-accent-dark\/5{border-color:rgba(32,96,176,.05)}.border-accent-dark\/50{border-color:rgba(32,96,176,.5)}.border-accent-dark\/55{border-color:rgba(32,96,176,.55)}.border-accent-dark\/60{border-color:rgba(32,96,176,.6)}.border-accent-dark\/65{border-color:rgba(32,96,176,.65)}.border-accent-dark\/70{border-color:rgba(32,96,176,.7)}.border-accent-dark\/75{border-color:rgba(32,96,176,.75)}.border-accent-dark\/80{border-color:rgba(32,96,176,.8)}.border-accent-dark\/85{border-color:rgba(32,96,176,.85)}.border-accent-dark\/90{border-color:rgba(32,96,176,.9)}.border-accent-dark\/95{border-color:rgba(32,96,176,.95)}.border-accent-light{--tw-border-opacity:1;border-color:rgb(96 160 255/var(--tw-border-opacity,1))}.border-accent-light\/0{border-color:rgba(96,160,255,0)}.border-accent-light\/10{border-color:rgba(96,160,255,.1)}.border-accent-light\/100{border-color:#60a0ff}.border-accent-light\/15{border-color:rgba(96,160,255,.15)}.border-accent-light\/20{border-color:rgba(96,160,255,.2)}.border-accent-light\/25{border-color:rgba(96,160,255,.25)}.border-accent-light\/30{border-color:rgba(96,160,255,.3)}.border-accent-light\/35{border-color:rgba(96,160,255,.35)}.border-accent-light\/40{border-color:rgba(96,160,255,.4)}.border-accent-light\/45{border-color:rgba(96,160,255,.45)}.border-accent-light\/5{border-color:rgba(96,160,255,.05)}.border-accent-light\/50{border-color:rgba(96,160,255,.5)}.border-accent-light\/55{border-color:rgba(96,160,255,.55)}.border-accent-light\/60{border-color:rgba(96,160,255,.6)}.border-accent-light\/65{border-color:rgba(96,160,255,.65)}.border-accent-light\/70{border-color:rgba(96,160,255,.7)}.border-accent-light\/75{border-color:rgba(96,160,255,.75)}.border-accent-light\/80{border-color:rgba(96,160,255,.8)}.border-accent-light\/85{border-color:rgba(96,160,255,.85)}.border-accent-light\/90{border-color:rgba(96,160,255,.9)}.border-accent-light\/95{border-color:rgba(96,160,255,.95)}.border-accent\/20{border-color:rgba(64,128,224,.2)}.border-accent\/40{border-color:rgba(64,128,224,.4)}.border-amber-100{--tw-border-opacity:1;border-color:rgb(254 243 199/var(--tw-border-opacity,1))}.border-amber-200{--tw-border-opacity:1;border-color:rgb(253 230 138/var(--tw-border-opacity,1))}.border-amber-300{--tw-border-opacity:1;border-color:rgb(252 211 77/var(--tw-border-opacity,1))}.border-amber-400{--tw-border-opacity:1;border-color:rgb(251 191 36/var(--tw-border-opacity,1))}.border-amber-400\/40{border-color:rgba(251,191,36,.4)}.border-amber-50{--tw-border-opacity:1;border-color:rgb(255 251 235/var(--tw-border-opacity,1))}.border-amber-500{--tw-border-opacity:1;border-color:rgb(245 158 11/var(--tw-border-opacity,1))}.border-amber-500\/30{border-color:rgba(245,158,11,.3)}.border-amber-600{--tw-border-opacity:1;border-color:rgb(217 119 6/var(--tw-border-opacity,1))}.border-amber-700{--tw-border-opacity:1;border-color:rgb(180 83 9/var(--tw-border-opacity,1))}.border-amber-700\/60{border-color:rgba(180,83,9,.6)}.border-amber-800{--tw-border-opacity:1;border-color:rgb(146 64 14/var(--tw-border-opacity,1))}.border-amber-800\/60{border-color:rgba(146,64,14,.6)}.border-amber-900{--tw-border-opacity:1;border-color:rgb(120 53 15/var(--tw-border-opacity,1))}.border-blue-100{--tw-border-opacity:1;border-color:rgb(219 234 254/var(--tw-border-opacity,1))}.border-blue-200{--tw-border-opacity:1;border-color:rgb(191 219 254/var(--tw-border-opacity,1))}.border-blue-300{--tw-border-opacity:1;border-color:rgb(147 197 253/var(--tw-border-opacity,1))}.border-blue-400{--tw-border-opacity:1;border-color:rgb(96 165 250/var(--tw-border-opacity,1))}.border-blue-50{--tw-border-opacity:1;border-color:rgb(239 246 255/var(--tw-border-opacity,1))}.border-blue-500{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity,1))}.border-blue-600{--tw-border-opacity:1;border-color:rgb(37 99 235/var(--tw-border-opacity,1))}.border-blue-700{--tw-border-opacity:1;border-color:rgb(29 78 216/var(--tw-border-opacity,1))}.border-blue-700\/60{border-color:rgba(29,78,216,.6)}.border-blue-800{--tw-border-opacity:1;border-color:rgb(30 64 175/var(--tw-border-opacity,1))}.border-blue-900{--tw-border-opacity:1;border-color:rgb(30 58 138/var(--tw-border-opacity,1))}.border-dark-500{--tw-border-opacity:1;border-color:rgb(30 30 58/var(--tw-border-opacity,1))}.border-dark-500\/0{border-color:rgba(30,30,58,0)}.border-dark-500\/10{border-color:rgba(30,30,58,.1)}.border-dark-500\/100{border-color:#1e1e3a}.border-dark-500\/15{border-color:rgba(30,30,58,.15)}.border-dark-500\/20{border-color:rgba(30,30,58,.2)}.border-dark-500\/25{border-color:rgba(30,30,58,.25)}.border-dark-500\/30{border-color:rgba(30,30,58,.3)}.border-dark-500\/35{border-color:rgba(30,30,58,.35)}.border-dark-500\/40{border-color:rgba(30,30,58,.4)}.border-dark-500\/45{border-color:rgba(30,30,58,.45)}.border-dark-500\/5{border-color:rgba(30,30,58,.05)}.border-dark-500\/50{border-color:rgba(30,30,58,.5)}.border-dark-500\/55{border-color:rgba(30,30,58,.55)}.border-dark-500\/60{border-color:rgba(30,30,58,.6)}.border-dark-500\/65{border-color:rgba(30,30,58,.65)}.border-dark-500\/70{border-color:rgba(30,30,58,.7)}.border-dark-500\/75{border-color:rgba(30,30,58,.75)}.border-dark-500\/80{border-color:rgba(30,30,58,.8)}.border-dark-500\/85{border-color:rgba(30,30,58,.85)}.border-dark-500\/90{border-color:rgba(30,30,58,.9)}.border-dark-500\/95{border-color:rgba(30,30,58,.95)}.border-dark-600{--tw-border-opacity:1;border-color:rgb(24 24 48/var(--tw-border-opacity,1))}.border-dark-600\/0{border-color:rgba(24,24,48,0)}.border-dark-600\/10{border-color:rgba(24,24,48,.1)}.border-dark-600\/100{border-color:#181830}.border-dark-600\/15{border-color:rgba(24,24,48,.15)}.border-dark-600\/20{border-color:rgba(24,24,48,.2)}.border-dark-600\/25{border-color:rgba(24,24,48,.25)}.border-dark-600\/30{border-color:rgba(24,24,48,.3)}.border-dark-600\/35{border-color:rgba(24,24,48,.35)}.border-dark-600\/40{border-color:rgba(24,24,48,.4)}.border-dark-600\/45{border-color:rgba(24,24,48,.45)}.border-dark-600\/5{border-color:rgba(24,24,48,.05)}.border-dark-600\/50{border-color:rgba(24,24,48,.5)}.border-dark-600\/55{border-color:rgba(24,24,48,.55)}.border-dark-600\/60{border-color:rgba(24,24,48,.6)}.border-dark-600\/65{border-color:rgba(24,24,48,.65)}.border-dark-600\/70{border-color:rgba(24,24,48,.7)}.border-dark-600\/75{border-color:rgba(24,24,48,.75)}.border-dark-600\/80{border-color:rgba(24,24,48,.8)}.border-dark-600\/85{border-color:rgba(24,24,48,.85)}.border-dark-600\/90{border-color:rgba(24,24,48,.9)}.border-dark-600\/95{border-color:rgba(24,24,48,.95)}.border-dark-700{--tw-border-opacity:1;border-color:rgb(16 16 30/var(--tw-border-opacity,1))}.border-dark-700\/0{border-color:rgba(16,16,30,0)}.border-dark-700\/10{border-color:rgba(16,16,30,.1)}.border-dark-700\/100{border-color:#10101e}.border-dark-700\/15{border-color:rgba(16,16,30,.15)}.border-dark-700\/20{border-color:rgba(16,16,30,.2)}.border-dark-700\/25{border-color:rgba(16,16,30,.25)}.border-dark-700\/30{border-color:rgba(16,16,30,.3)}.border-dark-700\/35{border-color:rgba(16,16,30,.35)}.border-dark-700\/40{border-color:rgba(16,16,30,.4)}.border-dark-700\/45{border-color:rgba(16,16,30,.45)}.border-dark-700\/5{border-color:rgba(16,16,30,.05)}.border-dark-700\/50{border-color:rgba(16,16,30,.5)}.border-dark-700\/55{border-color:rgba(16,16,30,.55)}.border-dark-700\/60{border-color:rgba(16,16,30,.6)}.border-dark-700\/65{border-color:rgba(16,16,30,.65)}.border-dark-700\/70{border-color:rgba(16,16,30,.7)}.border-dark-700\/75{border-color:rgba(16,16,30,.75)}.border-dark-700\/80{border-color:rgba(16,16,30,.8)}.border-dark-700\/85{border-color:rgba(16,16,30,.85)}.border-dark-700\/90{border-color:rgba(16,16,30,.9)}.border-dark-700\/95{border-color:rgba(16,16,30,.95)}.border-dark-800{--tw-border-opacity:1;border-color:rgb(10 10 18/var(--tw-border-opacity,1))}.border-dark-800\/0{border-color:rgba(10,10,18,0)}.border-dark-800\/10{border-color:rgba(10,10,18,.1)}.border-dark-800\/100{border-color:#0a0a12}.border-dark-800\/15{border-color:rgba(10,10,18,.15)}.border-dark-800\/20{border-color:rgba(10,10,18,.2)}.border-dark-800\/25{border-color:rgba(10,10,18,.25)}.border-dark-800\/30{border-color:rgba(10,10,18,.3)}.border-dark-800\/35{border-color:rgba(10,10,18,.35)}.border-dark-800\/40{border-color:rgba(10,10,18,.4)}.border-dark-800\/45{border-color:rgba(10,10,18,.45)}.border-dark-800\/5{border-color:rgba(10,10,18,.05)}.border-dark-800\/50{border-color:rgba(10,10,18,.5)}.border-dark-800\/55{border-color:rgba(10,10,18,.55)}.border-dark-800\/60{border-color:rgba(10,10,18,.6)}.border-dark-800\/65{border-color:rgba(10,10,18,.65)}.border-dark-800\/70{border-color:rgba(10,10,18,.7)}.border-dark-800\/75{border-color:rgba(10,10,18,.75)}.border-dark-800\/80{border-color:rgba(10,10,18,.8)}.border-dark-800\/85{border-color:rgba(10,10,18,.85)}.border-dark-800\/90{border-color:rgba(10,10,18,.9)}.border-dark-800\/95{border-color:rgba(10,10,18,.95)}.border-dark-900{--tw-border-opacity:1;border-color:rgb(5 5 8/var(--tw-border-opacity,1))}.border-dark-900\/0{border-color:rgba(5,5,8,0)}.border-dark-900\/10{border-color:rgba(5,5,8,.1)}.border-dark-900\/100{border-color:#050508}.border-dark-900\/15{border-color:rgba(5,5,8,.15)}.border-dark-900\/20{border-color:rgba(5,5,8,.2)}.border-dark-900\/25{border-color:rgba(5,5,8,.25)}.border-dark-900\/30{border-color:rgba(5,5,8,.3)}.border-dark-900\/35{border-color:rgba(5,5,8,.35)}.border-dark-900\/40{border-color:rgba(5,5,8,.4)}.border-dark-900\/45{border-color:rgba(5,5,8,.45)}.border-dark-900\/5{border-color:rgba(5,5,8,.05)}.border-dark-900\/50{border-color:rgba(5,5,8,.5)}.border-dark-900\/55{border-color:rgba(5,5,8,.55)}.border-dark-900\/60{border-color:rgba(5,5,8,.6)}.border-dark-900\/65{border-color:rgba(5,5,8,.65)}.border-dark-900\/70{border-color:rgba(5,5,8,.7)}.border-dark-900\/75{border-color:rgba(5,5,8,.75)}.border-dark-900\/80{border-color:rgba(5,5,8,.8)}.border-dark-900\/85{border-color:rgba(5,5,8,.85)}.border-dark-900\/90{border-color:rgba(5,5,8,.9)}.border-dark-900\/95{border-color:rgba(5,5,8,.95)}.border-emerald-700\/60{border-color:rgba(4,120,87,.6)}.border-emerald-800\/60{border-color:rgba(6,95,70,.6)}.border-fb-border{--tw-border-opacity:1;border-color:rgb(51 65 85/var(--tw-border-opacity,1))}.border-fb-border\/30{border-color:rgba(51,65,85,.3)}.border-fb-border\/40{border-color:rgba(51,65,85,.4)}.border-fb-border\/50{border-color:rgba(51,65,85,.5)}.border-fb-border\/60{border-color:rgba(51,65,85,.6)}.border-fb-border\/70{border-color:rgba(51,65,85,.7)}.border-fb-good\/40{border-color:rgba(34,197,94,.4)}.border-fb-good\/50{border-color:rgba(34,197,94,.5)}.border-fb-low\/40{border-color:rgba(239,68,68,.4)}.border-fb-primary{--tw-border-opacity:1;border-color:rgb(14 165 233/var(--tw-border-opacity,1))}.border-fb-primary\/30{border-color:rgba(14,165,233,.3)}.border-fb-primary\/40{border-color:rgba(14,165,233,.4)}.border-fb-primary\/50{border-color:rgba(14,165,233,.5)}.border-fb-primary\/60{border-color:rgba(14,165,233,.6)}.border-fb-textDim{--tw-border-opacity:1;border-color:rgb(148 163 184/var(--tw-border-opacity,1))}.border-gold{--tw-border-opacity:1;border-color:rgb(232 192 64/var(--tw-border-opacity,1))}.border-gold\/20{border-color:rgba(232,192,64,.2)}.border-gold\/40{border-color:rgba(232,192,64,.4)}.border-gold\/50{border-color:rgba(232,192,64,.5)}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity,1))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/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-400{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity,1))}.border-gray-50{--tw-border-opacity:1;border-color:rgb(249 250 251/var(--tw-border-opacity,1))}.border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/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-gray-700\/60{border-color:rgba(55,65,81,.6)}.border-gray-800{--tw-border-opacity:1;border-color:rgb(31 41 55/var(--tw-border-opacity,1))}.border-gray-800\/50{border-color:rgba(31,41,55,.5)}.border-gray-800\/70{border-color:rgba(31,41,55,.7)}.border-gray-900{--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity,1))}.border-green-100{--tw-border-opacity:1;border-color:rgb(220 252 231/var(--tw-border-opacity,1))}.border-green-200{--tw-border-opacity:1;border-color:rgb(187 247 208/var(--tw-border-opacity,1))}.border-green-300{--tw-border-opacity:1;border-color:rgb(134 239 172/var(--tw-border-opacity,1))}.border-green-400{--tw-border-opacity:1;border-color:rgb(74 222 128/var(--tw-border-opacity,1))}.border-green-50{--tw-border-opacity:1;border-color:rgb(240 253 244/var(--tw-border-opacity,1))}.border-green-500{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity,1))}.border-green-600{--tw-border-opacity:1;border-color:rgb(22 163 74/var(--tw-border-opacity,1))}.border-green-700{--tw-border-opacity:1;border-color:rgb(21 128 61/var(--tw-border-opacity,1))}.border-green-800{--tw-border-opacity:1;border-color:rgb(22 101 52/var(--tw-border-opacity,1))}.border-green-900{--tw-border-opacity:1;border-color:rgb(20 83 45/var(--tw-border-opacity,1))}.border-indigo-100{--tw-border-opacity:1;border-color:rgb(224 231 255/var(--tw-border-opacity,1))}.border-indigo-200{--tw-border-opacity:1;border-color:rgb(199 210 254/var(--tw-border-opacity,1))}.border-indigo-300{--tw-border-opacity:1;border-color:rgb(165 180 252/var(--tw-border-opacity,1))}.border-indigo-400{--tw-border-opacity:1;border-color:rgb(129 140 248/var(--tw-border-opacity,1))}.border-indigo-50{--tw-border-opacity:1;border-color:rgb(238 242 255/var(--tw-border-opacity,1))}.border-indigo-500{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.border-indigo-600{--tw-border-opacity:1;border-color:rgb(79 70 229/var(--tw-border-opacity,1))}.border-indigo-700{--tw-border-opacity:1;border-color:rgb(67 56 202/var(--tw-border-opacity,1))}.border-indigo-800{--tw-border-opacity:1;border-color:rgb(55 48 163/var(--tw-border-opacity,1))}.border-indigo-900{--tw-border-opacity:1;border-color:rgb(49 46 129/var(--tw-border-opacity,1))}.border-orange-200{--tw-border-opacity:1;border-color:rgb(254 215 170/var(--tw-border-opacity,1))}.border-pink-100{--tw-border-opacity:1;border-color:rgb(252 231 243/var(--tw-border-opacity,1))}.border-pink-200{--tw-border-opacity:1;border-color:rgb(251 207 232/var(--tw-border-opacity,1))}.border-pink-300{--tw-border-opacity:1;border-color:rgb(249 168 212/var(--tw-border-opacity,1))}.border-pink-400{--tw-border-opacity:1;border-color:rgb(244 114 182/var(--tw-border-opacity,1))}.border-pink-50{--tw-border-opacity:1;border-color:rgb(253 242 248/var(--tw-border-opacity,1))}.border-pink-500{--tw-border-opacity:1;border-color:rgb(236 72 153/var(--tw-border-opacity,1))}.border-pink-600{--tw-border-opacity:1;border-color:rgb(219 39 119/var(--tw-border-opacity,1))}.border-pink-700{--tw-border-opacity:1;border-color:rgb(190 24 93/var(--tw-border-opacity,1))}.border-pink-800{--tw-border-opacity:1;border-color:rgb(157 23 77/var(--tw-border-opacity,1))}.border-pink-900{--tw-border-opacity:1;border-color:rgb(131 24 67/var(--tw-border-opacity,1))}.border-purple-100{--tw-border-opacity:1;border-color:rgb(243 232 255/var(--tw-border-opacity,1))}.border-purple-200{--tw-border-opacity:1;border-color:rgb(233 213 255/var(--tw-border-opacity,1))}.border-purple-300{--tw-border-opacity:1;border-color:rgb(216 180 254/var(--tw-border-opacity,1))}.border-purple-400{--tw-border-opacity:1;border-color:rgb(192 132 252/var(--tw-border-opacity,1))}.border-purple-400\/30{border-color:rgba(192,132,252,.3)}.border-purple-400\/40{border-color:rgba(192,132,252,.4)}.border-purple-50{--tw-border-opacity:1;border-color:rgb(250 245 255/var(--tw-border-opacity,1))}.border-purple-500{--tw-border-opacity:1;border-color:rgb(168 85 247/var(--tw-border-opacity,1))}.border-purple-600{--tw-border-opacity:1;border-color:rgb(147 51 234/var(--tw-border-opacity,1))}.border-purple-700{--tw-border-opacity:1;border-color:rgb(126 34 206/var(--tw-border-opacity,1))}.border-purple-800{--tw-border-opacity:1;border-color:rgb(107 33 168/var(--tw-border-opacity,1))}.border-purple-900{--tw-border-opacity:1;border-color:rgb(88 28 135/var(--tw-border-opacity,1))}.border-red-100{--tw-border-opacity:1;border-color:rgb(254 226 226/var(--tw-border-opacity,1))}.border-red-200{--tw-border-opacity:1;border-color:rgb(254 202 202/var(--tw-border-opacity,1))}.border-red-300{--tw-border-opacity:1;border-color:rgb(252 165 165/var(--tw-border-opacity,1))}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113/var(--tw-border-opacity,1))}.border-red-50{--tw-border-opacity:1;border-color:rgb(254 242 242/var(--tw-border-opacity,1))}.border-red-500{--tw-border-opacity:1;border-color:rgb(239 68 68/var(--tw-border-opacity,1))}.border-red-500\/40{border-color:rgba(239,68,68,.4)}.border-red-600{--tw-border-opacity:1;border-color:rgb(220 38 38/var(--tw-border-opacity,1))}.border-red-700{--tw-border-opacity:1;border-color:rgb(185 28 28/var(--tw-border-opacity,1))}.border-red-700\/60{border-color:rgba(185,28,28,.6)}.border-red-800{--tw-border-opacity:1;border-color:rgb(153 27 27/var(--tw-border-opacity,1))}.border-red-800\/70{border-color:rgba(153,27,27,.7)}.border-red-900{--tw-border-opacity:1;border-color:rgb(127 29 29/var(--tw-border-opacity,1))}.border-red-900\/30{border-color:rgba(127,29,29,.3)}.border-red-900\/50{border-color:rgba(127,29,29,.5)}.border-slate-100{--tw-border-opacity:1;border-color:rgb(241 245 249/var(--tw-border-opacity,1))}.border-slate-200{--tw-border-opacity:1;border-color:rgb(226 232 240/var(--tw-border-opacity,1))}.border-slate-300{--tw-border-opacity:1;border-color:rgb(203 213 225/var(--tw-border-opacity,1))}.border-slate-400{--tw-border-opacity:1;border-color:rgb(148 163 184/var(--tw-border-opacity,1))}.border-slate-50{--tw-border-opacity:1;border-color:rgb(248 250 252/var(--tw-border-opacity,1))}.border-slate-500{--tw-border-opacity:1;border-color:rgb(100 116 139/var(--tw-border-opacity,1))}.border-slate-600{--tw-border-opacity:1;border-color:rgb(71 85 105/var(--tw-border-opacity,1))}.border-slate-700{--tw-border-opacity:1;border-color:rgb(51 65 85/var(--tw-border-opacity,1))}.border-slate-800{--tw-border-opacity:1;border-color:rgb(30 41 59/var(--tw-border-opacity,1))}.border-slate-900{--tw-border-opacity:1;border-color:rgb(15 23 42/var(--tw-border-opacity,1))}.border-teal-700\/60{border-color:rgba(15,118,110,.6)}.border-violet-100{--tw-border-opacity:1;border-color:rgb(237 233 254/var(--tw-border-opacity,1))}.border-yellow-100{--tw-border-opacity:1;border-color:rgb(254 249 195/var(--tw-border-opacity,1))}.border-yellow-200{--tw-border-opacity:1;border-color:rgb(254 240 138/var(--tw-border-opacity,1))}.border-yellow-300{--tw-border-opacity:1;border-color:rgb(253 224 71/var(--tw-border-opacity,1))}.border-yellow-400{--tw-border-opacity:1;border-color:rgb(250 204 21/var(--tw-border-opacity,1))}.border-yellow-400\/40{border-color:rgba(250,204,21,.4)}.border-yellow-50{--tw-border-opacity:1;border-color:rgb(254 252 232/var(--tw-border-opacity,1))}.border-yellow-500{--tw-border-opacity:1;border-color:rgb(234 179 8/var(--tw-border-opacity,1))}.border-yellow-600{--tw-border-opacity:1;border-color:rgb(202 138 4/var(--tw-border-opacity,1))}.border-yellow-700{--tw-border-opacity:1;border-color:rgb(161 98 7/var(--tw-border-opacity,1))}.border-yellow-800{--tw-border-opacity:1;border-color:rgb(133 77 14/var(--tw-border-opacity,1))}.border-yellow-900{--tw-border-opacity:1;border-color:rgb(113 63 18/var(--tw-border-opacity,1))}.\!bg-accent{--tw-bg-opacity:1!important;background-color:rgb(64 128 224/var(--tw-bg-opacity,1))!important}.bg-accent{--tw-bg-opacity:1;background-color:rgb(64 128 224/var(--tw-bg-opacity,1))}.bg-accent-dark{--tw-bg-opacity:1;background-color:rgb(32 96 176/var(--tw-bg-opacity,1))}.bg-accent-dark\/0{background-color:rgba(32,96,176,0)}.bg-accent-dark\/10{background-color:rgba(32,96,176,.1)}.bg-accent-dark\/100{background-color:#2060b0}.bg-accent-dark\/15{background-color:rgba(32,96,176,.15)}.bg-accent-dark\/20{background-color:rgba(32,96,176,.2)}.bg-accent-dark\/25{background-color:rgba(32,96,176,.25)}.bg-accent-dark\/30{background-color:rgba(32,96,176,.3)}.bg-accent-dark\/35{background-color:rgba(32,96,176,.35)}.bg-accent-dark\/40{background-color:rgba(32,96,176,.4)}.bg-accent-dark\/45{background-color:rgba(32,96,176,.45)}.bg-accent-dark\/5{background-color:rgba(32,96,176,.05)}.bg-accent-dark\/50{background-color:rgba(32,96,176,.5)}.bg-accent-dark\/55{background-color:rgba(32,96,176,.55)}.bg-accent-dark\/60{background-color:rgba(32,96,176,.6)}.bg-accent-dark\/65{background-color:rgba(32,96,176,.65)}.bg-accent-dark\/70{background-color:rgba(32,96,176,.7)}.bg-accent-dark\/75{background-color:rgba(32,96,176,.75)}.bg-accent-dark\/80{background-color:rgba(32,96,176,.8)}.bg-accent-dark\/85{background-color:rgba(32,96,176,.85)}.bg-accent-dark\/90{background-color:rgba(32,96,176,.9)}.bg-accent-dark\/95{background-color:rgba(32,96,176,.95)}.bg-accent-light{--tw-bg-opacity:1;background-color:rgb(96 160 255/var(--tw-bg-opacity,1))}.bg-accent-light\/0{background-color:rgba(96,160,255,0)}.bg-accent-light\/10{background-color:rgba(96,160,255,.1)}.bg-accent-light\/100{background-color:#60a0ff}.bg-accent-light\/15{background-color:rgba(96,160,255,.15)}.bg-accent-light\/20{background-color:rgba(96,160,255,.2)}.bg-accent-light\/25{background-color:rgba(96,160,255,.25)}.bg-accent-light\/30{background-color:rgba(96,160,255,.3)}.bg-accent-light\/35{background-color:rgba(96,160,255,.35)}.bg-accent-light\/40{background-color:rgba(96,160,255,.4)}.bg-accent-light\/45{background-color:rgba(96,160,255,.45)}.bg-accent-light\/5{background-color:rgba(96,160,255,.05)}.bg-accent-light\/50{background-color:rgba(96,160,255,.5)}.bg-accent-light\/55{background-color:rgba(96,160,255,.55)}.bg-accent-light\/60{background-color:rgba(96,160,255,.6)}.bg-accent-light\/65{background-color:rgba(96,160,255,.65)}.bg-accent-light\/70{background-color:rgba(96,160,255,.7)}.bg-accent-light\/75{background-color:rgba(96,160,255,.75)}.bg-accent-light\/80{background-color:rgba(96,160,255,.8)}.bg-accent-light\/85{background-color:rgba(96,160,255,.85)}.bg-accent-light\/90{background-color:rgba(96,160,255,.9)}.bg-accent-light\/95{background-color:rgba(96,160,255,.95)}.bg-accent\/10{background-color:rgba(64,128,224,.1)}.bg-accent\/20{background-color:rgba(64,128,224,.2)}.bg-accent\/30{background-color:rgba(64,128,224,.3)}.bg-amber-100{--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity,1))}.bg-amber-200{--tw-bg-opacity:1;background-color:rgb(253 230 138/var(--tw-bg-opacity,1))}.bg-amber-300{--tw-bg-opacity:1;background-color:rgb(252 211 77/var(--tw-bg-opacity,1))}.bg-amber-400{--tw-bg-opacity:1;background-color:rgb(251 191 36/var(--tw-bg-opacity,1))}.bg-amber-400\/10{background-color:rgba(251,191,36,.1)}.bg-amber-50{--tw-bg-opacity:1;background-color:rgb(255 251 235/var(--tw-bg-opacity,1))}.bg-amber-500{--tw-bg-opacity:1;background-color:rgb(245 158 11/var(--tw-bg-opacity,1))}.bg-amber-500\/10{background-color:rgba(245,158,11,.1)}.bg-amber-500\/5{background-color:rgba(245,158,11,.05)}.bg-amber-600{--tw-bg-opacity:1;background-color:rgb(217 119 6/var(--tw-bg-opacity,1))}.bg-amber-700{--tw-bg-opacity:1;background-color:rgb(180 83 9/var(--tw-bg-opacity,1))}.bg-amber-800{--tw-bg-opacity:1;background-color:rgb(146 64 14/var(--tw-bg-opacity,1))}.bg-amber-900{--tw-bg-opacity:1;background-color:rgb(120 53 15/var(--tw-bg-opacity,1))}.bg-amber-900\/30{background-color:rgba(120,53,15,.3)}.bg-amber-900\/60{background-color:rgba(120,53,15,.6)}.bg-amber-900\/80{background-color:rgba(120,53,15,.8)}.bg-black\/30{background-color:rgba(0,0,0,.3)}.bg-black\/40{background-color:rgba(0,0,0,.4)}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-black\/55{background-color:rgba(0,0,0,.55)}.bg-black\/60{background-color:rgba(0,0,0,.6)}.bg-black\/70{background-color:rgba(0,0,0,.7)}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity,1))}.bg-blue-200{--tw-bg-opacity:1;background-color:rgb(191 219 254/var(--tw-bg-opacity,1))}.bg-blue-300{--tw-bg-opacity:1;background-color:rgb(147 197 253/var(--tw-bg-opacity,1))}.bg-blue-400{--tw-bg-opacity:1;background-color:rgb(96 165 250/var(--tw-bg-opacity,1))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity,1))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.bg-blue-500\/10{background-color:rgba(59,130,246,.1)}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.bg-blue-700{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity,1))}.bg-blue-800{--tw-bg-opacity:1;background-color:rgb(30 64 175/var(--tw-bg-opacity,1))}.bg-blue-900{--tw-bg-opacity:1;background-color:rgb(30 58 138/var(--tw-bg-opacity,1))}.bg-blue-900\/40{background-color:rgba(30,58,138,.4)}.bg-current{background-color:currentColor}.bg-cyan-400{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity,1))}.bg-dark-500{--tw-bg-opacity:1;background-color:rgb(30 30 58/var(--tw-bg-opacity,1))}.bg-dark-500\/0{background-color:rgba(30,30,58,0)}.bg-dark-500\/10{background-color:rgba(30,30,58,.1)}.bg-dark-500\/100{background-color:#1e1e3a}.bg-dark-500\/15{background-color:rgba(30,30,58,.15)}.bg-dark-500\/20{background-color:rgba(30,30,58,.2)}.bg-dark-500\/25{background-color:rgba(30,30,58,.25)}.bg-dark-500\/30{background-color:rgba(30,30,58,.3)}.bg-dark-500\/35{background-color:rgba(30,30,58,.35)}.bg-dark-500\/40{background-color:rgba(30,30,58,.4)}.bg-dark-500\/45{background-color:rgba(30,30,58,.45)}.bg-dark-500\/5{background-color:rgba(30,30,58,.05)}.bg-dark-500\/50{background-color:rgba(30,30,58,.5)}.bg-dark-500\/55{background-color:rgba(30,30,58,.55)}.bg-dark-500\/60{background-color:rgba(30,30,58,.6)}.bg-dark-500\/65{background-color:rgba(30,30,58,.65)}.bg-dark-500\/70{background-color:rgba(30,30,58,.7)}.bg-dark-500\/75{background-color:rgba(30,30,58,.75)}.bg-dark-500\/80{background-color:rgba(30,30,58,.8)}.bg-dark-500\/85{background-color:rgba(30,30,58,.85)}.bg-dark-500\/90{background-color:rgba(30,30,58,.9)}.bg-dark-500\/95{background-color:rgba(30,30,58,.95)}.bg-dark-600{--tw-bg-opacity:1;background-color:rgb(24 24 48/var(--tw-bg-opacity,1))}.bg-dark-600\/0{background-color:rgba(24,24,48,0)}.bg-dark-600\/10{background-color:rgba(24,24,48,.1)}.bg-dark-600\/100{background-color:#181830}.bg-dark-600\/15{background-color:rgba(24,24,48,.15)}.bg-dark-600\/20{background-color:rgba(24,24,48,.2)}.bg-dark-600\/25{background-color:rgba(24,24,48,.25)}.bg-dark-600\/30{background-color:rgba(24,24,48,.3)}.bg-dark-600\/35{background-color:rgba(24,24,48,.35)}.bg-dark-600\/40{background-color:rgba(24,24,48,.4)}.bg-dark-600\/45{background-color:rgba(24,24,48,.45)}.bg-dark-600\/5{background-color:rgba(24,24,48,.05)}.bg-dark-600\/50{background-color:rgba(24,24,48,.5)}.bg-dark-600\/55{background-color:rgba(24,24,48,.55)}.bg-dark-600\/60{background-color:rgba(24,24,48,.6)}.bg-dark-600\/65{background-color:rgba(24,24,48,.65)}.bg-dark-600\/70{background-color:rgba(24,24,48,.7)}.bg-dark-600\/75{background-color:rgba(24,24,48,.75)}.bg-dark-600\/80{background-color:rgba(24,24,48,.8)}.bg-dark-600\/85{background-color:rgba(24,24,48,.85)}.bg-dark-600\/90{background-color:rgba(24,24,48,.9)}.bg-dark-600\/95{background-color:rgba(24,24,48,.95)}.bg-dark-700{--tw-bg-opacity:1;background-color:rgb(16 16 30/var(--tw-bg-opacity,1))}.bg-dark-700\/0{background-color:rgba(16,16,30,0)}.bg-dark-700\/10{background-color:rgba(16,16,30,.1)}.bg-dark-700\/100{background-color:#10101e}.bg-dark-700\/15{background-color:rgba(16,16,30,.15)}.bg-dark-700\/20{background-color:rgba(16,16,30,.2)}.bg-dark-700\/25{background-color:rgba(16,16,30,.25)}.bg-dark-700\/30{background-color:rgba(16,16,30,.3)}.bg-dark-700\/35{background-color:rgba(16,16,30,.35)}.bg-dark-700\/40{background-color:rgba(16,16,30,.4)}.bg-dark-700\/45{background-color:rgba(16,16,30,.45)}.bg-dark-700\/5{background-color:rgba(16,16,30,.05)}.bg-dark-700\/50{background-color:rgba(16,16,30,.5)}.bg-dark-700\/55{background-color:rgba(16,16,30,.55)}.bg-dark-700\/60{background-color:rgba(16,16,30,.6)}.bg-dark-700\/65{background-color:rgba(16,16,30,.65)}.bg-dark-700\/70{background-color:rgba(16,16,30,.7)}.bg-dark-700\/75{background-color:rgba(16,16,30,.75)}.bg-dark-700\/80{background-color:rgba(16,16,30,.8)}.bg-dark-700\/85{background-color:rgba(16,16,30,.85)}.bg-dark-700\/90{background-color:rgba(16,16,30,.9)}.bg-dark-700\/95{background-color:rgba(16,16,30,.95)}.bg-dark-800{--tw-bg-opacity:1;background-color:rgb(10 10 18/var(--tw-bg-opacity,1))}.bg-dark-800\/0{background-color:rgba(10,10,18,0)}.bg-dark-800\/10{background-color:rgba(10,10,18,.1)}.bg-dark-800\/100{background-color:#0a0a12}.bg-dark-800\/15{background-color:rgba(10,10,18,.15)}.bg-dark-800\/20{background-color:rgba(10,10,18,.2)}.bg-dark-800\/25{background-color:rgba(10,10,18,.25)}.bg-dark-800\/30{background-color:rgba(10,10,18,.3)}.bg-dark-800\/35{background-color:rgba(10,10,18,.35)}.bg-dark-800\/40{background-color:rgba(10,10,18,.4)}.bg-dark-800\/45{background-color:rgba(10,10,18,.45)}.bg-dark-800\/5{background-color:rgba(10,10,18,.05)}.bg-dark-800\/50{background-color:rgba(10,10,18,.5)}.bg-dark-800\/55{background-color:rgba(10,10,18,.55)}.bg-dark-800\/60{background-color:rgba(10,10,18,.6)}.bg-dark-800\/65{background-color:rgba(10,10,18,.65)}.bg-dark-800\/70{background-color:rgba(10,10,18,.7)}.bg-dark-800\/75{background-color:rgba(10,10,18,.75)}.bg-dark-800\/80{background-color:rgba(10,10,18,.8)}.bg-dark-800\/85{background-color:rgba(10,10,18,.85)}.bg-dark-800\/90{background-color:rgba(10,10,18,.9)}.bg-dark-800\/95{background-color:rgba(10,10,18,.95)}.bg-dark-900{--tw-bg-opacity:1;background-color:rgb(5 5 8/var(--tw-bg-opacity,1))}.bg-dark-900\/0{background-color:rgba(5,5,8,0)}.bg-dark-900\/10{background-color:rgba(5,5,8,.1)}.bg-dark-900\/100{background-color:#050508}.bg-dark-900\/15{background-color:rgba(5,5,8,.15)}.bg-dark-900\/20{background-color:rgba(5,5,8,.2)}.bg-dark-900\/25{background-color:rgba(5,5,8,.25)}.bg-dark-900\/30{background-color:rgba(5,5,8,.3)}.bg-dark-900\/35{background-color:rgba(5,5,8,.35)}.bg-dark-900\/40{background-color:rgba(5,5,8,.4)}.bg-dark-900\/45{background-color:rgba(5,5,8,.45)}.bg-dark-900\/5{background-color:rgba(5,5,8,.05)}.bg-dark-900\/50{background-color:rgba(5,5,8,.5)}.bg-dark-900\/55{background-color:rgba(5,5,8,.55)}.bg-dark-900\/60{background-color:rgba(5,5,8,.6)}.bg-dark-900\/65{background-color:rgba(5,5,8,.65)}.bg-dark-900\/70{background-color:rgba(5,5,8,.7)}.bg-dark-900\/75{background-color:rgba(5,5,8,.75)}.bg-dark-900\/80{background-color:rgba(5,5,8,.8)}.bg-dark-900\/85{background-color:rgba(5,5,8,.85)}.bg-dark-900\/90{background-color:rgba(5,5,8,.9)}.bg-dark-900\/95{background-color:rgba(5,5,8,.95)}.bg-emerald-400{--tw-bg-opacity:1;background-color:rgb(52 211 153/var(--tw-bg-opacity,1))}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-emerald-500\/10{background-color:rgba(16,185,129,.1)}.bg-emerald-500\/5{background-color:rgba(16,185,129,.05)}.bg-emerald-700{--tw-bg-opacity:1;background-color:rgb(4 120 87/var(--tw-bg-opacity,1))}.bg-fb-bg{--tw-bg-opacity:1;background-color:rgb(15 23 42/var(--tw-bg-opacity,1))}.bg-fb-bg\/40{background-color:rgba(15,23,42,.4)}.bg-fb-bg\/50{background-color:rgba(15,23,42,.5)}.bg-fb-bg\/60{background-color:rgba(15,23,42,.6)}.bg-fb-bg\/95{background-color:rgba(15,23,42,.95)}.bg-fb-border{--tw-bg-opacity:1;background-color:rgb(51 65 85/var(--tw-bg-opacity,1))}.bg-fb-card{--tw-bg-opacity:1;background-color:rgb(30 41 59/var(--tw-bg-opacity,1))}.bg-fb-card\/50{background-color:rgba(30,41,59,.5)}.bg-fb-card\/60{background-color:rgba(30,41,59,.6)}.bg-fb-card\/80{background-color:rgba(30,41,59,.8)}.bg-fb-cardMuted{--tw-bg-opacity:1;background-color:rgb(11 18 32/var(--tw-bg-opacity,1))}.bg-fb-good{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-fb-good\/30{background-color:rgba(34,197,94,.3)}.bg-fb-low{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-fb-low\/15{background-color:rgba(239,68,68,.15)}.bg-fb-low\/30{background-color:rgba(239,68,68,.3)}.bg-fb-mid{--tw-bg-opacity:1;background-color:rgb(234 179 8/var(--tw-bg-opacity,1))}.bg-fb-primary{--tw-bg-opacity:1;background-color:rgb(14 165 233/var(--tw-bg-opacity,1))}.bg-fb-primary\/10{background-color:rgba(14,165,233,.1)}.bg-fb-primary\/15{background-color:rgba(14,165,233,.15)}.bg-fb-primary\/20{background-color:rgba(14,165,233,.2)}.bg-fb-primary\/5{background-color:rgba(14,165,233,.05)}.bg-fb-sidebar{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.bg-fb-sidebar\/80{background-color:rgba(17,24,39,.8)}.bg-fb-sidebar\/95{background-color:rgba(17,24,39,.95)}.bg-gold{--tw-bg-opacity:1;background-color:rgb(232 192 64/var(--tw-bg-opacity,1))}.bg-gold\/10{background-color:rgba(232,192,64,.1)}.bg-gold\/5{background-color:rgba(232,192,64,.05)}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/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-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity,1))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity,1))}.bg-gray-500\/40{background-color:hsla(220,9%,46%,.4)}.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-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.bg-gray-800\/50{background-color:rgba(31,41,55,.5)}.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{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(220 252 231/var(--tw-bg-opacity,1))}.bg-green-200{--tw-bg-opacity:1;background-color:rgb(187 247 208/var(--tw-bg-opacity,1))}.bg-green-300{--tw-bg-opacity:1;background-color:rgb(134 239 172/var(--tw-bg-opacity,1))}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(74 222 128/var(--tw-bg-opacity,1))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity,1))}.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-green-700{--tw-bg-opacity:1;background-color:rgb(21 128 61/var(--tw-bg-opacity,1))}.bg-green-800{--tw-bg-opacity:1;background-color:rgb(22 101 52/var(--tw-bg-opacity,1))}.bg-green-900{--tw-bg-opacity:1;background-color:rgb(20 83 45/var(--tw-bg-opacity,1))}.bg-green-900\/30{background-color:rgba(20,83,45,.3)}.bg-green-900\/40{background-color:rgba(20,83,45,.4)}.bg-green-900\/50{background-color:rgba(20,83,45,.5)}.bg-green-900\/60{background-color:rgba(20,83,45,.6)}.bg-green-900\/80{background-color:rgba(20,83,45,.8)}.bg-indigo-100{--tw-bg-opacity:1;background-color:rgb(224 231 255/var(--tw-bg-opacity,1))}.bg-indigo-200{--tw-bg-opacity:1;background-color:rgb(199 210 254/var(--tw-bg-opacity,1))}.bg-indigo-300{--tw-bg-opacity:1;background-color:rgb(165 180 252/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-50{--tw-bg-opacity:1;background-color:rgb(238 242 255/var(--tw-bg-opacity,1))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity,1))}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity,1))}.bg-indigo-700{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity,1))}.bg-indigo-800{--tw-bg-opacity:1;background-color:rgb(55 48 163/var(--tw-bg-opacity,1))}.bg-indigo-900{--tw-bg-opacity:1;background-color:rgb(49 46 129/var(--tw-bg-opacity,1))}.bg-orange-400{--tw-bg-opacity:1;background-color:rgb(251 146 60/var(--tw-bg-opacity,1))}.bg-orange-500{--tw-bg-opacity:1;background-color:rgb(249 115 22/var(--tw-bg-opacity,1))}.bg-orange-600{--tw-bg-opacity:1;background-color:rgb(234 88 12/var(--tw-bg-opacity,1))}.bg-pink-100{--tw-bg-opacity:1;background-color:rgb(252 231 243/var(--tw-bg-opacity,1))}.bg-pink-200{--tw-bg-opacity:1;background-color:rgb(251 207 232/var(--tw-bg-opacity,1))}.bg-pink-300{--tw-bg-opacity:1;background-color:rgb(249 168 212/var(--tw-bg-opacity,1))}.bg-pink-400{--tw-bg-opacity:1;background-color:rgb(244 114 182/var(--tw-bg-opacity,1))}.bg-pink-50{--tw-bg-opacity:1;background-color:rgb(253 242 248/var(--tw-bg-opacity,1))}.bg-pink-500{--tw-bg-opacity:1;background-color:rgb(236 72 153/var(--tw-bg-opacity,1))}.bg-pink-600{--tw-bg-opacity:1;background-color:rgb(219 39 119/var(--tw-bg-opacity,1))}.bg-pink-700{--tw-bg-opacity:1;background-color:rgb(190 24 93/var(--tw-bg-opacity,1))}.bg-pink-800{--tw-bg-opacity:1;background-color:rgb(157 23 77/var(--tw-bg-opacity,1))}.bg-pink-900{--tw-bg-opacity:1;background-color:rgb(131 24 67/var(--tw-bg-opacity,1))}.bg-purple-100{--tw-bg-opacity:1;background-color:rgb(243 232 255/var(--tw-bg-opacity,1))}.bg-purple-200{--tw-bg-opacity:1;background-color:rgb(233 213 255/var(--tw-bg-opacity,1))}.bg-purple-300{--tw-bg-opacity:1;background-color:rgb(216 180 254/var(--tw-bg-opacity,1))}.bg-purple-400{--tw-bg-opacity:1;background-color:rgb(192 132 252/var(--tw-bg-opacity,1))}.bg-purple-50{--tw-bg-opacity:1;background-color:rgb(250 245 255/var(--tw-bg-opacity,1))}.bg-purple-500{--tw-bg-opacity:1;background-color:rgb(168 85 247/var(--tw-bg-opacity,1))}.bg-purple-500\/10{background-color:rgba(168,85,247,.1)}.bg-purple-500\/20{background-color:rgba(168,85,247,.2)}.bg-purple-500\/40{background-color:rgba(168,85,247,.4)}.bg-purple-600{--tw-bg-opacity:1;background-color:rgb(147 51 234/var(--tw-bg-opacity,1))}.bg-purple-700{--tw-bg-opacity:1;background-color:rgb(126 34 206/var(--tw-bg-opacity,1))}.bg-purple-800{--tw-bg-opacity:1;background-color:rgb(107 33 168/var(--tw-bg-opacity,1))}.bg-purple-900{--tw-bg-opacity:1;background-color:rgb(88 28 135/var(--tw-bg-opacity,1))}.bg-purple-900\/30{background-color:rgba(88,28,135,.3)}.bg-purple-900\/40{background-color:rgba(88,28,135,.4)}.bg-purple-900\/60{background-color:rgba(88,28,135,.6)}.bg-purple-900\/80{background-color:rgba(88,28,135,.8)}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity,1))}.bg-red-200{--tw-bg-opacity:1;background-color:rgb(254 202 202/var(--tw-bg-opacity,1))}.bg-red-300{--tw-bg-opacity:1;background-color:rgb(252 165 165/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-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/10{background-color:rgba(239,68,68,.1)}.bg-red-500\/5{background-color:rgba(239,68,68,.05)}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity,1))}.bg-red-600\/80{background-color:rgba(220,38,38,.8)}.bg-red-700{--tw-bg-opacity:1;background-color:rgb(185 28 28/var(--tw-bg-opacity,1))}.bg-red-800{--tw-bg-opacity:1;background-color:rgb(153 27 27/var(--tw-bg-opacity,1))}.bg-red-900{--tw-bg-opacity:1;background-color:rgb(127 29 29/var(--tw-bg-opacity,1))}.bg-red-900\/10{background-color:rgba(127,29,29,.1)}.bg-red-900\/30{background-color:rgba(127,29,29,.3)}.bg-red-900\/40{background-color:rgba(127,29,29,.4)}.bg-red-900\/95{background-color:rgba(127,29,29,.95)}.bg-slate-100{--tw-bg-opacity:1;background-color:rgb(241 245 249/var(--tw-bg-opacity,1))}.bg-slate-200{--tw-bg-opacity:1;background-color:rgb(226 232 240/var(--tw-bg-opacity,1))}.bg-slate-300{--tw-bg-opacity:1;background-color:rgb(203 213 225/var(--tw-bg-opacity,1))}.bg-slate-400{--tw-bg-opacity:1;background-color:rgb(148 163 184/var(--tw-bg-opacity,1))}.bg-slate-50{--tw-bg-opacity:1;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.bg-slate-500{--tw-bg-opacity:1;background-color:rgb(100 116 139/var(--tw-bg-opacity,1))}.bg-slate-600{--tw-bg-opacity:1;background-color:rgb(71 85 105/var(--tw-bg-opacity,1))}.bg-slate-700{--tw-bg-opacity:1;background-color:rgb(51 65 85/var(--tw-bg-opacity,1))}.bg-slate-800{--tw-bg-opacity:1;background-color:rgb(30 41 59/var(--tw-bg-opacity,1))}.bg-slate-900{--tw-bg-opacity:1;background-color:rgb(15 23 42/var(--tw-bg-opacity,1))}.bg-teal-500\/10{background-color:rgba(20,184,166,.1)}.bg-transparent{background-color:transparent}.bg-violet-200{--tw-bg-opacity:1;background-color:rgb(221 214 254/var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity,1))}.bg-yellow-200{--tw-bg-opacity:1;background-color:rgb(254 240 138/var(--tw-bg-opacity,1))}.bg-yellow-300{--tw-bg-opacity:1;background-color:rgb(253 224 71/var(--tw-bg-opacity,1))}.bg-yellow-400{--tw-bg-opacity:1;background-color:rgb(250 204 21/var(--tw-bg-opacity,1))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity,1))}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgb(234 179 8/var(--tw-bg-opacity,1))}.bg-yellow-500\/10{background-color:rgba(234,179,8,.1)}.bg-yellow-600{--tw-bg-opacity:1;background-color:rgb(202 138 4/var(--tw-bg-opacity,1))}.bg-yellow-700{--tw-bg-opacity:1;background-color:rgb(161 98 7/var(--tw-bg-opacity,1))}.bg-yellow-800{--tw-bg-opacity:1;background-color:rgb(133 77 14/var(--tw-bg-opacity,1))}.bg-yellow-900{--tw-bg-opacity:1;background-color:rgb(113 63 18/var(--tw-bg-opacity,1))}.bg-yellow-900\/30{background-color:rgba(113,63,18,.3)}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-accent-light{--tw-gradient-from:#60a0ff var(--tw-gradient-from-position);--tw-gradient-to:rgba(96,160,255,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.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-black\/80{--tw-gradient-from:rgba(0,0,0,.8) 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-black\/90{--tw-gradient-from:rgba(0,0,0,.9) 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)}.via-black\/40{--tw-gradient-to:transparent var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(0,0,0,.4) var(--tw-gradient-via-position),var(--tw-gradient-to)}.to-purple-400{--tw-gradient-to:#c084fc var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.object-cover{-o-object-fit:cover;object-fit:cover}.object-center{-o-object-position:center;object-position:center}.object-right{-o-object-position:right;object-position:right}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-10{padding:2.5rem}.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-0\.5{padding-left:.125rem;padding-right:.125rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.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}.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-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-24{padding-top:6rem;padding-bottom:6rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-px{padding-top:1px;padding-bottom:1px}.pb-1{padding-bottom:.25rem}.pb-10{padding-bottom:2.5rem}.pb-16{padding-bottom:4rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pl-0{padding-left:0}.pl-10{padding-left:2.5rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pl-8{padding-left:2rem}.pr-0{padding-right:0}.pr-1{padding-right:.25rem}.pr-3{padding-right:.75rem}.pr-4{padding-right:1rem}.pt-0\.5{padding-top:.125rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pt-24{padding-top:6rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.align-middle{vertical-align:middle}.font-display{font-family:Inter,system-ui,sans-serif}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.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-6xl{font-size:3.75rem;line-height:1}.text-9xl{font-size:8rem;line-height:1}.text-\[0\.625rem\]{font-size:.625rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[9px\]{font-size:9px}.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-black{font-weight:900}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.normal-case{text-transform:none}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-5{line-height:1.25rem}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-tight{letter-spacing:-.025em}.tracking-tighter{letter-spacing:-.05em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.\!text-white{--tw-text-opacity:1!important;color:rgb(255 255 255/var(--tw-text-opacity,1))!important}.text-\[\#FF4B4B\]{--tw-text-opacity:1;color:rgb(255 75 75/var(--tw-text-opacity,1))}.text-accent{--tw-text-opacity:1;color:rgb(64 128 224/var(--tw-text-opacity,1))}.text-accent-dark{--tw-text-opacity:1;color:rgb(32 96 176/var(--tw-text-opacity,1))}.text-accent-dark\/0{color:rgba(32,96,176,0)}.text-accent-dark\/10{color:rgba(32,96,176,.1)}.text-accent-dark\/100{color:#2060b0}.text-accent-dark\/15{color:rgba(32,96,176,.15)}.text-accent-dark\/20{color:rgba(32,96,176,.2)}.text-accent-dark\/25{color:rgba(32,96,176,.25)}.text-accent-dark\/30{color:rgba(32,96,176,.3)}.text-accent-dark\/35{color:rgba(32,96,176,.35)}.text-accent-dark\/40{color:rgba(32,96,176,.4)}.text-accent-dark\/45{color:rgba(32,96,176,.45)}.text-accent-dark\/5{color:rgba(32,96,176,.05)}.text-accent-dark\/50{color:rgba(32,96,176,.5)}.text-accent-dark\/55{color:rgba(32,96,176,.55)}.text-accent-dark\/60{color:rgba(32,96,176,.6)}.text-accent-dark\/65{color:rgba(32,96,176,.65)}.text-accent-dark\/70{color:rgba(32,96,176,.7)}.text-accent-dark\/75{color:rgba(32,96,176,.75)}.text-accent-dark\/80{color:rgba(32,96,176,.8)}.text-accent-dark\/85{color:rgba(32,96,176,.85)}.text-accent-dark\/90{color:rgba(32,96,176,.9)}.text-accent-dark\/95{color:rgba(32,96,176,.95)}.text-accent-light{--tw-text-opacity:1;color:rgb(96 160 255/var(--tw-text-opacity,1))}.text-accent-light\/0{color:rgba(96,160,255,0)}.text-accent-light\/10{color:rgba(96,160,255,.1)}.text-accent-light\/100{color:#60a0ff}.text-accent-light\/15{color:rgba(96,160,255,.15)}.text-accent-light\/20{color:rgba(96,160,255,.2)}.text-accent-light\/25{color:rgba(96,160,255,.25)}.text-accent-light\/30{color:rgba(96,160,255,.3)}.text-accent-light\/35{color:rgba(96,160,255,.35)}.text-accent-light\/40{color:rgba(96,160,255,.4)}.text-accent-light\/45{color:rgba(96,160,255,.45)}.text-accent-light\/5{color:rgba(96,160,255,.05)}.text-accent-light\/50{color:rgba(96,160,255,.5)}.text-accent-light\/55{color:rgba(96,160,255,.55)}.text-accent-light\/60{color:rgba(96,160,255,.6)}.text-accent-light\/65{color:rgba(96,160,255,.65)}.text-accent-light\/70{color:rgba(96,160,255,.7)}.text-accent-light\/75{color:rgba(96,160,255,.75)}.text-accent-light\/80{color:rgba(96,160,255,.8)}.text-accent-light\/85{color:rgba(96,160,255,.85)}.text-accent-light\/90{color:rgba(96,160,255,.9)}.text-accent-light\/95{color:rgba(96,160,255,.95)}.text-accent\/70{color:rgba(64,128,224,.7)}.text-amber-100{--tw-text-opacity:1;color:rgb(254 243 199/var(--tw-text-opacity,1))}.text-amber-200{--tw-text-opacity:1;color:rgb(253 230 138/var(--tw-text-opacity,1))}.text-amber-300{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.text-amber-300\/90{color:rgba(252,211,77,.9)}.text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.text-amber-400\/90{color:rgba(251,191,36,.9)}.text-amber-50{--tw-text-opacity:1;color:rgb(255 251 235/var(--tw-text-opacity,1))}.text-amber-500{--tw-text-opacity:1;color:rgb(245 158 11/var(--tw-text-opacity,1))}.text-amber-600{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.text-amber-700{--tw-text-opacity:1;color:rgb(180 83 9/var(--tw-text-opacity,1))}.text-amber-800{--tw-text-opacity:1;color:rgb(146 64 14/var(--tw-text-opacity,1))}.text-amber-900{--tw-text-opacity:1;color:rgb(120 53 15/var(--tw-text-opacity,1))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.text-blue-100{--tw-text-opacity:1;color:rgb(219 234 254/var(--tw-text-opacity,1))}.text-blue-200{--tw-text-opacity:1;color:rgb(191 219 254/var(--tw-text-opacity,1))}.text-blue-300{--tw-text-opacity:1;color:rgb(147 197 253/var(--tw-text-opacity,1))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.text-blue-400\/70{color:rgba(96,165,250,.7)}.text-blue-50{--tw-text-opacity:1;color:rgb(239 246 255/var(--tw-text-opacity,1))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity,1))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity,1))}.text-blue-800{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity,1))}.text-blue-900{--tw-text-opacity:1;color:rgb(30 58 138/var(--tw-text-opacity,1))}.text-cyan-300{--tw-text-opacity:1;color:rgb(103 232 249/var(--tw-text-opacity,1))}.text-cyan-300\/90{color:rgba(103,232,249,.9)}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.text-dark-500{--tw-text-opacity:1;color:rgb(30 30 58/var(--tw-text-opacity,1))}.text-dark-500\/0{color:rgba(30,30,58,0)}.text-dark-500\/10{color:rgba(30,30,58,.1)}.text-dark-500\/100{color:#1e1e3a}.text-dark-500\/15{color:rgba(30,30,58,.15)}.text-dark-500\/20{color:rgba(30,30,58,.2)}.text-dark-500\/25{color:rgba(30,30,58,.25)}.text-dark-500\/30{color:rgba(30,30,58,.3)}.text-dark-500\/35{color:rgba(30,30,58,.35)}.text-dark-500\/40{color:rgba(30,30,58,.4)}.text-dark-500\/45{color:rgba(30,30,58,.45)}.text-dark-500\/5{color:rgba(30,30,58,.05)}.text-dark-500\/50{color:rgba(30,30,58,.5)}.text-dark-500\/55{color:rgba(30,30,58,.55)}.text-dark-500\/60{color:rgba(30,30,58,.6)}.text-dark-500\/65{color:rgba(30,30,58,.65)}.text-dark-500\/70{color:rgba(30,30,58,.7)}.text-dark-500\/75{color:rgba(30,30,58,.75)}.text-dark-500\/80{color:rgba(30,30,58,.8)}.text-dark-500\/85{color:rgba(30,30,58,.85)}.text-dark-500\/90{color:rgba(30,30,58,.9)}.text-dark-500\/95{color:rgba(30,30,58,.95)}.text-dark-600{--tw-text-opacity:1;color:rgb(24 24 48/var(--tw-text-opacity,1))}.text-dark-600\/0{color:rgba(24,24,48,0)}.text-dark-600\/10{color:rgba(24,24,48,.1)}.text-dark-600\/100{color:#181830}.text-dark-600\/15{color:rgba(24,24,48,.15)}.text-dark-600\/20{color:rgba(24,24,48,.2)}.text-dark-600\/25{color:rgba(24,24,48,.25)}.text-dark-600\/30{color:rgba(24,24,48,.3)}.text-dark-600\/35{color:rgba(24,24,48,.35)}.text-dark-600\/40{color:rgba(24,24,48,.4)}.text-dark-600\/45{color:rgba(24,24,48,.45)}.text-dark-600\/5{color:rgba(24,24,48,.05)}.text-dark-600\/50{color:rgba(24,24,48,.5)}.text-dark-600\/55{color:rgba(24,24,48,.55)}.text-dark-600\/60{color:rgba(24,24,48,.6)}.text-dark-600\/65{color:rgba(24,24,48,.65)}.text-dark-600\/70{color:rgba(24,24,48,.7)}.text-dark-600\/75{color:rgba(24,24,48,.75)}.text-dark-600\/80{color:rgba(24,24,48,.8)}.text-dark-600\/85{color:rgba(24,24,48,.85)}.text-dark-600\/90{color:rgba(24,24,48,.9)}.text-dark-600\/95{color:rgba(24,24,48,.95)}.text-dark-700{--tw-text-opacity:1;color:rgb(16 16 30/var(--tw-text-opacity,1))}.text-dark-700\/0{color:rgba(16,16,30,0)}.text-dark-700\/10{color:rgba(16,16,30,.1)}.text-dark-700\/100{color:#10101e}.text-dark-700\/15{color:rgba(16,16,30,.15)}.text-dark-700\/20{color:rgba(16,16,30,.2)}.text-dark-700\/25{color:rgba(16,16,30,.25)}.text-dark-700\/30{color:rgba(16,16,30,.3)}.text-dark-700\/35{color:rgba(16,16,30,.35)}.text-dark-700\/40{color:rgba(16,16,30,.4)}.text-dark-700\/45{color:rgba(16,16,30,.45)}.text-dark-700\/5{color:rgba(16,16,30,.05)}.text-dark-700\/50{color:rgba(16,16,30,.5)}.text-dark-700\/55{color:rgba(16,16,30,.55)}.text-dark-700\/60{color:rgba(16,16,30,.6)}.text-dark-700\/65{color:rgba(16,16,30,.65)}.text-dark-700\/70{color:rgba(16,16,30,.7)}.text-dark-700\/75{color:rgba(16,16,30,.75)}.text-dark-700\/80{color:rgba(16,16,30,.8)}.text-dark-700\/85{color:rgba(16,16,30,.85)}.text-dark-700\/90{color:rgba(16,16,30,.9)}.text-dark-700\/95{color:rgba(16,16,30,.95)}.text-dark-800{--tw-text-opacity:1;color:rgb(10 10 18/var(--tw-text-opacity,1))}.text-dark-800\/0{color:rgba(10,10,18,0)}.text-dark-800\/10{color:rgba(10,10,18,.1)}.text-dark-800\/100{color:#0a0a12}.text-dark-800\/15{color:rgba(10,10,18,.15)}.text-dark-800\/20{color:rgba(10,10,18,.2)}.text-dark-800\/25{color:rgba(10,10,18,.25)}.text-dark-800\/30{color:rgba(10,10,18,.3)}.text-dark-800\/35{color:rgba(10,10,18,.35)}.text-dark-800\/40{color:rgba(10,10,18,.4)}.text-dark-800\/45{color:rgba(10,10,18,.45)}.text-dark-800\/5{color:rgba(10,10,18,.05)}.text-dark-800\/50{color:rgba(10,10,18,.5)}.text-dark-800\/55{color:rgba(10,10,18,.55)}.text-dark-800\/60{color:rgba(10,10,18,.6)}.text-dark-800\/65{color:rgba(10,10,18,.65)}.text-dark-800\/70{color:rgba(10,10,18,.7)}.text-dark-800\/75{color:rgba(10,10,18,.75)}.text-dark-800\/80{color:rgba(10,10,18,.8)}.text-dark-800\/85{color:rgba(10,10,18,.85)}.text-dark-800\/90{color:rgba(10,10,18,.9)}.text-dark-800\/95{color:rgba(10,10,18,.95)}.text-dark-900{--tw-text-opacity:1;color:rgb(5 5 8/var(--tw-text-opacity,1))}.text-dark-900\/0{color:rgba(5,5,8,0)}.text-dark-900\/10{color:rgba(5,5,8,.1)}.text-dark-900\/100{color:#050508}.text-dark-900\/15{color:rgba(5,5,8,.15)}.text-dark-900\/20{color:rgba(5,5,8,.2)}.text-dark-900\/25{color:rgba(5,5,8,.25)}.text-dark-900\/30{color:rgba(5,5,8,.3)}.text-dark-900\/35{color:rgba(5,5,8,.35)}.text-dark-900\/40{color:rgba(5,5,8,.4)}.text-dark-900\/45{color:rgba(5,5,8,.45)}.text-dark-900\/5{color:rgba(5,5,8,.05)}.text-dark-900\/50{color:rgba(5,5,8,.5)}.text-dark-900\/55{color:rgba(5,5,8,.55)}.text-dark-900\/60{color:rgba(5,5,8,.6)}.text-dark-900\/65{color:rgba(5,5,8,.65)}.text-dark-900\/70{color:rgba(5,5,8,.7)}.text-dark-900\/75{color:rgba(5,5,8,.75)}.text-dark-900\/80{color:rgba(5,5,8,.8)}.text-dark-900\/85{color:rgba(5,5,8,.85)}.text-dark-900\/90{color:rgba(5,5,8,.9)}.text-dark-900\/95{color:rgba(5,5,8,.95)}.text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.text-emerald-400{--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.text-fb-accent{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-fb-gold{--tw-text-opacity:1;color:rgb(232 192 64/var(--tw-text-opacity,1))}.text-fb-good{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity,1))}.text-fb-low{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-fb-mid{--tw-text-opacity:1;color:rgb(234 179 8/var(--tw-text-opacity,1))}.text-fb-primary{--tw-text-opacity:1;color:rgb(14 165 233/var(--tw-text-opacity,1))}.text-fb-primary\/90{color:rgba(14,165,233,.9)}.text-fb-text{--tw-text-opacity:1;color:rgb(248 250 252/var(--tw-text-opacity,1))}.text-fb-text\/80{color:rgba(248,250,252,.8)}.text-fb-textDim{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity,1))}.text-fb-textDim\/40{color:rgba(148,163,184,.4)}.text-fb-textDim\/60{color:rgba(148,163,184,.6)}.text-fb-textDim\/70{color:rgba(148,163,184,.7)}.text-gold{--tw-text-opacity:1;color:rgb(232 192 64/var(--tw-text-opacity,1))}.text-gray-100{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity,1))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-50{--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/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-green-100{--tw-text-opacity:1;color:rgb(220 252 231/var(--tw-text-opacity,1))}.text-green-200{--tw-text-opacity:1;color:rgb(187 247 208/var(--tw-text-opacity,1))}.text-green-300{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.text-green-50{--tw-text-opacity:1;color:rgb(240 253 244/var(--tw-text-opacity,1))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity,1))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity,1))}.text-green-700{--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity,1))}.text-green-800{--tw-text-opacity:1;color:rgb(22 101 52/var(--tw-text-opacity,1))}.text-green-900{--tw-text-opacity:1;color:rgb(20 83 45/var(--tw-text-opacity,1))}.text-indigo-100{--tw-text-opacity:1;color:rgb(224 231 255/var(--tw-text-opacity,1))}.text-indigo-200{--tw-text-opacity:1;color:rgb(199 210 254/var(--tw-text-opacity,1))}.text-indigo-300{--tw-text-opacity:1;color:rgb(165 180 252/var(--tw-text-opacity,1))}.text-indigo-400{--tw-text-opacity:1;color:rgb(129 140 248/var(--tw-text-opacity,1))}.text-indigo-50{--tw-text-opacity:1;color:rgb(238 242 255/var(--tw-text-opacity,1))}.text-indigo-500{--tw-text-opacity:1;color:rgb(99 102 241/var(--tw-text-opacity,1))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity,1))}.text-indigo-700{--tw-text-opacity:1;color:rgb(67 56 202/var(--tw-text-opacity,1))}.text-indigo-800{--tw-text-opacity:1;color:rgb(55 48 163/var(--tw-text-opacity,1))}.text-indigo-900{--tw-text-opacity:1;color:rgb(49 46 129/var(--tw-text-opacity,1))}.text-pink-100{--tw-text-opacity:1;color:rgb(252 231 243/var(--tw-text-opacity,1))}.text-pink-200{--tw-text-opacity:1;color:rgb(251 207 232/var(--tw-text-opacity,1))}.text-pink-300{--tw-text-opacity:1;color:rgb(249 168 212/var(--tw-text-opacity,1))}.text-pink-400{--tw-text-opacity:1;color:rgb(244 114 182/var(--tw-text-opacity,1))}.text-pink-50{--tw-text-opacity:1;color:rgb(253 242 248/var(--tw-text-opacity,1))}.text-pink-500{--tw-text-opacity:1;color:rgb(236 72 153/var(--tw-text-opacity,1))}.text-pink-600{--tw-text-opacity:1;color:rgb(219 39 119/var(--tw-text-opacity,1))}.text-pink-700{--tw-text-opacity:1;color:rgb(190 24 93/var(--tw-text-opacity,1))}.text-pink-800{--tw-text-opacity:1;color:rgb(157 23 77/var(--tw-text-opacity,1))}.text-pink-900{--tw-text-opacity:1;color:rgb(131 24 67/var(--tw-text-opacity,1))}.text-purple-100{--tw-text-opacity:1;color:rgb(243 232 255/var(--tw-text-opacity,1))}.text-purple-200{--tw-text-opacity:1;color:rgb(233 213 255/var(--tw-text-opacity,1))}.text-purple-300{--tw-text-opacity:1;color:rgb(216 180 254/var(--tw-text-opacity,1))}.text-purple-400{--tw-text-opacity:1;color:rgb(192 132 252/var(--tw-text-opacity,1))}.text-purple-50{--tw-text-opacity:1;color:rgb(250 245 255/var(--tw-text-opacity,1))}.text-purple-500{--tw-text-opacity:1;color:rgb(168 85 247/var(--tw-text-opacity,1))}.text-purple-600{--tw-text-opacity:1;color:rgb(147 51 234/var(--tw-text-opacity,1))}.text-purple-700{--tw-text-opacity:1;color:rgb(126 34 206/var(--tw-text-opacity,1))}.text-purple-800{--tw-text-opacity:1;color:rgb(107 33 168/var(--tw-text-opacity,1))}.text-purple-900{--tw-text-opacity:1;color:rgb(88 28 135/var(--tw-text-opacity,1))}.text-red-100{--tw-text-opacity:1;color:rgb(254 226 226/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-400\/90{color:hsla(0,91%,71%,.9)}.text-red-50{--tw-text-opacity:1;color:rgb(254 242 242/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.text-red-900{--tw-text-opacity:1;color:rgb(127 29 29/var(--tw-text-opacity,1))}.text-sky-500{--tw-text-opacity:1;color:rgb(14 165 233/var(--tw-text-opacity,1))}.text-slate-100{--tw-text-opacity:1;color:rgb(241 245 249/var(--tw-text-opacity,1))}.text-slate-200{--tw-text-opacity:1;color:rgb(226 232 240/var(--tw-text-opacity,1))}.text-slate-300{--tw-text-opacity:1;color:rgb(203 213 225/var(--tw-text-opacity,1))}.text-slate-400{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity,1))}.text-slate-50{--tw-text-opacity:1;color:rgb(248 250 252/var(--tw-text-opacity,1))}.text-slate-500{--tw-text-opacity:1;color:rgb(100 116 139/var(--tw-text-opacity,1))}.text-slate-600{--tw-text-opacity:1;color:rgb(71 85 105/var(--tw-text-opacity,1))}.text-slate-700{--tw-text-opacity:1;color:rgb(51 65 85/var(--tw-text-opacity,1))}.text-slate-800{--tw-text-opacity:1;color:rgb(30 41 59/var(--tw-text-opacity,1))}.text-slate-900{--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity,1))}.text-teal-300{--tw-text-opacity:1;color:rgb(94 234 212/var(--tw-text-opacity,1))}.text-transparent{color:transparent}.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-yellow-100{--tw-text-opacity:1;color:rgb(254 249 195/var(--tw-text-opacity,1))}.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-300\/80{color:rgba(253,224,71,.8)}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.text-yellow-50{--tw-text-opacity:1;color:rgb(254 252 232/var(--tw-text-opacity,1))}.text-yellow-500{--tw-text-opacity:1;color:rgb(234 179 8/var(--tw-text-opacity,1))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity,1))}.text-yellow-700{--tw-text-opacity:1;color:rgb(161 98 7/var(--tw-text-opacity,1))}.text-yellow-800{--tw-text-opacity:1;color:rgb(133 77 14/var(--tw-text-opacity,1))}.text-yellow-900{--tw-text-opacity:1;color:rgb(113 63 18/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.overline{text-decoration-line:overline}.line-through{text-decoration-line:line-through}.placeholder-fb-textDim::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(148 163 184/var(--tw-placeholder-opacity,1))}.placeholder-fb-textDim::placeholder{--tw-placeholder-opacity:1;color:rgb(148 163 184/var(--tw-placeholder-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))}.placeholder-gray-600::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(75 85 99/var(--tw-placeholder-opacity,1))}.placeholder-gray-600::placeholder{--tw-placeholder-opacity:1;color:rgb(75 85 99/var(--tw-placeholder-opacity,1))}.accent-accent{accent-color:#4080e0}.accent-fb-primary{accent-color:#0ea5e9}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-20{opacity:.2}.opacity-30{opacity:.3}.opacity-40{opacity:.4}.opacity-5{opacity:.05}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-2xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.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-\[0_0_10px_3px_rgba\(52\2c 211\2c 153\2c 0\.95\)\]{--tw-shadow:0 0 10px 3px rgba(52,211,153,.95);--tw-shadow-colored:0 0 10px 3px 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-\[0_0_12px_rgba\(192\2c 132\2c 252\2c 0\.55\)\]{--tw-shadow:0 0 12px rgba(192,132,252,.55);--tw-shadow-colored:0 0 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-\[0_0_12px_rgba\(192\2c 132\2c 252\2c 0\.65\)\]{--tw-shadow:0 0 12px rgba(192,132,252,.65);--tw-shadow-colored:0 0 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-\[0_0_12px_rgba\(216\2c 180\2c 254\2c 0\.65\)\]{--tw-shadow:0 0 12px rgba(216,180,254,.65);--tw-shadow-colored:0 0 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-\[0_0_12px_rgba\(221\2c 214\2c 254\2c 0\.6\)\]{--tw-shadow:0 0 12px rgba(221,214,254,.6);--tw-shadow-colored:0 0 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-\[0_0_12px_rgba\(251\2c 146\2c 60\2c 0\.55\)\]{--tw-shadow:0 0 12px rgba(251,146,60,.55);--tw-shadow-colored:0 0 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-\[0_0_12px_rgba\(251\2c 146\2c 60\2c 0\.65\)\]{--tw-shadow:0 0 12px rgba(251,146,60,.65);--tw-shadow-colored:0 0 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-\[0_0_12px_rgba\(96\2c 165\2c 250\2c 0\.55\)\]{--tw-shadow:0 0 12px rgba(96,165,250,.55);--tw-shadow-colored:0 0 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-\[0_0_12px_rgba\(96\2c 165\2c 250\2c 0\.65\)\]{--tw-shadow:0 0 12px rgba(96,165,250,.65);--tw-shadow-colored:0 0 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-\[0_0_4px_1px_rgba\(185\2c 28\2c 28\2c 0\.7\)\]{--tw-shadow:0 0 4px 1px rgba(185,28,28,.7);--tw-shadow-colored:0 0 4px 1px 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-\[0_0_6px_2px_rgba\(239\2c 68\2c 68\2c 0\.8\)\]{--tw-shadow:0 0 6px 2px rgba(239,68,68,.8);--tw-shadow-colored:0 0 6px 2px 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-\[0_0_6px_2px_rgba\(249\2c 115\2c 22\2c 0\.85\)\]{--tw-shadow:0 0 6px 2px rgba(249,115,22,.85);--tw-shadow-colored:0 0 6px 2px 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-\[0_0_8px_2px_rgba\(234\2c 179\2c 8\2c 0\.9\)\]{--tw-shadow:0 0 8px 2px rgba(234,179,8,.9);--tw-shadow-colored:0 0 8px 2px 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-\[0_0_8px_2px_rgba\(52\2c 211\2c 153\2c 0\.85\)\]{--tw-shadow:0 0 8px 2px rgba(52,211,153,.85);--tw-shadow-colored:0 0 8px 2px 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-\[0_0_8px_rgba\(255\2c 255\2c 255\2c 0\.5\)\]{--tw-shadow:0 0 8px hsla(0,0%,100%,.5);--tw-shadow-colored:0 0 8px 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-\[0_0_8px_rgba\(74\2c 222\2c 128\2c 0\.5\)\]{--tw-shadow:0 0 8px rgba(74,222,128,.5);--tw-shadow-colored:0 0 8px var(--tw-shadow-color)}.shadow-\[0_0_8px_rgba\(74\2c 222\2c 128\2c 0\.5\)\],.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-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);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-fb-primary\/20{--tw-shadow-color:rgba(14,165,233,.2);--tw-shadow:var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.\!ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)!important;--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)!important;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)!important}.ring{--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(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-1{--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)}.ring-2{--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(2px + 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)}.ring-amber-100{--tw-ring-opacity:1;--tw-ring-color:rgb(254 243 199/var(--tw-ring-opacity,1))}.ring-amber-200{--tw-ring-opacity:1;--tw-ring-color:rgb(253 230 138/var(--tw-ring-opacity,1))}.ring-amber-300{--tw-ring-opacity:1;--tw-ring-color:rgb(252 211 77/var(--tw-ring-opacity,1))}.ring-amber-400{--tw-ring-opacity:1;--tw-ring-color:rgb(251 191 36/var(--tw-ring-opacity,1))}.ring-amber-50{--tw-ring-opacity:1;--tw-ring-color:rgb(255 251 235/var(--tw-ring-opacity,1))}.ring-amber-500{--tw-ring-opacity:1;--tw-ring-color:rgb(245 158 11/var(--tw-ring-opacity,1))}.ring-amber-600{--tw-ring-opacity:1;--tw-ring-color:rgb(217 119 6/var(--tw-ring-opacity,1))}.ring-amber-700{--tw-ring-opacity:1;--tw-ring-color:rgb(180 83 9/var(--tw-ring-opacity,1))}.ring-amber-800{--tw-ring-opacity:1;--tw-ring-color:rgb(146 64 14/var(--tw-ring-opacity,1))}.ring-amber-900{--tw-ring-opacity:1;--tw-ring-color:rgb(120 53 15/var(--tw-ring-opacity,1))}.ring-blue-100{--tw-ring-opacity:1;--tw-ring-color:rgb(219 234 254/var(--tw-ring-opacity,1))}.ring-blue-200{--tw-ring-opacity:1;--tw-ring-color:rgb(191 219 254/var(--tw-ring-opacity,1))}.ring-blue-300{--tw-ring-opacity:1;--tw-ring-color:rgb(147 197 253/var(--tw-ring-opacity,1))}.ring-blue-400{--tw-ring-opacity:1;--tw-ring-color:rgb(96 165 250/var(--tw-ring-opacity,1))}.ring-blue-50{--tw-ring-opacity:1;--tw-ring-color:rgb(239 246 255/var(--tw-ring-opacity,1))}.ring-blue-500{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246/var(--tw-ring-opacity,1))}.ring-blue-600{--tw-ring-opacity:1;--tw-ring-color:rgb(37 99 235/var(--tw-ring-opacity,1))}.ring-blue-700{--tw-ring-opacity:1;--tw-ring-color:rgb(29 78 216/var(--tw-ring-opacity,1))}.ring-blue-800{--tw-ring-opacity:1;--tw-ring-color:rgb(30 64 175/var(--tw-ring-opacity,1))}.ring-blue-900{--tw-ring-opacity:1;--tw-ring-color:rgb(30 58 138/var(--tw-ring-opacity,1))}.ring-cyan-500\/30{--tw-ring-color:rgba(6,182,212,.3)}.ring-emerald-400{--tw-ring-opacity:1;--tw-ring-color:rgb(52 211 153/var(--tw-ring-opacity,1))}.ring-fb-primary{--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity,1))}.ring-gray-100{--tw-ring-opacity:1;--tw-ring-color:rgb(243 244 246/var(--tw-ring-opacity,1))}.ring-gray-200{--tw-ring-opacity:1;--tw-ring-color:rgb(229 231 235/var(--tw-ring-opacity,1))}.ring-gray-300{--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity,1))}.ring-gray-400{--tw-ring-opacity:1;--tw-ring-color:rgb(156 163 175/var(--tw-ring-opacity,1))}.ring-gray-50{--tw-ring-opacity:1;--tw-ring-color:rgb(249 250 251/var(--tw-ring-opacity,1))}.ring-gray-500{--tw-ring-opacity:1;--tw-ring-color:rgb(107 114 128/var(--tw-ring-opacity,1))}.ring-gray-600{--tw-ring-opacity:1;--tw-ring-color:rgb(75 85 99/var(--tw-ring-opacity,1))}.ring-gray-700{--tw-ring-opacity:1;--tw-ring-color:rgb(55 65 81/var(--tw-ring-opacity,1))}.ring-gray-800{--tw-ring-opacity:1;--tw-ring-color:rgb(31 41 55/var(--tw-ring-opacity,1))}.ring-gray-900{--tw-ring-opacity:1;--tw-ring-color:rgb(17 24 39/var(--tw-ring-opacity,1))}.ring-green-100{--tw-ring-opacity:1;--tw-ring-color:rgb(220 252 231/var(--tw-ring-opacity,1))}.ring-green-200{--tw-ring-opacity:1;--tw-ring-color:rgb(187 247 208/var(--tw-ring-opacity,1))}.ring-green-300{--tw-ring-opacity:1;--tw-ring-color:rgb(134 239 172/var(--tw-ring-opacity,1))}.ring-green-400{--tw-ring-opacity:1;--tw-ring-color:rgb(74 222 128/var(--tw-ring-opacity,1))}.ring-green-50{--tw-ring-opacity:1;--tw-ring-color:rgb(240 253 244/var(--tw-ring-opacity,1))}.ring-green-500{--tw-ring-opacity:1;--tw-ring-color:rgb(34 197 94/var(--tw-ring-opacity,1))}.ring-green-600{--tw-ring-opacity:1;--tw-ring-color:rgb(22 163 74/var(--tw-ring-opacity,1))}.ring-green-700{--tw-ring-opacity:1;--tw-ring-color:rgb(21 128 61/var(--tw-ring-opacity,1))}.ring-green-800{--tw-ring-opacity:1;--tw-ring-color:rgb(22 101 52/var(--tw-ring-opacity,1))}.ring-green-900{--tw-ring-opacity:1;--tw-ring-color:rgb(20 83 45/var(--tw-ring-opacity,1))}.ring-indigo-100{--tw-ring-opacity:1;--tw-ring-color:rgb(224 231 255/var(--tw-ring-opacity,1))}.ring-indigo-200{--tw-ring-opacity:1;--tw-ring-color:rgb(199 210 254/var(--tw-ring-opacity,1))}.ring-indigo-300{--tw-ring-opacity:1;--tw-ring-color:rgb(165 180 252/var(--tw-ring-opacity,1))}.ring-indigo-400{--tw-ring-opacity:1;--tw-ring-color:rgb(129 140 248/var(--tw-ring-opacity,1))}.ring-indigo-50{--tw-ring-opacity:1;--tw-ring-color:rgb(238 242 255/var(--tw-ring-opacity,1))}.ring-indigo-500{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity,1))}.ring-indigo-600{--tw-ring-opacity:1;--tw-ring-color:rgb(79 70 229/var(--tw-ring-opacity,1))}.ring-indigo-700{--tw-ring-opacity:1;--tw-ring-color:rgb(67 56 202/var(--tw-ring-opacity,1))}.ring-indigo-800{--tw-ring-opacity:1;--tw-ring-color:rgb(55 48 163/var(--tw-ring-opacity,1))}.ring-indigo-900{--tw-ring-opacity:1;--tw-ring-color:rgb(49 46 129/var(--tw-ring-opacity,1))}.ring-pink-100{--tw-ring-opacity:1;--tw-ring-color:rgb(252 231 243/var(--tw-ring-opacity,1))}.ring-pink-200{--tw-ring-opacity:1;--tw-ring-color:rgb(251 207 232/var(--tw-ring-opacity,1))}.ring-pink-300{--tw-ring-opacity:1;--tw-ring-color:rgb(249 168 212/var(--tw-ring-opacity,1))}.ring-pink-400{--tw-ring-opacity:1;--tw-ring-color:rgb(244 114 182/var(--tw-ring-opacity,1))}.ring-pink-50{--tw-ring-opacity:1;--tw-ring-color:rgb(253 242 248/var(--tw-ring-opacity,1))}.ring-pink-500{--tw-ring-opacity:1;--tw-ring-color:rgb(236 72 153/var(--tw-ring-opacity,1))}.ring-pink-600{--tw-ring-opacity:1;--tw-ring-color:rgb(219 39 119/var(--tw-ring-opacity,1))}.ring-pink-700{--tw-ring-opacity:1;--tw-ring-color:rgb(190 24 93/var(--tw-ring-opacity,1))}.ring-pink-800{--tw-ring-opacity:1;--tw-ring-color:rgb(157 23 77/var(--tw-ring-opacity,1))}.ring-pink-900{--tw-ring-opacity:1;--tw-ring-color:rgb(131 24 67/var(--tw-ring-opacity,1))}.ring-purple-100{--tw-ring-opacity:1;--tw-ring-color:rgb(243 232 255/var(--tw-ring-opacity,1))}.ring-purple-200{--tw-ring-opacity:1;--tw-ring-color:rgb(233 213 255/var(--tw-ring-opacity,1))}.ring-purple-300{--tw-ring-opacity:1;--tw-ring-color:rgb(216 180 254/var(--tw-ring-opacity,1))}.ring-purple-400{--tw-ring-opacity:1;--tw-ring-color:rgb(192 132 252/var(--tw-ring-opacity,1))}.ring-purple-50{--tw-ring-opacity:1;--tw-ring-color:rgb(250 245 255/var(--tw-ring-opacity,1))}.ring-purple-500{--tw-ring-opacity:1;--tw-ring-color:rgb(168 85 247/var(--tw-ring-opacity,1))}.ring-purple-600{--tw-ring-opacity:1;--tw-ring-color:rgb(147 51 234/var(--tw-ring-opacity,1))}.ring-purple-700{--tw-ring-opacity:1;--tw-ring-color:rgb(126 34 206/var(--tw-ring-opacity,1))}.ring-purple-800{--tw-ring-opacity:1;--tw-ring-color:rgb(107 33 168/var(--tw-ring-opacity,1))}.ring-purple-900{--tw-ring-opacity:1;--tw-ring-color:rgb(88 28 135/var(--tw-ring-opacity,1))}.ring-red-100{--tw-ring-opacity:1;--tw-ring-color:rgb(254 226 226/var(--tw-ring-opacity,1))}.ring-red-200{--tw-ring-opacity:1;--tw-ring-color:rgb(254 202 202/var(--tw-ring-opacity,1))}.ring-red-300{--tw-ring-opacity:1;--tw-ring-color:rgb(252 165 165/var(--tw-ring-opacity,1))}.ring-red-400{--tw-ring-opacity:1;--tw-ring-color:rgb(248 113 113/var(--tw-ring-opacity,1))}.ring-red-50{--tw-ring-opacity:1;--tw-ring-color:rgb(254 242 242/var(--tw-ring-opacity,1))}.ring-red-500{--tw-ring-opacity:1;--tw-ring-color:rgb(239 68 68/var(--tw-ring-opacity,1))}.ring-red-600{--tw-ring-opacity:1;--tw-ring-color:rgb(220 38 38/var(--tw-ring-opacity,1))}.ring-red-700{--tw-ring-opacity:1;--tw-ring-color:rgb(185 28 28/var(--tw-ring-opacity,1))}.ring-red-800{--tw-ring-opacity:1;--tw-ring-color:rgb(153 27 27/var(--tw-ring-opacity,1))}.ring-red-900{--tw-ring-opacity:1;--tw-ring-color:rgb(127 29 29/var(--tw-ring-opacity,1))}.ring-slate-100{--tw-ring-opacity:1;--tw-ring-color:rgb(241 245 249/var(--tw-ring-opacity,1))}.ring-slate-200{--tw-ring-opacity:1;--tw-ring-color:rgb(226 232 240/var(--tw-ring-opacity,1))}.ring-slate-300{--tw-ring-opacity:1;--tw-ring-color:rgb(203 213 225/var(--tw-ring-opacity,1))}.ring-slate-400{--tw-ring-opacity:1;--tw-ring-color:rgb(148 163 184/var(--tw-ring-opacity,1))}.ring-slate-50{--tw-ring-opacity:1;--tw-ring-color:rgb(248 250 252/var(--tw-ring-opacity,1))}.ring-slate-500{--tw-ring-opacity:1;--tw-ring-color:rgb(100 116 139/var(--tw-ring-opacity,1))}.ring-slate-600{--tw-ring-opacity:1;--tw-ring-color:rgb(71 85 105/var(--tw-ring-opacity,1))}.ring-slate-700{--tw-ring-opacity:1;--tw-ring-color:rgb(51 65 85/var(--tw-ring-opacity,1))}.ring-slate-800{--tw-ring-opacity:1;--tw-ring-color:rgb(30 41 59/var(--tw-ring-opacity,1))}.ring-slate-900{--tw-ring-opacity:1;--tw-ring-color:rgb(15 23 42/var(--tw-ring-opacity,1))}.ring-yellow-100{--tw-ring-opacity:1;--tw-ring-color:rgb(254 249 195/var(--tw-ring-opacity,1))}.ring-yellow-200{--tw-ring-opacity:1;--tw-ring-color:rgb(254 240 138/var(--tw-ring-opacity,1))}.ring-yellow-300{--tw-ring-opacity:1;--tw-ring-color:rgb(253 224 71/var(--tw-ring-opacity,1))}.ring-yellow-400{--tw-ring-opacity:1;--tw-ring-color:rgb(250 204 21/var(--tw-ring-opacity,1))}.ring-yellow-50{--tw-ring-opacity:1;--tw-ring-color:rgb(254 252 232/var(--tw-ring-opacity,1))}.ring-yellow-500{--tw-ring-opacity:1;--tw-ring-color:rgb(234 179 8/var(--tw-ring-opacity,1))}.ring-yellow-600{--tw-ring-opacity:1;--tw-ring-color:rgb(202 138 4/var(--tw-ring-opacity,1))}.ring-yellow-700{--tw-ring-opacity:1;--tw-ring-color:rgb(161 98 7/var(--tw-ring-opacity,1))}.ring-yellow-800{--tw-ring-opacity:1;--tw-ring-color:rgb(133 77 14/var(--tw-ring-opacity,1))}.ring-yellow-900{--tw-ring-opacity:1;--tw-ring-color:rgb(113 63 18/var(--tw-ring-opacity,1))}.blur{--tw-blur:blur(8px)}.blur,.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))}.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)!important}.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{--tw-backdrop-blur:blur(8px)}.backdrop-blur,.backdrop-blur-lg{-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-lg{--tw-backdrop-blur:blur(16px)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px)}.backdrop-blur-md,.backdrop-blur-sm{-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)}.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-100{transition-duration:.1s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:left-\[2px\]:after{content:var(--tw-content);left:2px}.after\:top-\[2px\]:after{content:var(--tw-content);top:2px}.after\:h-4:after{content:var(--tw-content);height:1rem}.after\:w-4:after{content:var(--tw-content);width:1rem}.after\:rounded-full:after{content:var(--tw-content);border-radius:9999px}.after\:border:after{content:var(--tw-content);border-width:1px}.after\:border-gray-300:after{content:var(--tw-content);--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.after\:bg-white:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.after\:transition-all:after{content:var(--tw-content);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.after\:content-\[\'\'\]:after{--tw-content:"";content:var(--tw-content)}.first\:border-t-0:first-child{border-top-width:0}.first\:pt-0:first-child{padding-top:0}.last\:mb-0:last-child{margin-bottom:0}.last\:border-0:last-child{border-width:0}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05;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-accent\/40:hover{border-color:rgba(64,128,224,.4)}.hover\:border-accent\/50:hover{border-color:rgba(64,128,224,.5)}.hover\:border-fb-accent\/50:hover{border-color:rgba(239,68,68,.5)}.hover\:border-fb-border:hover{--tw-border-opacity:1;border-color:rgb(51 65 85/var(--tw-border-opacity,1))}.hover\:border-fb-border\/70:hover{border-color:rgba(51,65,85,.7)}.hover\:border-fb-primary:hover{--tw-border-opacity:1;border-color:rgb(14 165 233/var(--tw-border-opacity,1))}.hover\:border-fb-primary\/40:hover{border-color:rgba(14,165,233,.4)}.hover\:border-fb-primary\/50:hover{border-color:rgba(14,165,233,.5)}.hover\:border-fb-primary\/60:hover{border-color:rgba(14,165,233,.6)}.hover\:border-gray-600:hover{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity,1))}.hover\:border-red-700:hover{--tw-border-opacity:1;border-color:rgb(185 28 28/var(--tw-border-opacity,1))}.hover\:border-white:hover{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity,1))}.hover\:bg-accent-light:hover{--tw-bg-opacity:1;background-color:rgb(96 160 255/var(--tw-bg-opacity,1))}.hover\:bg-accent\/30:hover{background-color:rgba(64,128,224,.3)}.hover\:bg-black\/70:hover{background-color:rgba(0,0,0,.7)}.hover\:bg-black\/75:hover{background-color:rgba(0,0,0,.75)}.hover\:bg-dark-500:hover{--tw-bg-opacity:1;background-color:rgb(30 30 58/var(--tw-bg-opacity,1))}.hover\:bg-dark-700:hover{--tw-bg-opacity:1;background-color:rgb(16 16 30/var(--tw-bg-opacity,1))}.hover\:bg-dark-700\/70:hover{background-color:rgba(16,16,30,.7)}.hover\:bg-emerald-600:hover{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.hover\:bg-fb-border:hover{--tw-bg-opacity:1;background-color:rgb(51 65 85/var(--tw-bg-opacity,1))}.hover\:bg-fb-card:hover{--tw-bg-opacity:1;background-color:rgb(30 41 59/var(--tw-bg-opacity,1))}.hover\:bg-fb-card\/50:hover{background-color:rgba(30,41,59,.5)}.hover\:bg-fb-card\/60:hover{background-color:rgba(30,41,59,.6)}.hover\:bg-fb-card\/70:hover{background-color:rgba(30,41,59,.7)}.hover\:bg-fb-low\/30:hover{background-color:rgba(239,68,68,.3)}.hover\:bg-fb-primary:hover{--tw-bg-opacity:1;background-color:rgb(14 165 233/var(--tw-bg-opacity,1))}.hover\:bg-fb-primaryHi:hover{--tw-bg-opacity:1;background-color:rgb(56 189 248/var(--tw-bg-opacity,1))}.hover\:bg-gold\/20:hover{background-color:rgba(232,192,64,.2)}.hover\:bg-green-900\/50:hover{background-color:rgba(20,83,45,.5)}.hover\:bg-purple-900\/60:hover{background-color:rgba(88,28,135,.6)}.hover\:bg-red-500:hover{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.hover\:bg-red-500\/10:hover{background-color:rgba(239,68,68,.1)}.hover\:bg-red-900\/30:hover{background-color:rgba(127,29,29,.3)}.hover\:bg-red-900\/50:hover{background-color:rgba(127,29,29,.5)}.hover\:bg-red-900\/60:hover{background-color:rgba(127,29,29,.6)}.hover\:bg-white\/5:hover{background-color:hsla(0,0%,100%,.05)}.hover\:text-accent:hover{--tw-text-opacity:1;color:rgb(64 128 224/var(--tw-text-opacity,1))}.hover\:text-accent-light:hover{--tw-text-opacity:1;color:rgb(96 160 255/var(--tw-text-opacity,1))}.hover\:text-fb-accent:hover{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.hover\:text-fb-primaryHi:hover{--tw-text-opacity:1;color:rgb(56 189 248/var(--tw-text-opacity,1))}.hover\:text-fb-text:hover{--tw-text-opacity:1;color:rgb(248 250 252/var(--tw-text-opacity,1))}.hover\:text-gray-200:hover{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.hover\:text-gray-400:hover{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.hover\:text-red-100:hover{--tw-text-opacity:1;color:rgb(254 226 226/var(--tw-text-opacity,1))}.hover\:text-red-200:hover{--tw-text-opacity:1;color:rgb(254 202 202/var(--tw-text-opacity,1))}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-red-400:hover{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:ring-1:hover{--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)}.hover\:ring-1:hover,.hover\:ring-2:hover{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.hover\:ring-2:hover{--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(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.hover\:ring-fb-primary\/40:hover{--tw-ring-color:rgba(14,165,233,.4)}.hover\:ring-fb-primary\/50:hover{--tw-ring-color:rgba(14,165,233,.5)}.focus\:border-accent:focus{--tw-border-opacity:1;border-color:rgb(64 128 224/var(--tw-border-opacity,1))}.focus\:border-accent\/50:focus{border-color:rgba(64,128,224,.5)}.focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity,1))}.focus\:border-fb-primary:focus{--tw-border-opacity:1;border-color:rgb(14 165 233/var(--tw-border-opacity,1))}.focus\:border-fb-primary\/60:focus{border-color:rgba(14,165,233,.6)}.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)}.focus\:ring-1:focus,.focus\:ring-2:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-2: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(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-accent\/30:focus{--tw-ring-color:rgba(64,128,224,.3)}.focus\:ring-accent\/40:focus{--tw-ring-color:rgba(64,128,224,.4)}.focus\:ring-accent\/60:focus{--tw-ring-color:rgba(64,128,224,.6)}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246/var(--tw-ring-opacity,1))}.focus\:ring-fb-primary:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity,1))}.focus\:ring-gray-500\/40:focus{--tw-ring-color:hsla(220,9%,46%,.4)}.focus\:ring-red-400\/60:focus{--tw-ring-color:hsla(0,91%,71%,.6)}.focus\:ring-sky-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(14 165 233/var(--tw-ring-opacity,1))}.active\:scale-95:active{--tw-scale-x:.95;--tw-scale-y:.95;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))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group[open] .group-open\:rotate-180{--tw-rotate:180deg}.group:hover .group-hover\:scale-105,.group[open] .group-open\:rotate-180{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\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05}.group:hover .group-hover\:text-accent-light{--tw-text-opacity:1;color:rgb(96 160 255/var(--tw-text-opacity,1))}.group:hover .group-hover\:text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.group:hover .group-hover\:text-fb-text{--tw-text-opacity:1;color:rgb(248 250 252/var(--tw-text-opacity,1))}.group:hover .group-hover\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.group:hover .group-hover\:opacity-0{opacity:0}.group:hover .group-hover\:opacity-100{opacity:1}.group:hover .group-hover\:opacity-70{opacity:.7}.peer:checked~.peer-checked\:bg-accent{--tw-bg-opacity:1;background-color:rgb(64 128 224/var(--tw-bg-opacity,1))}.peer:checked~.peer-checked\:after\:translate-x-full:after{content:var(--tw-content);--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))}.peer:checked~.peer-checked\:after\:border-white:after{content:var(--tw-content);--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity,1))}.peer:focus~.peer-focus\:outline-none{outline:2px solid transparent;outline-offset:2px}@media (min-width:640px){.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:w-96{width:24rem}.sm\:w-\[26rem\]{width:26rem}.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\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:text-left{text-align:left}.sm\:text-right{text-align:right}}@media (min-width:768px){.md\:col-span-2{grid-column:span 2/span 2}.md\:-mx-8{margin-left:-2rem;margin-right:-2rem}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-60{width:15rem}.md\:w-auto{width:auto}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-end{align-items:flex-end}.md\:items-center{align-items:center}.md\:p-8{padding:2rem}.md\:px-8{padding-left:2rem;padding-right:2rem}.md\:text-3xl{font-size:1.875rem;line-height:2.25rem}}@media (min-width:1024px){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:grid{display:grid}.lg\:w-96{width:24rem}.lg\:max-w-96{max-width:24rem}.lg\:flex-none{flex:none}.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\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:grid-cols-\[minmax\(9rem\2c 0\.9fr\)_minmax\(12rem\2c 1fr\)_4rem_minmax\(12rem\2c 1fr\)\]{grid-template-columns:minmax(9rem,.9fr) minmax(12rem,1fr) 4rem minmax(12rem,1fr)}.lg\:items-center{align-items:center}.lg\:items-stretch{align-items:stretch}.lg\:justify-end{justify-content:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:gap-16{gap:4rem}}@media (min-width:1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:gap-24{gap:6rem}} \ No newline at end of file diff --git a/static/v3/index.html b/static/v3/index.html index ca3cd88..d9cb6ca 100644 --- a/static/v3/index.html +++ b/static/v3/index.html @@ -681,6 +681,28 @@ + +
+
+
Metadata matching
+
Matches your charts against MusicBrainz in the background to tidy names, years and genres — display only, your files are never modified. Matches at or above the confidence level apply automatically; the rest wait in the library's review queue. Turning this off never disables manual match fixes.
+
+
+ + +
+
+ + +
+
@@ -1118,6 +1140,9 @@ + + diff --git a/static/v3/match-review.js b/static/v3/match-review.js new file mode 100644 index 0000000..bf1a222 --- /dev/null +++ b/static/v3/match-review.js @@ -0,0 +1,414 @@ +// Match-Review UI (P8 — library-metadata design §5/§11). A self-contained +// module: the ambient "⚑ N to review" chip lives in the songs toolbar +// (songs.js renders the element and calls the hooks below); the review MODAL, +// the per-field available/missing detail, and the Settings → Library +// "Metadata matching" card behaviour all live here. +// +// The modal reviews ONE chart at a time (the scraper-review model from +// media-server / emulation-frontend apps): the chart's current metadata — +// with explicit "Missing: …" chips — above the candidate list, each +// candidate carrying "Adds / Shows as" chips, with Skip / Not a match / +// Search instead / Use selected plus ‹ › navigation. +// +// Engagement guardrails (§11): opt-in tool-state, not a score. The chip only +// appears when there is something to review, matching is silent on success +// (no toasts, no sounds — hearing-safe), and nothing here ever writes to +// pack files; a confirmed match only improves the local display cache. +(function () { + 'use strict'; + + const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ( + { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); + const enc = encodeURIComponent; + + function artUrl(song) { + const v = song.mtime ? ('?v=' + Math.floor(song.mtime)) : ''; + return '/api/song/' + enc(song.filename) + '/art' + v; + } + + function fmtDur(sec) { + if (!sec && sec !== 0) return ''; + const s = Math.max(0, Math.round(sec)); + return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); + } + + // ── Ambient chip + the Settings card's status line ─────────────────────── + // songs.js renders `#v3-songs-match-review` (hidden) in its toolbar and + // calls window.__fbMatchReviewChip() after each toolbar build; review + // actions here re-call it. The same fetch feeds the Settings status line. + // Silent on failure — surfaces just stay as they are. + let _chipBusy = false; + async function refreshChip() { + if (_chipBusy) return; + _chipBusy = true; + try { + const r = await fetch('/api/enrichment/status'); + if (!r.ok) return; + const body = await r.json(); + const st = body.states || {}; + const n = st.review || 0; + const chip = document.getElementById('v3-songs-match-review'); + if (chip) { + chip.textContent = '⚑ ' + n + ' to review'; + chip.classList.toggle('hidden', !n); + } + const line = document.getElementById('enrich-status'); + if (line) { + const parts = [ + ((st.matched || 0) + (st.manual || 0)) + ' matched', + n + ' to review', + (st.failed || 0) + ' unmatched', + ]; + if (st.unscanned) parts.push(st.unscanned + ' queued'); + line.textContent = (body.running ? 'Matching… · ' : '') + parts.join(' · '); + } + } catch (_) { /* offline — leave as-is */ } finally { + _chipBusy = false; + } + } + + // ── Review modal (body-appended singleton, one chart at a time) ───────── + let _queue = []; + let _idx = 0; + let _lastFocus = null; + + function ensureModal() { + let m = document.getElementById('v3-match-modal'); + if (m) return m; + const overlay = document.createElement('div'); + overlay.id = 'v3-match-overlay'; + overlay.className = 'fixed inset-0 bg-black/60 z-40 hidden'; + overlay.addEventListener('click', closeModal); + document.body.appendChild(overlay); + m = document.createElement('div'); + m.id = 'v3-match-modal'; + m.className = 'fixed inset-0 z-50 hidden flex items-center justify-center p-4 pointer-events-none'; + m.innerHTML = ''; + m.addEventListener('keydown', onModalKeydown); + document.body.appendChild(m); + return m; + } + + function isTyping(e) { + const t = e.target; + return t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA'); + } + + function onModalKeydown(e) { + if (e.key === 'Escape') { e.stopPropagation(); closeModal(); return; } + if (e.key === 'ArrowLeft' && !isTyping(e)) { e.preventDefault(); nav(-1); return; } + if (e.key === 'ArrowRight' && !isTyping(e)) { e.preventDefault(); nav(1); return; } + if (e.key !== 'Tab') return; + // Light focus trap: cycle within the panel. + const panel = document.getElementById('v3-match-panel'); + if (!panel) return; + const foci = panel.querySelectorAll('button, input, [tabindex="0"]'); + if (!foci.length) return; + const first = foci[0], last = foci[foci.length - 1]; + if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } + else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } + } + + function openModal() { + _lastFocus = document.activeElement; + const m = ensureModal(); + renderLoading(); + m.classList.remove('hidden'); + document.getElementById('v3-match-overlay')?.classList.remove('hidden'); + loadQueue(); + } + + function closeModal() { + document.getElementById('v3-match-modal')?.classList.add('hidden'); + document.getElementById('v3-match-overlay')?.classList.add('hidden'); + refreshChip(); + if (_lastFocus && _lastFocus.isConnected) { try { _lastFocus.focus(); } catch (_) { } } + _lastFocus = null; + } + + function nav(step) { + if (!_queue.length) return; + _idx = Math.min(Math.max(_idx + step, 0), _queue.length - 1); + renderCurrent(); + } + + async function loadQueue() { + try { + const r = await fetch('/api/enrichment/review?limit=200'); + _queue = r.ok ? ((await r.json()).songs || []) : []; + } catch (_) { _queue = []; } + _idx = 0; + renderCurrent(); + } + + function headerHtml() { + const counter = _queue.length + ? '' + + '' + + (_idx + 1) + ' of ' + _queue.length + + '' + : ''; + return '
' + + '

Match review

' + counter + + '
'; + } + + function renderLoading() { + const panel = document.getElementById('v3-match-panel'); + if (!panel) return; + panel.innerHTML = headerHtml() + + '

Loading…

'; + panel.querySelector('[data-mr-close]')?.addEventListener('click', closeModal); + } + + function renderDone() { + const panel = document.getElementById('v3-match-panel'); + if (!panel) return; + panel.innerHTML = headerHtml() + + '

Nothing waiting for review.

' + + '

Medium-confidence matches queue here while the library is matched in the background. Matching options live in Settings → Library.

'; + panel.querySelector('[data-mr-close]')?.addEventListener('click', closeModal); + } + + // Amber "what this chart lacks" chips. Album/year come from the library + // row; cover art is detected from the art request failing (flagged onto + // the song object by the onerror handler, then re-rendered). + function missingChips(song) { + const missing = []; + if (!String(song.album || '').trim()) missing.push('album'); + if (!String(song.year || '').trim()) missing.push('year'); + if (song._artMissing) missing.push('cover art'); + if (!missing.length) return ''; + return '
' + + 'Missing:' + + missing.map((f) => '' + esc(f) + '').join('') + + '
'; + } + + // Per-candidate "what accepting this gets you": fields the chart lacks + // that the candidate supplies, and fields whose DISPLAYED value would + // change (never the file). + function diffChips(song, cand) { + const adds = []; + const changes = []; + const have = (v) => String(v == null ? '' : v).trim(); + const differ = (a, b) => have(a) && have(b) && have(a).toLowerCase() !== have(b).toLowerCase(); + if (have(cand.album)) { if (!have(song.album)) adds.push('album'); else if (differ(song.album, cand.album)) changes.push('album'); } + if (have(cand.year)) { if (!have(song.year)) adds.push('year'); else if (differ(song.year, cand.year)) changes.push('year'); } + if (cand.genres && cand.genres.length) adds.push('genres'); + if (have(cand.isrc)) adds.push('ISRC'); + if (differ(song.artist, cand.artist)) changes.push(have(song.artist) + ' → ' + have(cand.artist)); + if (differ(song.title, cand.title)) changes.push('title'); + let html = ''; + if (adds.length) html += 'Adds: ' + esc(adds.join(' · ')) + ''; + if (changes.length) html += (html ? ' ' : '') + 'Shows as: ' + esc(changes.join(' · ')) + ''; + return html ? '' + html + '' : ''; + } + + function candRowHtml(song, c, i, selected) { + const meta = [c.artist, c.album, c.year, fmtDur(c.duration)].filter(Boolean).join(' · '); + const pct = c.score != null ? Math.round(c.score * 100) + '%' : ''; + return ''; + } + + function renderCurrent() { + const panel = document.getElementById('v3-match-panel'); + if (!panel) return; + if (!_queue.length) { renderDone(); return; } + _idx = Math.min(_idx, _queue.length - 1); + const song = _queue[_idx]; + if (song._sel == null) song._sel = 0; + const sub = [song.artist, song.album, song.year, fmtDur(song.duration)].filter(Boolean).join(' · '); + + panel.innerHTML = headerHtml() + + '
' + + // The chart being matched + '
' + + '' + + '
' + + '
' + esc(song.title) + '
' + + '
' + esc(sub) + '
' + + '
' + esc(song.filename) + '
' + + missingChips(song) + + '
' + + // Candidates + '
' + + '
Candidates (MusicBrainz)
' + + (song.candidates || []).map((c, i) => candRowHtml(song, c, i, i === song._sel)).join('') + + '
' + + // Search-instead panel + '' + + '
' + + // Footer actions + '
' + + '
' + + '' + + '
' + + '
' + + '' + + '' + + '
'; + + wireCurrent(panel, song); + } + + function wireCurrent(panel, song) { + panel.querySelector('[data-mr-close]')?.addEventListener('click', closeModal); + panel.querySelector('[data-mr-prev]')?.addEventListener('click', () => nav(-1)); + panel.querySelector('[data-mr-next]')?.addEventListener('click', () => nav(1)); + panel.querySelector('[data-mr-skip]')?.addEventListener('click', () => nav(1)); + // Art failure → flag + re-render once so the "cover art" chip shows. + const img = panel.querySelector('[data-mr-art]'); + if (img) img.onerror = () => { + img.style.visibility = 'hidden'; + if (!song._artMissing) { song._artMissing = true; renderCurrent(); } + }; + panel.querySelectorAll('[data-mr-cand]').forEach((btn) => { + btn.addEventListener('click', () => { + song._sel = Number(btn.getAttribute('data-mr-cand')); + renderCurrent(); + }); + }); + panel.querySelector('[data-mr-accept]')?.addEventListener('click', async () => { + const cand = (song.candidates || [])[song._sel || 0]; + if (!cand) return; + await post('/api/enrichment/review/' + enc(song.filename) + '/accept', + { recording_id: cand.recording_id }); + settle(song); + }); + panel.querySelector('[data-mr-reject]')?.addEventListener('click', async () => { + await post('/api/enrichment/review/' + enc(song.filename) + '/reject'); + settle(song); + }); + const sp = panel.querySelector('[data-mr-search-panel]'); + const input = panel.querySelector('[data-mr-search-input]'); + panel.querySelector('[data-mr-search-toggle]')?.addEventListener('click', () => { + sp?.classList.toggle('hidden'); + if (sp && !sp.classList.contains('hidden') && input && !input.value) { + input.value = [song.artist, song.title].filter(Boolean).join(' – '); + input.focus(); + } + }); + const go = () => runSearch(panel, song); + panel.querySelector('[data-mr-search-go]')?.addEventListener('click', go); + input?.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); go(); } }); + } + + // Silent-on-success: the chart just leaves the queue and the next one + // renders; the last one renders the done state. No toasts, no sounds. + function settle(song) { + const i = _queue.indexOf(song); + if (i >= 0) _queue.splice(i, 1); + if (_idx >= _queue.length) _idx = Math.max(0, _queue.length - 1); + refreshChip(); + renderCurrent(); + } + + async function runSearch(panel, song) { + const input = panel.querySelector('[data-mr-search-input]'); + const out = panel.querySelector('[data-mr-search-results]'); + if (!input || !out) return; + const qRaw = input.value.trim(); + if (!qRaw) return; + // "Artist – Title" splits on the first dash; a plain phrase searches + // as a title, which MusicBrainz handles well enough. + const m = qRaw.split(/\s+[–—-]\s+/); + const artist = m.length > 1 ? m[0] : ''; + const title = m.length > 1 ? m.slice(1).join(' - ') : qRaw; + out.innerHTML = '

Searching…

'; + let body = null; + try { + const r = await fetch('/api/enrichment/search?artist=' + enc(artist) + + '&title=' + enc(title) + '&filename=' + enc(song.filename)); + if (r.status === 503) { + out.innerHTML = '

MusicBrainz is unavailable — try again later.

'; + return; + } + if (r.ok) body = await r.json(); + } catch (_) { /* falls through to the no-results line */ } + const cands = (body && body.candidates) || []; + if (!cands.length) { + out.innerHTML = '

No results.

'; + return; + } + out.innerHTML = cands.map((c, i) => candRowHtml(song, c, i, false)).join(''); + out.querySelectorAll('[data-mr-cand]').forEach((btn) => { + btn.addEventListener('click', async () => { + const cand = cands[Number(btn.getAttribute('data-mr-cand'))]; + if (!cand) return; + await post('/api/enrichment/review/' + enc(song.filename) + '/pick', + { candidate: cand }); + settle(song); + }); + }); + } + + async function post(url, payload) { + try { + await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload || {}), + }); + } catch (_) { /* offline — the row simply stays queued */ } + } + + // ── Settings → Library → "Metadata matching" card ──────────────────────── + // Markup lives statically in index.html (the v3 settings pattern); this + // wires it. All null-guarded so v2 (which lacks the elements) no-ops. + function wireSettingsCard() { + const toggle = document.getElementById('enrich-enabled'); + const sel = document.getElementById('enrich-threshold'); + const btn = document.getElementById('enrich-match-now'); + if (!toggle && !sel && !btn) return; + (async () => { + try { + const r = await fetch('/api/settings'); + if (r.ok) { + const cfg = await r.json(); + if (toggle) toggle.checked = cfg.enrich_enabled !== false; + if (sel) { + const t = Number(cfg.enrich_auto_threshold); + const want = Number.isFinite(t) ? t : 0.9; + // Snap to the nearest offered option. + let best = sel.options[0]; + for (const o of sel.options) { + if (Math.abs(Number(o.value) - want) < Math.abs(Number(best.value) - want)) best = o; + } + if (best) sel.value = best.value; + } + } + } catch (_) { /* leave markup defaults */ } + refreshChip(); // also fills #enrich-status + })(); + const save = (key, value) => post('/api/settings', { [key]: value }); + toggle?.addEventListener('change', () => save('enrich_enabled', !!toggle.checked)); + sel?.addEventListener('change', () => save('enrich_auto_threshold', Number(sel.value))); + btn?.addEventListener('click', async () => { + await post('/api/enrichment/kick'); + const line = document.getElementById('enrich-status'); + if (line) line.textContent = 'Matching…'; + setTimeout(refreshChip, 1500); + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', wireSettingsCard, { once: true }); + } else { + wireSettingsCard(); + } + + window.__fbMatchReviewChip = refreshChip; + window.__fbOpenMatchReview = openModal; +})(); diff --git a/static/v3/songs.js b/static/v3/songs.js index b36555c..5f6b999 100644 --- a/static/v3/songs.js +++ b/static/v3/songs.js @@ -59,7 +59,7 @@ artist: '', album: '', grouping: true, // one card per song (multi-chart grouping); persisted - filters: { arr_has: [], arr_lacks: [], stem_has: [], stem_lacks: [], lyrics: '', tunings: [], mastery: [] }, + filters: { arr_has: [], arr_lacks: [], stem_has: [], stem_lacks: [], lyrics: '', tunings: [], mastery: [], match: [] }, page: 0, total: 0, loading: false, built: false, accuracy: {}, tuningNames: [], artistCatalog: [], renderedHash: '', scrollBound: false, @@ -110,6 +110,7 @@ const f = state.filters; return f.arr_has.length + f.arr_lacks.length + f.stem_has.length + f.stem_lacks.length + (f.lyrics ? 1 : 0) + f.tunings.length + (f.mastery ? f.mastery.length : 0) + + (f.match ? f.match.length : 0) + (state.artist ? 1 : 0) + (state.album ? 1 : 0); } @@ -133,6 +134,7 @@ lyrics: f.lyrics || '', tunings: [...(f.tunings || [])].sort(), mastery: [...(f.mastery || [])].sort(), + match: [...(f.match || [])].sort(), }, }); } @@ -155,10 +157,15 @@ const f = saved.filters; if (f && typeof f === 'object') { const arr = (x) => (Array.isArray(x) ? x.slice() : []); + // mastery + match are session-only facets (deliberately not + // persisted), but the restored object must still CARRY the keys — + // the filter drawer indexes f.mastery/f.match unconditionally, so + // dropping them here breaks the drawer for anyone with saved prefs. state.filters = { arr_has: arr(f.arr_has), arr_lacks: arr(f.arr_lacks), stem_has: arr(f.stem_has), stem_lacks: arr(f.stem_lacks), lyrics: f.lyrics || '', tunings: arr(f.tunings), + mastery: [], match: [], }; } } @@ -263,6 +270,7 @@ if (f.lyrics) p.set('has_lyrics', f.lyrics); if (f.tunings.length) p.set('tunings', f.tunings.join(',')); if (f.mastery && f.mastery.length) p.set('mastery', f.mastery.join(',')); + if (f.match && f.match.length) p.set('match', f.match.join(',')); Object.entries(extra || {}).forEach(([k, v]) => p.set(k, v)); return p; } @@ -2029,6 +2037,9 @@ section('Lyrics', ['', '1', '0'].map((v) => '').join('')) + // Progress (mastery bands) — multi-select; server filters via song_stats. section('Progress', [['mastered', 'Mastered'], ['in_progress', 'In progress'], ['not_started', 'Not started']].map((it) => '').join('')) + + // Match (P8) — the song's metadata-match lifecycle state, a triage + // facet for the enrichment layer. Session-only, like Progress. + section('Match', [['review', 'To review'], ['matched', 'Matched'], ['unmatched', 'Unmatched'], ['pending', 'Not scanned']].map((it) => '').join('')) + section('Tuning', (state.tuningNames || []).map((t) => { // Filter on the server's grouping key (raw offsets for customs) // so two "Custom Tuning" entries are distinct; show their target @@ -2080,11 +2091,12 @@ renderDrawer(); reload(); // re-fetches grid + rail with/without group=1, saves prefs }); + d.querySelectorAll('[data-match]').forEach((b) => b.addEventListener('click', () => { const v = b.getAttribute('data-match'); const i = f.match.indexOf(v); if (i >= 0) f.match.splice(i, 1); else f.match.push(v); renderDrawer(); })); d.querySelector('[data-drawer-save]')?.addEventListener('click', saveCurrentAsCollection); d.querySelector('[data-drawer-tidy]')?.addEventListener('click', openArtistTidyUp); d.querySelector('[data-drawer-close]')?.addEventListener('click', closeDrawer); d.querySelector('[data-drawer-clear]')?.addEventListener('click', async () => { - state.filters = { arr_has: [], arr_lacks: [], stem_has: [], stem_lacks: [], lyrics: '', tunings: [], mastery: [] }; + state.filters = { arr_has: [], arr_lacks: [], stem_has: [], stem_lacks: [], lyrics: '', tunings: [], mastery: [], match: [] }; state.artist = ''; state.album = ''; renderDrawer(); @@ -2540,7 +2552,12 @@ '
' + '
' + '
' + - '

' + + // The match-review chip is ambient tool-state (design §11): only + // rendered when matches are waiting, silent otherwise. Populated + + // shown by match-review.js (window.__fbMatchReviewChip), which + // also owns the drawer the click opens. + '

' + + '
' + '
' + (providers.length > 1 ? '' : '') + '' + @@ -2599,6 +2616,10 @@ }); byId('v3-songs-filters').addEventListener('click', openDrawer); byId('v3-songs-overlay').addEventListener('click', closeDrawer); + // Match-review chip: feature-detected (match-review.js owns the + // drawer + the count; absent → the chip just stays hidden). + byId('v3-songs-match-review')?.addEventListener('click', () => { if (window.__fbOpenMatchReview) window.__fbOpenMatchReview(); }); + if (window.__fbMatchReviewChip) window.__fbMatchReviewChip(); byId('v3-songs-upload').addEventListener('click', () => { const legacy = document.getElementById('upload-songs-file'); // Upload targets the LOCAL library + scan; watchUploadScan refreshes diff --git a/tests/test_demo_mode.py b/tests/test_demo_mode.py index b2df662..69fb6fe 100644 --- a/tests/test_demo_mode.py +++ b/tests/test_demo_mode.py @@ -91,6 +91,13 @@ def test_demo_off_settings_post_not_blocked(tmp_path, monkeypatch): ("GET", "/api/plugins/updates"), ("POST", "/api/plugins/highway_3d/files"), ("DELETE", "/api/plugins/highway_3d/files"), + # Enrichment (P8): review writes + the MusicBrainz search proxy (the + # proxy would spend the shared rate limit for anonymous demo visitors). + ("POST", "/api/enrichment/review/some-file/accept"), + ("POST", "/api/enrichment/review/some-file/reject"), + ("POST", "/api/enrichment/review/some-file/pick"), + ("POST", "/api/enrichment/kick"), + ("GET", "/api/enrichment/search"), ]) def test_demo_on_blocked_routes_return_403(tmp_path, monkeypatch, method, path): server, client = _make_client(tmp_path, monkeypatch, demo=True) diff --git a/tests/test_mb_enrichment.py b/tests/test_mb_enrichment.py new file mode 100644 index 0000000..525d6be --- /dev/null +++ b/tests/test_mb_enrichment.py @@ -0,0 +1,512 @@ +"""Server-level tests for the P8 MusicBrainz matcher + Match-Review flow. + +The HTTP transport is a fake installed over `server._mb_http_get` — the ONE +seam enrichment uses to reach the network — so nothing here ever opens a +socket. The offline default is itself under test: without explicitly +enabling the network flag, a pass must skip matching entirely (pytest can +never hit MusicBrainz, whatever a test triggers). +""" + +import importlib +import sys + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture() +def server(tmp_path, monkeypatch, isolate_logging): + monkeypatch.setenv("CONFIG_DIR", str(tmp_path / "config")) + dlc = tmp_path / "dlc" + dlc.mkdir() + monkeypatch.setenv("DLC_DIR", str(dlc)) + monkeypatch.setenv("FEEDBACK_SKIP_STARTUP_TASKS", "1") + sys.modules.pop("server", None) + srv = importlib.import_module("server") + try: + yield srv + finally: + conn = getattr(getattr(srv, "meta_db", None), "conn", None) + if conn is not None: + conn.close() + sys.modules.pop("server", None) + + +@pytest.fixture() +def client(server): + return TestClient(server.app) + + +class FakeMB: + """Canned MusicBrainz: records every call, serves per-path responses.""" + + def __init__(self): + self.calls = [] + self.search_response = {"recordings": []} + self.recording_lookups = {} # mbid → recording doc + self.isrc_lookups = {} # isrc → {"recordings": [...]} + self.raise_transport = False + + def __call__(self, path, params): + if self.raise_transport: + raise self._srv.EnrichTransportError("fake network down") + self.calls.append((path, dict(params))) + if path == "recording": + return self.search_response + if path.startswith("recording/"): + return self.recording_lookups.get(path.split("/", 1)[1]) + if path.startswith("isrc/"): + return self.isrc_lookups.get(path.split("/", 1)[1]) + raise AssertionError(f"unexpected MB path {path!r}") + + @property + def search_calls(self): + return [c for c in self.calls if c[0] == "recording"] + + +@pytest.fixture() +def mb(server, monkeypatch): + """Install the fake transport AND enable the network flag (the test env + disables it by default — see test_offline_default_skips_matching).""" + fake = FakeMB() + fake._srv = server + monkeypatch.setattr(server, "_mb_http_get", fake) + monkeypatch.setattr(server, "_enrich_network_enabled", lambda: True) + return fake + + +def _put(server, fn, title="Thunderstruck (v2)", artist="ACDC", album="", + duration=292, year="1990"): + server.meta_db.put(fn, 0, 0, { + "title": title, "artist": artist, "album": album, "year": year, + "duration": duration, "arrangements": [{"name": "Lead", "index": 0}], + }) + + +def mb_doc(rid="rec-1", title="Thunderstruck", artist="AC/DC", artist_id="art-1", + album="The Razors Edge", date="1990-09-24", length_ms=292000, score=100): + return { + "id": rid, "score": score, "title": title, "length": length_ms, + "isrcs": ["AUAP09000045"], + "artist-credit": [{"name": artist, "artist": { + "id": artist_id, "name": artist, "sort-name": artist}}], + "releases": [{"id": "rel-1", "title": album, "status": "Official", + "date": date, "release-group": {"primary-type": "Album"}}], + "tags": [{"name": "hard rock", "count": 7}], + } + + +# ── offline safety (the pytest-never-hits-network contract) ────────────────── + +def test_offline_default_skips_matching(server, monkeypatch): + """Under the test env (FEEDBACK_SKIP_STARTUP_TASKS) a pass stamps hashes + but never matches — even with a transport installed.""" + fake = FakeMB() + fake._srv = server + monkeypatch.setattr(server, "_mb_http_get", fake) + _put(server, "a.sloppak") + server._background_enrich() + assert fake.calls == [] + assert server.meta_db.get_enrichment("a.sloppak")["match_state"] == "unscanned" + + +def test_real_transport_refuses_when_offline(server): + """_mb_http_get itself raises (before any socket) when the network is + disabled — defence in depth under pytest.""" + with pytest.raises(server.EnrichTransportError): + server._mb_http_get("recording", {"query": "x"}) + + +def test_transport_error_pauses_pass_without_burning_attempts(server, mb): + _put(server, "a.sloppak") + _put(server, "b.sloppak", title="Other Song") + mb.raise_transport = True + server._background_enrich() + for fn in ("a.sloppak", "b.sloppak"): + row = server.meta_db.get_enrichment(fn) + assert row["match_state"] == "unscanned" + assert row["attempts"] == 0 + # Network comes back → the next kick matches both. + mb.raise_transport = False + mb.search_response = {"recordings": [mb_doc()]} + server._background_enrich() + assert server.meta_db.get_enrichment("a.sloppak")["match_state"] == "matched" + + +# ── text tiers ──────────────────────────────────────────────────────────────── + +def test_high_confidence_auto_matches_and_settles(server, mb): + _put(server, "a.sloppak") + mb.search_response = {"recordings": [mb_doc()]} + server._background_enrich() + row = server.meta_db.get_enrichment("a.sloppak") + assert row["match_state"] == "matched" + assert row["match_source"] == "text" + assert row["match_score"] >= 0.95 + assert row["mb_recording_id"] == "rec-1" + assert row["canon_artist"] == "AC/DC" + assert row["canon_title"] == "Thunderstruck" + assert row["canon_album"] == "The Razors Edge" + assert row["canon_year"] == "1990" + assert row["genres"] == ["hard rock"] + # Settled: another pass makes NO further network calls… + n = len(mb.calls) + server._background_enrich() + assert len(mb.calls) == n + # …until the identity changes, which re-matches. + _put(server, "a.sloppak", title="Back in Black") + mb.search_response = {"recordings": [mb_doc(rid="rec-2", title="Back in Black", + album="Back in Black", date="1980-07-25")]} + server._background_enrich() + row = server.meta_db.get_enrichment("a.sloppak") + assert row["mb_recording_id"] == "rec-2" + + +def test_medium_confidence_goes_to_review_not_canonical(server, mb): + # Partial artist agreement → medium confidence. + _put(server, "a.sloppak", artist="AC/DC ft Nobody") + mb.search_response = {"recordings": [mb_doc()]} + server._background_enrich() + row = server.meta_db.get_enrichment("a.sloppak") + assert row["match_state"] == "review" + assert row["match_source"] == "text" + # Review before auto-canonicalize: NO canonical values written yet. + assert row["canon_artist"] is None + assert row["mb_recording_id"] is None + assert row["candidates"] and row["candidates"][0]["recording_id"] == "rec-1" + # A review row is settled while its identity is unchanged — no re-query. + n = len(mb.calls) + server._background_enrich() + assert len(mb.calls) == n + + +def test_low_confidence_fails_with_backoff(server, mb): + _put(server, "a.sloppak") + mb.search_response = {"recordings": [mb_doc(rid="rec-x", title="Sunrise", + artist="Norah Jones")]} + server._background_enrich() + row = server.meta_db.get_enrichment("a.sloppak") + assert row["match_state"] == "failed" + assert row["attempts"] == 1 + assert row["last_attempt_at"] is not None + # Immediately after, the backoff hasn't elapsed → no retry, no network. + n = len(mb.calls) + server._background_enrich() + assert len(mb.calls) == n + assert server.meta_db.get_enrichment("a.sloppak")["attempts"] == 1 + # Rewind the clock two hours → eligible again, attempts increments. + with server.meta_db._lock: + server.meta_db.conn.execute( + "UPDATE song_enrichment SET last_attempt_at = last_attempt_at - 7200") + server.meta_db.conn.commit() + server._background_enrich() + assert len(mb.calls) == n + 1 + assert server.meta_db.get_enrichment("a.sloppak")["attempts"] == 2 + + +def test_no_results_fails(server, mb): + _put(server, "a.sloppak") + mb.search_response = {"recordings": []} + server._background_enrich() + assert server.meta_db.get_enrichment("a.sloppak")["match_state"] == "failed" + + +# ── the content-hash match cache ────────────────────────────────────────────── + +def test_cache_hit_copies_match_without_network(server, mb): + _put(server, "a.sloppak") + _put(server, "b.sloppak") # identical identity → same content_hash + mb.search_response = {"recordings": [mb_doc()]} + server._background_enrich() + assert len(mb.search_calls) == 1 # ONE search covered both charts + a = server.meta_db.get_enrichment("a.sloppak") + b = server.meta_db.get_enrichment("b.sloppak") + assert a["match_state"] == b["match_state"] == "matched" + assert {a["match_source"], b["match_source"]} == {"text", "cache"} + assert a["mb_recording_id"] == b["mb_recording_id"] == "rec-1" + + +# ── exact keys from the manifest (tier 0 / tier 1) ──────────────────────────── + +def _write_sloppak_manifest(server, name, extra_yaml=""): + d = server.DLC_DIR / name + d.mkdir(parents=True) + (d / "manifest.yaml").write_text( + "title: Thunderstruck\nartist: AC/DC\nduration: 292\n" + "arrangements: []\nstems: []\n" + extra_yaml, + encoding="utf-8") + + +def test_manifest_mbid_tier0(server, mb): + mbid = "12345678-abcd-4ef0-9876-0123456789ab" + _write_sloppak_manifest(server, "a.sloppak", f"mbid: {mbid}\n") + _put(server, "a.sloppak") + mb.recording_lookups[mbid] = mb_doc(rid=mbid) + server._background_enrich() + row = server.meta_db.get_enrichment("a.sloppak") + assert row["match_state"] == "matched" + assert row["match_source"] == "mbid" + assert row["match_score"] == 1.0 + assert row["mb_recording_id"] == mbid + assert mb.search_calls == [] # trusted key — no text search + + +def test_manifest_isrc_tier1(server, mb): + _write_sloppak_manifest(server, "a.sloppak", "isrc: AUAP09000045\n") + _put(server, "a.sloppak") + mb.isrc_lookups["AUAP09000045"] = {"recordings": [mb_doc()]} + server._background_enrich() + row = server.meta_db.get_enrichment("a.sloppak") + assert row["match_state"] == "matched" + assert row["match_source"] == "isrc" + assert mb.search_calls == [] + + +def test_bad_manifest_mbid_falls_through_to_text(server, mb): + mbid = "12345678-abcd-4ef0-9876-0123456789ab" + _write_sloppak_manifest(server, "a.sloppak", f"mbid: {mbid}\n") + _put(server, "a.sloppak") + mb.recording_lookups.clear() # lookup 404s (typo'd manifest) + mb.search_response = {"recordings": [mb_doc()]} + server._background_enrich() + row = server.meta_db.get_enrichment("a.sloppak") + assert row["match_state"] == "matched" + assert row["match_source"] == "text" + + +# ── manual is sacred ────────────────────────────────────────────────────────── + +def test_manual_never_overwritten_by_matcher(server, mb): + _put(server, "a.sloppak") + server.meta_db.set_enrichment_manual( + "a.sloppak", {"recording_id": "user-pick", "title": "Thunderstruck", + "artist": "AC/DC"}, source="search") + mb.search_response = {"recordings": [mb_doc(rid="machine-pick")]} + server._background_enrich() + row = server.meta_db.get_enrichment("a.sloppak") + assert row["match_state"] == "manual" + assert row["mb_recording_id"] == "user-pick" + # The writer refuses machine writes onto manual outright. + ok = server.meta_db.apply_enrichment_match( + "a.sloppak", row["content_hash"], "matched", source="text", score=1.0, + cand={"recording_id": "machine-pick", "title": "X"}) + assert ok is False + + +# ── review routes ───────────────────────────────────────────────────────────── + +def _seed_review(server, mb, fn="a.sloppak", title="Thunderstruck (v2)"): + # Distinct raw titles give distinct content hashes (else the match cache + # legitimately copies an earlier row instead of running the text tiers). + _put(server, fn, title=title, artist="AC/DC ft Nobody") + mb.search_response = {"recordings": [mb_doc()]} + server._background_enrich() + assert server.meta_db.get_enrichment(fn)["match_state"] == "review" + + +def test_review_queue_route(server, mb, client): + _seed_review(server, mb) + body = client.get("/api/enrichment/review").json() + assert body["total_review"] == 1 + assert body["songs"][0]["filename"] == "a.sloppak" + assert body["songs"][0]["candidates"][0]["recording_id"] == "rec-1" + + +def test_review_accept_route(server, mb, client): + _seed_review(server, mb) + r = client.post("/api/enrichment/review/a.sloppak/accept", + json={"recording_id": "rec-1"}) + assert r.status_code == 200 + row = server.meta_db.get_enrichment("a.sloppak") + assert row["match_state"] == "manual" + assert row["match_source"] == "review" + assert row["canon_artist"] == "AC/DC" + assert client.get("/api/enrichment/review").json()["total_review"] == 0 + # Accepting a candidate that isn't in the stored list → 404. + _seed_review(server, mb, fn="b.sloppak", title="Thunderstruck (Live)") + r = client.post("/api/enrichment/review/b.sloppak/accept", + json={"recording_id": "nope"}) + assert r.status_code == 404 + + +def test_review_reject_route_never_retries(server, mb, client): + _seed_review(server, mb) + r = client.post("/api/enrichment/review/a.sloppak/reject") + assert r.status_code == 200 + row = server.meta_db.get_enrichment("a.sloppak") + assert row["match_state"] == "failed" + assert row["match_source"] == "rejected" + # Rejected rows are excluded from the retry backoff forever… + n = len(mb.calls) + server._background_enrich() + assert len(mb.calls) == n + # …but an identity edit re-queues (the user fixed the metadata). + _put(server, "a.sloppak", artist="AC/DC") + server._background_enrich() + assert server.meta_db.get_enrichment("a.sloppak")["match_state"] == "matched" + # Rejecting a manual row is refused. + r = client.post("/api/enrichment/review/a.sloppak/reject") + assert r.status_code == 200 # matched → rejectable + client.post("/api/enrichment/review/a.sloppak/pick", + json={"candidate": {"recording_id": "rec-9", "title": "T"}}) + r = client.post("/api/enrichment/review/a.sloppak/reject") + assert r.status_code == 404 + + +def test_pick_route_fix_match(server, mb, client): + _put(server, "a.sloppak") + r = client.post("/api/enrichment/review/a.sloppak/pick", json={"candidate": { + "recording_id": "rec-77", "title": "Thunderstruck", "artist": "AC/DC", + "album": "The Razors Edge", "year": "1990", "genres": ["hard rock"], + "junk_key": "dropped"}}) + assert r.status_code == 200 + row = server.meta_db.get_enrichment("a.sloppak") + assert row["match_state"] == "manual" + assert row["match_source"] == "search" + assert row["mb_recording_id"] == "rec-77" + # Malformed candidate → 400; unknown song → 404. + assert client.post("/api/enrichment/review/a.sloppak/pick", + json={"candidate": {"title": "no id"}}).status_code == 400 + assert client.post("/api/enrichment/review/ghost.sloppak/pick", + json={"candidate": {"recording_id": "x", "title": "t"}} + ).status_code == 404 + + +def test_search_proxy(server, mb, client, monkeypatch): + _put(server, "a.sloppak") + mb.search_response = {"recordings": [mb_doc()]} + assert client.get("/api/enrichment/search").status_code == 400 + body = client.get("/api/enrichment/search", + params={"title": "Thunderstruck", "artist": "AC/DC", + "filename": "a.sloppak"}).json() + assert body["candidates"][0]["recording_id"] == "rec-1" + assert body["candidates"][0]["score"] > 0.9 + # Transport failure surfaces as 503, not a 500. + def _down(path, params): + raise server.EnrichTransportError("down") + monkeypatch.setattr(server, "_mb_http_get", _down) + r = client.get("/api/enrichment/search", params={"title": "x"}) + assert r.status_code == 503 + + +# ── the match facet on /api/library + /api/library/stats ───────────────────── + +def test_match_facet_filters_grid_and_stats(server, mb, client, monkeypatch): + _put(server, "auto.sloppak") # → matched + _put(server, "rev.sloppak", title="Revsong", artist="AC/DC ft Nobody") # → review + _put(server, "fail.sloppak", title="Failsong", artist="Zzz") # → failed + _put(server, "pend.sloppak", title="Pendsong", artist="Yyy") # stays unscanned + + def _routed(path, params): + q = params.get("query", "") + if "thunderstruck" in q: + return {"recordings": [mb_doc()]} + if "revsong" in q: + return {"recordings": [mb_doc(rid="rec-r", title="Revsong")]} + return {"recordings": []} + monkeypatch.setattr(server, "_mb_http_get", _routed) + server._background_enrich() + # Pendsong got failed by the pass (no results); reset it to unscanned to + # represent the not-yet-scanned band. + with server.meta_db._lock: + server.meta_db.conn.execute( + "UPDATE song_enrichment SET match_state='unscanned', attempts=0, " + "last_attempt_at=NULL WHERE filename='pend.sloppak'") + server.meta_db.conn.commit() + + def names(match): + return sorted(s["filename"] for s in client.get( + "/api/library", params={"match": match, "size": 50}).json()["songs"]) + + assert names("review") == ["rev.sloppak"] + assert names("matched") == ["auto.sloppak"] + assert names("unmatched") == ["fail.sloppak"] + assert names("pending") == ["pend.sloppak"] + assert names("review,matched") == ["auto.sloppak", "rev.sloppak"] + # Stats agree with the grid (the rail's lockstep contract). + total = client.get("/api/library/stats", + params={"match": "review"}).json()["total_songs"] + assert total == 1 + # Unknown values are ignored → unfiltered. + assert len(names("bogus")) == 4 + + +def test_status_counts_by_state(server, mb, client): + _seed_review(server, mb) + states = client.get("/api/enrichment/status").json()["states"] + assert states.get("review") == 1 + + +# ── settings: enable toggle + auto-apply confidence ─────────────────────────── + +def test_auto_threshold_setting_moves_the_auto_review_boundary(server, mb, client): + # artist exact (1.0) + title 4/5 token overlap (0.8) and NO year/duration + # corroboration → combined exactly 0.90. + mb.search_response = {"recordings": [ + mb_doc(title="Highway Hell", artist="AC/DC", date="", length_ms=None)]} + client.post("/api/settings", json={"enrich_auto_threshold": 0.95}) + _put(server, "a.sloppak", title="Highway to Hell", artist="AC/DC", + year="", duration=0) + server._background_enrich() + assert server.meta_db.get_enrichment("a.sloppak")["match_state"] == "review" + # Lower the bar to the default 0.90 → an identity edit re-queues, and the + # same 0.90-scored candidate now auto-applies. + client.post("/api/settings", json={"enrich_auto_threshold": 0.9}) + _put(server, "a.sloppak", title="Highway to Hell", artist="AC/DC", + year="", duration=0, album="Different Album") + server._background_enrich() + row = server.meta_db.get_enrichment("a.sloppak") + assert row["match_state"] == "matched" + assert abs(row["match_score"] - 0.9) < 1e-6 + + +def test_enrich_enabled_setting_gates_background_matching(server, mb, client): + client.post("/api/settings", json={"enrich_enabled": False}) + _put(server, "a.sloppak") + mb.search_response = {"recordings": [mb_doc()]} + server._background_enrich() + assert mb.calls == [] + assert server.meta_db.get_enrichment("a.sloppak")["match_state"] == "unscanned" + # Manual search/fix stays available while the background matcher is off. + r = client.get("/api/enrichment/search", params={"title": "Thunderstruck"}) + assert r.status_code == 200 + # Re-enable → the next pass matches. + client.post("/api/settings", json={"enrich_enabled": True}) + server._background_enrich() + assert server.meta_db.get_enrichment("a.sloppak")["match_state"] == "matched" + + +def test_settings_validation(server, client): + assert "error" in client.post( + "/api/settings", json={"enrich_enabled": "yes"}).json() + assert "error" in client.post( + "/api/settings", json={"enrich_auto_threshold": "high"}).json() + assert "error" in client.post( + "/api/settings", json={"enrich_auto_threshold": 2.5}).json() + ok = client.post("/api/settings", json={"enrich_auto_threshold": 1.01}).json() + assert "error" not in ok + assert client.get("/api/settings").json()["enrich_auto_threshold"] == 1.01 + + +def test_kick_route(server, mb, client): + import time as _t + body = client.post("/api/enrichment/kick").json() + assert "started" in body + # Let the kicked pass settle so its daemon thread can't bleed into the + # fixture teardown (the DB connection closes there). + for _ in range(200): + if not client.get("/api/enrichment/status").json()["running"]: + break + _t.sleep(0.02) + + +def test_review_queue_orders_missing_data_first(server, mb, client): + # Complete chart first alphabetically, incomplete second — the queue must + # surface the incomplete (missing album + year) one first anyway. + _seed_review(server, mb, fn="aa.sloppak", title="Thunderstruck (v2)") + _put(server, "zz.sloppak", title="Thunderstruck (Live)", + artist="AC/DC ft Nobody", album="", year="") + server._background_enrich() + assert server.meta_db.get_enrichment("zz.sloppak")["match_state"] == "review" + songs = client.get("/api/enrichment/review").json()["songs"] + assert [s["filename"] for s in songs] == ["zz.sloppak", "aa.sloppak"] diff --git a/tests/test_mb_match.py b/tests/test_mb_match.py new file mode 100644 index 0000000..b2912c9 --- /dev/null +++ b/tests/test_mb_match.py @@ -0,0 +1,227 @@ +"""Unit tests for lib/mb_match.py — the pure text-matching engine (P8). + +No network, no database, no server import: denoise/tokenize, similarity, +scoring + tier classification, Lucene query building, and MusicBrainz +response parsing are all exercised as plain functions. +""" + +import mb_match as m + + +# ── denoise / tokenize ──────────────────────────────────────────────────────── + +def test_denoise_lowercases_and_strips_punct_and_diacritics(): + assert m.denoise("Motörhead") == "motorhead" + assert m.denoise("Beyoncé!!") == "beyonce" + assert m.denoise("Guns N' Roses") == "guns n roses" + assert m.denoise(" Weird spacing ") == "weird spacing" + + +def test_denoise_strips_noise_parentheticals(): + # The design's explicit list: author suffixes + (440Hz)/(Live)/(No Lead)/(v2). + assert m.denoise("Thunderstruck (440Hz)") == "thunderstruck" + assert m.denoise("Thunderstruck (Live)") == "thunderstruck" + assert m.denoise("Thunderstruck (No Lead)") == "thunderstruck" + assert m.denoise("Thunderstruck (v2)") == "thunderstruck" + assert m.denoise("Thunderstruck [Remastered 2012]") == "thunderstruck" + assert m.denoise("One (Live at Wembley)") == "one" + + +def test_denoise_strips_author_credits(): + assert m.denoise("Back in Black (by SomeCharter)") == "back in black" + assert m.denoise("Back in Black (charted by X99)") == "back in black" + assert m.denoise("Back in Black - by SomeCharter") == "back in black" + + +def test_denoise_keeps_meaningful_parentheticals(): + # A parenthetical with no noise term survives (both sides get the same + # treatment, so symmetric content still matches). + assert m.denoise("Doin' It (All for My Baby)") == "doin it all for my baby" + + +def test_denoise_leading_the_is_artist_only(): + assert m.denoise("The Beatles", strip_leading_the=True) == "beatles" + # Titles keep their "The" — never strip it there. + assert m.denoise("The Trooper") == "the trooper" + + +def test_ampersand_folds_to_and(): + assert m.similarity("Angus & Julia Stone", "Angus and Julia Stone", artist=True) == 1.0 + + +# ── similarity ──────────────────────────────────────────────────────────────── + +def test_similarity_exact_and_empty(): + assert m.similarity("Back in Black", "Back In Black!") == 1.0 + assert m.similarity("", "Anything") == 0.0 + assert m.similarity(None, None) == 0.0 + + +def test_similarity_folds_spelling_drift_via_compaction(): + # The headline case: ACDC / AC DC / AC/DC all name the same artist. + assert m.similarity("ACDC", "AC/DC", artist=True) == 1.0 + assert m.similarity("AC DC", "ACDC", artist=True) == 1.0 + assert m.similarity("Greenday", "Green Day", artist=True) == 1.0 + + +def test_similarity_partial_overlap(): + s = m.similarity("Highway to Hell", "Highway Hell") + assert 0.7 < s < 1.0 + assert m.similarity("Back in Black", "Paint It Black") < 0.5 + + +# ── scoring + tiers ─────────────────────────────────────────────────────────── + +SONG = {"artist": "ACDC", "title": "Thunderstruck (v2)", "album": "The Razors Edge", + "year": "1990", "duration": 292} + + +def test_score_exact_match_is_high(): + cand = {"artist": "AC/DC", "title": "Thunderstruck", "year": "1990", "duration": 292} + s = m.score_candidate(SONG, cand) + assert s == 1.0 + assert m.classify(SONG, cand, s) == "auto" + + +def test_score_cover_never_auto(): + # Perfect title, wrong artist (a cover) — must not auto-match. + cand = {"artist": "Some Cover Band", "title": "Thunderstruck"} + s = m.score_candidate(SONG, cand) + assert m.classify(SONG, cand, s) != "auto" + + +def test_missing_artist_caps_at_review(): + song = {"artist": "", "title": "Thunderstruck", "duration": 292} + cand = {"artist": "AC/DC", "title": "Thunderstruck", "duration": 292} + s = m.score_candidate(song, cand) + # artist half scores 0 → combined ≤ 0.55 + bonuses → review at best. + assert m.classify(song, cand, s) != "auto" + + +def test_year_and_duration_corroborate(): + # Fuzzy title so the base sits below the 1.0 cap and bonuses are visible. + base = {"artist": "AC/DC", "title": "Thunderstruck Thunder"} + plain = m.score_candidate(SONG, base) + with_year = m.score_candidate(SONG, dict(base, year="1990")) + with_dur = m.score_candidate(SONG, dict(base, duration=290)) + assert with_year > plain + assert with_dur > plain + + +def test_fuzzy_title_with_corroboration_lands_review_or_auto(): + cand = {"artist": "AC/DC", "title": "Thunderstruck Thunder"} + s = m.score_candidate(SONG, cand) + assert m.classify(SONG, cand, s) in ("review", "auto") + + +def test_unrelated_is_none(): + cand = {"artist": "Norah Jones", "title": "Sunrise"} + s = m.score_candidate(SONG, cand) + assert m.classify(SONG, cand, s) == "none" + + +def test_classify_auto_min_override(): + # The host's "auto-apply confidence" setting: a perfect match autos at + # any real threshold, and "Always review" (>1.0) sends even it to review. + cand = {"artist": "AC/DC", "title": "Thunderstruck", "year": "1990", "duration": 292} + s = m.score_candidate(SONG, cand) + assert s == 1.0 + assert m.classify(SONG, cand, s, auto_min=0.9) == "auto" + assert m.classify(SONG, cand, s, auto_min=1.01) == "review" + # The per-field floors are independent of the threshold: a wrong-artist + # cover stays non-auto even at a permissive auto_min. + cover = {"artist": "Some Cover Band", "title": "Thunderstruck"} + cs = m.score_candidate(SONG, cover) + assert m.classify(SONG, cover, cs, auto_min=0.5) != "auto" + + +def test_rank_candidates_orders_by_our_score(): + cands = [ + {"recording_id": "b", "artist": "Someone Else", "title": "Thunderstruck", "mb_score": 100}, + {"recording_id": "a", "artist": "AC/DC", "title": "Thunderstruck", "mb_score": 90}, + ] + ranked = m.rank_candidates(SONG, cands) + assert [c["recording_id"] for c in ranked] == ["a", "b"] + assert all("score" in c for c in ranked) + + +# ── query building ──────────────────────────────────────────────────────────── + +def test_build_recording_query_denoises_and_quotes(): + q = m.build_recording_query("ACDC", 'Thunderstruck (v2)') + assert q == 'recording:"thunderstruck" AND artist:"acdc"' + + +def test_build_recording_query_escapes_and_handles_missing_artist(): + q = m.build_recording_query("", 'Say "Hello"') + # Quotes are punct-stripped by denoise, so nothing to escape here — but + # the artist clause must be absent entirely. + assert q.startswith('recording:"') + assert "artist:" not in q + + +# ── MusicBrainz response parsing ────────────────────────────────────────────── + +MB_DOC = { + "id": "rec-123", + "score": 98, + "title": "Thunderstruck", + "length": 292773, + "isrcs": ["AUAP09000045"], + "artist-credit": [ + {"name": "AC/DC", "joinphrase": "", + "artist": {"id": "art-1", "name": "AC/DC", "sort-name": "AC/DC"}}, + ], + "releases": [ + {"id": "rel-compilation", "title": "Greatest Hits", "status": "Official", + "date": "2005-01-01", "release-group": {"primary-type": "Compilation"}}, + {"id": "rel-album", "title": "The Razors Edge", "status": "Official", + "date": "1990-09-24", "release-group": {"primary-type": "Album"}}, + {"id": "rel-boot", "title": "Bootleg", "status": "Bootleg", + "date": "1989-01-01", "release-group": {"primary-type": "Album"}}, + ], + "tags": [{"name": "hard rock", "count": 10}, {"name": "rock", "count": 4}], +} + + +def test_parse_recording_doc_normalizes(): + c = m.parse_recording_doc(MB_DOC) + assert c["recording_id"] == "rec-123" + assert c["title"] == "Thunderstruck" + assert c["artist"] == "AC/DC" + assert c["artist_id"] == "art-1" + # Official Album beats the compilation and the bootleg. + assert c["album"] == "The Razors Edge" + assert c["release_id"] == "rel-album" + assert c["year"] == "1990" + assert c["duration"] == 293 + assert c["isrc"] == "AUAP09000045" + assert c["genres"] == ["hard rock", "rock"] + assert c["mb_score"] == 98 + + +def test_parse_recording_doc_joined_artist_credit(): + doc = dict(MB_DOC) + doc["artist-credit"] = [ + {"name": "Queen", "joinphrase": " & ", + "artist": {"id": "q", "name": "Queen", "sort-name": "Queen"}}, + {"name": "David Bowie", + "artist": {"id": "b", "name": "David Bowie", "sort-name": "Bowie, David"}}, + ] + c = m.parse_recording_doc(doc) + assert c["artist"] == "Queen & David Bowie" + assert c["artist_id"] == "q" + + +def test_parse_recording_doc_rejects_malformed(): + assert m.parse_recording_doc({}) is None + assert m.parse_recording_doc({"id": "x"}) is None + assert m.parse_recording_doc(None) is None + + +def test_parse_search_response(): + body = {"recordings": [MB_DOC, {"bogus": True}]} + cands = m.parse_search_response(body) + assert len(cands) == 1 + assert m.parse_search_response({}) == [] + assert m.parse_search_response(None) == []