From 69c39ee800fdeb25afca8ca2ffc908167a097763 Mon Sep 17 00:00:00 2001 From: QueryPlanner Date: Thu, 20 Aug 2026 21:47:34 +0530 Subject: [PATCH] feat: add per-user model cost dashboard - Capture exact, upstream, and estimated LiteLLM costs - Aggregate idempotent cumulative and UTC-month ledger totals - Add dashboard cost metrics and regression coverage --- docs/architecture.md | 4 +- docs/base-infra/environment-variables.md | 1 + docs/base-infra/observability.md | 12 + src/blacki/agent.py | 3 + src/blacki/callbacks.py | 23 ++ src/blacki/dashboard/data.py | 482 +++++++++++++++++++++- src/blacki/dashboard/models.py | 5 + src/blacki/dashboard/routes.py | 2 + src/blacki/dashboard/static/dashboard.css | 2 +- src/blacki/dashboard/static/dashboard.js | 67 ++- src/blacki/dashboard/templates/index.html | 22 +- src/blacki/llm_costs.py | 293 +++++++++++++ src/blacki/usage_ledger.py | 414 +++++++++++++++++++ tests/dashboard/test_assets.py | 24 ++ tests/dashboard/test_data.py | 293 ++++++++++++- tests/dashboard/test_routes.py | 1 + tests/test_integration.py | 11 + tests/test_llm_costs.py | 268 ++++++++++++ tests/test_logging_callbacks.py | 22 + tests/test_usage_ledger.py | 205 +++++++++ 20 files changed, 2141 insertions(+), 13 deletions(-) create mode 100644 src/blacki/llm_costs.py create mode 100644 src/blacki/usage_ledger.py create mode 100644 tests/test_llm_costs.py create mode 100644 tests/test_usage_ledger.py diff --git a/docs/architecture.md b/docs/architecture.md index 8241ece..f793104 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -16,7 +16,9 @@ python -m blacki.server Telegram long polling, and exposes `/live`, `/ready`, and `/health`. It also mounts the package-backed private observability dashboard at `/dashboard` with APIs for aggregate statistics, users, sessions, local logs, -and local traces. +and local traces. LiteLLM-backed requests additionally write content-free +usage and cost records to the local SQLite ledger so the dashboard can show +per-user and per-session spend without parsing prompts or responses. `src/blacki/agent.py` creates the `LlmAgent`, selects a native Gemini model or a LiteLLM/OpenRouter model from the environment, registers tools, and assembles diff --git a/docs/base-infra/environment-variables.md b/docs/base-infra/environment-variables.md index 965de62..9c8dead 100644 --- a/docs/base-infra/environment-variables.md +++ b/docs/base-infra/environment-variables.md @@ -52,6 +52,7 @@ surface on every host interface. | `ALLOW_ORIGINS` | local origins JSON | JSON array of CORS origins | | `AGENT_ENGINE` | unset | Optional Agent Engine identifier | | `SQLITE_PATH` | `{AGENT_DIR}/.adk/tools.db` | SQLite file for application tools | +| `BLACKI_COST_LEDGER_PATH` | `{AGENT_DIR}/.adk/costs.db` | Content-free model usage and cost ledger | For `ALLOW_ORIGINS`, use a JSON array string: diff --git a/docs/base-infra/observability.md b/docs/base-infra/observability.md index 5ab37b3..ee7098b 100644 --- a/docs/base-infra/observability.md +++ b/docs/base-infra/observability.md @@ -121,6 +121,18 @@ Telegram `/reset` starts the next versioned ADK session and leaves earlier session rows available. The append-only log and trace files are not deleted by `/reset`, so the dashboard continues to show that history. +For LiteLLM-backed requests, Blacki also writes a content-free SQLite usage +ledger at `{AGENT_DIR}/.adk/costs.db` (override with +`BLACKI_COST_LEDGER_PATH`). The ledger stores identity, model, token, provider +response, and fixed-point cost fields, but never prompts, responses, or tool +arguments. Provider-reported OpenRouter account cost and upstream inference +cost are kept separately; a LiteLLM catalog calculation is labelled estimated. +The dashboard uses UTC calendar months for monthly totals and averages. The +average is across users with an exact or estimated cost in the current month; +users with unavailable cost are excluded and the reported coverage is shown. +Records created before cost capture, or responses without a provider cost, +remain unavailable rather than being treated as zero. + This is an admin-only, private-data surface. The application does not add HTTP Basic auth, cookies, or Tailscale identity-header authentication. For direct tailnet access, set `HOST_BIND_IP` to the host's Tailscale IPv4 address diff --git a/src/blacki/agent.py b/src/blacki/agent.py index 762b7e2..4871077 100644 --- a/src/blacki/agent.py +++ b/src/blacki/agent.py @@ -176,6 +176,9 @@ def _build_model() -> str | LiteLlm: litellm_kwargs: dict[str, Any] = {} if model_name.lower().startswith("openrouter/") and openrouter_api_key: litellm_kwargs["api_key"] = openrouter_api_key + from .llm_costs import CostAwareLiteLLMClient + + litellm_kwargs["llm_client"] = CostAwareLiteLLMClient() logger.info("Using LiteLlm for model: %s", model_name) return LiteLlm(model=model_name, **litellm_kwargs) diff --git a/src/blacki/callbacks.py b/src/blacki/callbacks.py index 06d280d..4a19d80 100644 --- a/src/blacki/callbacks.py +++ b/src/blacki/callbacks.py @@ -19,6 +19,7 @@ from google.adk.tools import ToolContext from google.adk.tools.base_tool import BaseTool +from .llm_costs import attach_cost_metadata, begin_cost_capture from .privacy import is_private_tool, private_tool_privacy_enabled from .telegram.api import TelegramApiClient, TelegramApiError from .telegram.formatting import format_for_telegram @@ -531,6 +532,12 @@ def before_model( f"*** Before LLM call for agent '{callback_context.agent_name}' " f"with invocation_id '{callback_context.invocation_id}' ***" ) + session = getattr(callback_context, "session", None) + begin_cost_capture( + user_id=getattr(callback_context, "user_id", None), + session_id=getattr(session, "id", None), + invocation_id=getattr(callback_context, "invocation_id", None), + ) self.logger.debug(f"State keys: {callback_context.state.to_dict().keys()}") redact_content = private_tool_privacy_enabled() @@ -578,6 +585,22 @@ def after_model( response_data = llm_content.model_dump(exclude_none=True, mode="json") self.logger.debug(f"LLM response: {response_data}") + cost_observation = attach_cost_metadata(llm_response) + if cost_observation: + self.logger.debug( + "LLM cost captured: %s", + { + key: cost_observation[key] + for key in ( + "cost_usd", + "upstream_cost_usd", + "estimated_cost_usd", + "cost_kind", + ) + if key in cost_observation + }, + ) + return None def before_tool( diff --git a/src/blacki/dashboard/data.py b/src/blacki/dashboard/data.py index 99a4f11..b1662b1 100644 --- a/src/blacki/dashboard/data.py +++ b/src/blacki/dashboard/data.py @@ -22,6 +22,12 @@ from typing import Any from urllib.parse import quote +from ..usage_ledger import ( + LedgerSummary, + UsageLedgerSnapshot, + fixed_cost_to_usd, + read_usage_ledger, +) from .models import ( DEGRADED_DATABASE_WARNING, MAX_ACCEPTED_JSONL_RECORDS, @@ -93,8 +99,12 @@ "llm.token_count.total", "llm.token_count.prompt", "llm.token_count.completion", + "gen_ai.usage.cost", + "gen_ai.usage.cost_estimate", + "gen_ai.cost.upstream_inference_cost", } ) +COST_METADATA_KEY = "blacki.cost" @dataclass(frozen=True, slots=True) @@ -735,6 +745,215 @@ def _event_usage(event: _EventRow) -> tuple[int, int, int]: return input_tokens, output_tokens, total_tokens +def _money_value(value: Any) -> float | None: + if value is None or isinstance(value, bool): + return None + try: + number = float(value) + except (TypeError, ValueError, OverflowError): + return None + if not math.isfinite(number) or number < 0: + return None + return round(number, 9) + + +def _event_cost(event: _EventRow) -> dict[str, Any] | None: + metadata = event.data.get("custom_metadata") or event.data.get("customMetadata") + if isinstance(metadata, str): + metadata, malformed = _parse_json_object(metadata) + if malformed: + return None + if not isinstance(metadata, Mapping): + return None + raw_cost = metadata.get(COST_METADATA_KEY) or metadata.get("blacki_cost") + if raw_cost is None and any( + key in metadata + for key in ( + "cost_usd", + "upstream_cost_usd", + "estimated_cost_usd", + "cost_kind", + ) + ): + raw_cost = metadata + if isinstance(raw_cost, str): + raw_cost, malformed = _parse_json_object(raw_cost) + if malformed: + return None + if not isinstance(raw_cost, Mapping): + return None + cost = _money_value(raw_cost.get("cost_usd", raw_cost.get("cost"))) + upstream = _money_value( + raw_cost.get( + "upstream_cost_usd", + raw_cost.get("upstream_inference_cost"), + ) + ) + estimated = _money_value( + raw_cost.get("estimated_cost_usd", raw_cost.get("estimate")) + ) + if cost is None and upstream is None and estimated is None: + cost_kind = str(raw_cost.get("cost_kind", "unknown")) + if cost_kind != "unknown": + return None + return { + "cost_usd": cost, + "upstream_cost_usd": upstream, + "estimated_cost_usd": estimated, + "cost_kind": str(raw_cost.get("cost_kind", "reported")), + "cost_source": str(raw_cost.get("cost_source", "event_metadata")), + } + + +def _event_cost_summary( + events: Iterable[_EventRow], + *, + since: float | None = None, + until: float | None = None, +) -> dict[str, Any]: + if until is None: + until = datetime.now(tz=UTC).timestamp() + selected = [event for event in events if _in_window(event.timestamp, since, until)] + records = 0 + reported_records = 0 + estimated_records = 0 + input_tokens = output_tokens = total_tokens = 0 + costs: list[float] = [] + upstream_costs: list[float] = [] + estimates: list[float] = [] + for event in selected: + input_count, output_count, total_count = _event_usage(event) + if input_count or output_count or total_count: + input_tokens += input_count + output_tokens += output_count + total_tokens += total_count + cost = _event_cost(event) + if cost is None: + continue + records += 1 + if cost["cost_usd"] is not None: + reported_records += 1 + costs.append(cost["cost_usd"]) + if cost["estimated_cost_usd"] is not None: + estimated_records += 1 + estimates.append(cost["estimated_cost_usd"]) + if cost["upstream_cost_usd"] is not None: + upstream_costs.append(cost["upstream_cost_usd"]) + usage_records = sum(1 for event in selected if any(_event_usage(event))) + return { + "records": records, + "reported_records": reported_records, + "estimated_records": estimated_records, + "usage_records": usage_records, + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "total_tokens": total_tokens, + "cost_usd": round(sum(costs), 9) if costs else None, + "upstream_cost_usd": ( + round(sum(upstream_costs), 9) if upstream_costs else None + ), + "estimated_cost_usd": round(sum(estimates), 9) if estimates else None, + } + + +def _ledger_cost_summary(summary: LedgerSummary) -> dict[str, Any]: + return { + "records": summary.records, + "reported_records": summary.reported_records, + "estimated_records": summary.estimated_records, + "usage_records": summary.records, + "input_tokens": summary.input_tokens, + "output_tokens": summary.output_tokens, + "total_tokens": summary.total_tokens, + "cost_usd": fixed_cost_to_usd(summary.cost_nano_usd), + "upstream_cost_usd": fixed_cost_to_usd(summary.upstream_cost_nano_usd), + "estimated_cost_usd": fixed_cost_to_usd(summary.estimated_cost_nano_usd), + } + + +def _cost_coverage(summary: Mapping[str, Any]) -> float | None: + denominator = max( + int(summary.get("records", 0)), + int(summary.get("usage_records", 0)), + ) + if denominator <= 0: + return None + return round(int(summary.get("reported_records", 0)) / denominator, 4) + + +def _cost_fields( + cumulative: Mapping[str, Any], + monthly: Mapping[str, Any], +) -> JsonObject: + cost = cumulative.get("cost_usd") + estimated = cumulative.get("estimated_cost_usd") + if cost is not None: + source = "reported" + elif estimated is not None: + source = "estimated" + elif cumulative.get("upstream_cost_usd") is not None: + source = "upstream_only" + elif cumulative.get("usage_records", 0): + source = "unavailable" + else: + source = "none" + return { + "cost_usd": cost, + "cumulative_cost_usd": cost, + "monthly_cost_usd": monthly.get("cost_usd"), + "provider_cost_usd": cumulative.get("upstream_cost_usd"), + "monthly_provider_cost_usd": monthly.get("upstream_cost_usd"), + "estimated_cost_usd": estimated, + "monthly_estimated_cost_usd": monthly.get("estimated_cost_usd"), + "cost_records": cumulative.get("reported_records", 0), + "usage_records": cumulative.get("usage_records", 0), + "cost_coverage": _cost_coverage(cumulative), + "cost_source": source, + } + + +def _average_monthly_cost(groups: Mapping[str, Mapping[str, Any]]) -> float | None: + values = [ + float(summary["cost_usd"]) + if summary.get("cost_usd") is not None + else float(summary["estimated_cost_usd"]) + for summary in groups.values() + if summary.get("cost_usd") is not None + or summary.get("estimated_cost_usd") is not None + ] + return round(sum(values) / len(values), 9) if values else None + + +def _month_start_timestamp(now: float) -> float: + current = datetime.fromtimestamp(now, tz=UTC) + return datetime(current.year, current.month, 1, tzinfo=UTC).timestamp() + + +def _group_event_costs( + events: Iterable[_EventRow], + *, + group_key: str, + month_start: float, + now: float, +) -> tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any]]]: + grouped: dict[str, list[_EventRow]] = defaultdict(list) + for event in events: + key = ( + event.user_id + if group_key == "user_id" + else f"{event.user_id}\x00{event.session_id}" + ) + grouped[key].append(event) + cumulative = { + key: _event_cost_summary(items, until=now) for key, items in grouped.items() + } + monthly = { + key: _event_cost_summary(items, since=month_start, until=now) + for key, items in grouped.items() + } + return cumulative, monthly + + def _event_is_error(event: _EventRow) -> bool: if event.data.get("error_code") or event.data.get("errorCode"): return True @@ -835,10 +1054,20 @@ def _session_item( *, is_current: bool, retained_session_ids: list[str], + cost_fields: Mapping[str, Any] | None = None, ) -> JsonObject: event_list = list(events) _, version = _session_version(session.session_id) stats = _event_stats(event_list) + if cost_fields is None: + now = datetime.now(tz=UTC).timestamp() + cumulative_cost = _event_cost_summary(event_list, until=now) + monthly_cost = _event_cost_summary( + event_list, + since=_month_start_timestamp(now), + until=now, + ) + cost_fields = _cost_fields(cumulative_cost, monthly_cost) reset_generation = max(0, version - 1) if version is not None else None return { "id": session.session_id, @@ -860,6 +1089,7 @@ def _session_item( "tokens": stats["tokens"], "error_count": stats["errors"], "latency_ms": stats["latency_ms"], + **cost_fields, } @@ -1074,6 +1304,7 @@ def _trace_item(record: dict[str, Any]) -> JsonObject | None: "attributes": _allowlisted_attributes(raw_event.get("attributes")), } events.append(event_item) + attributes = _allowlisted_attributes(record.get("attributes")) return { "trace_id": trace_id, "span_id": span_id if isinstance(span_id, str) else None, @@ -1086,7 +1317,15 @@ def _trace_item(record: dict[str, Any]) -> JsonObject | None: "status": _trace_status(record), "span_count": 1, "spans": 1, - "attributes": _allowlisted_attributes(record.get("attributes")), + "attributes": attributes, + "cost_usd": _money_value(attributes.get("gen_ai.usage.cost")), + "estimated_cost_usd": _money_value( + attributes.get("gen_ai.usage.cost_estimate") + ), + "upstream_cost_usd": _money_value( + attributes.get("gen_ai.cost.upstream_inference_cost") + ), + "cost_source": attributes.get("gen_ai.cost.source"), "events": events, } @@ -1140,6 +1379,21 @@ def _trace_summary(spans: list[JsonObject]) -> JsonObject: raw_attributes = span.get("attributes") if isinstance(raw_attributes, Mapping): attributes.update(raw_attributes) + cost_values = [ + value + for span in spans + if (value := _money_value(span.get("cost_usd"))) is not None + ] + estimated_values = [ + value + for span in spans + if (value := _money_value(span.get("estimated_cost_usd"))) is not None + ] + upstream_values = [ + value + for span in spans + if (value := _money_value(span.get("upstream_cost_usd"))) is not None + ] return { "trace_id": latest.get("trace_id"), "span_id": latest.get("span_id"), @@ -1153,6 +1407,13 @@ def _trace_summary(spans: list[JsonObject]) -> JsonObject: "span_count": len(spans), "spans": len(spans), "attributes": attributes, + "cost_usd": round(sum(cost_values), 9) if cost_values else None, + "estimated_cost_usd": ( + round(sum(estimated_values), 9) if estimated_values else None + ), + "upstream_cost_usd": ( + round(sum(upstream_values), 9) if upstream_values else None + ), "events": [], } @@ -1198,11 +1459,17 @@ def __init__( log_dir: Path, app_name: str = "blacki", identity_db_path: Path | None = None, + cost_ledger_path: Path | None = None, ) -> None: self.session_db_path = Path(session_db_path) self.log_dir = Path(log_dir) self.app_name = str(app_name) self.identity_db_path = Path(identity_db_path) if identity_db_path else None + self.cost_ledger_path = ( + Path(cost_ledger_path) + if cost_ledger_path is not None + else self.log_dir / "blacki-costs.db" + ) async def _snapshot(self) -> _Snapshot: return await asyncio.to_thread( @@ -1226,6 +1493,22 @@ def _events_by_session( ) return grouped + async def _cost_ledger( + self, + *, + selected_since: float | None, + selected_until: float, + ) -> UsageLedgerSnapshot: + now = datetime.now(tz=UTC).timestamp() + return await asyncio.to_thread( + read_usage_ledger, + self.cost_ledger_path, + selected_since=selected_since, + selected_until=selected_until, + month_start=_month_start_timestamp(now), + now=now, + ) + async def get_overview(self, window: str) -> JsonObject: snapshot, log_scan, trace_scan = await asyncio.gather( self._snapshot(), @@ -1263,6 +1546,91 @@ async def get_overview(self, window: str) -> JsonObject: selected_users = {session.user_id for session in selected_sessions} selected_users.update(event.user_id for event in selected_events) stats = _event_stats(selected_events) + now = datetime.now(tz=UTC).timestamp() + month_start = _month_start_timestamp(now) + cost_ledger = await self._cost_ledger( + selected_since=since, + selected_until=until, + ) + fallback_selected_cost = _event_cost_summary( + selected_events, + since=since, + until=until, + ) + fallback_cumulative_cost = _event_cost_summary( + snapshot.events, + until=until, + ) + fallback_monthly_cost = _event_cost_summary( + snapshot.events, + since=month_start, + until=until, + ) + _, fallback_monthly_user_costs = _group_event_costs( + snapshot.events, + group_key="user_id", + month_start=month_start, + now=until, + ) + if cost_ledger.available: + selected_cost = _ledger_cost_summary(cost_ledger.selected) + cumulative_cost = _ledger_cost_summary(cost_ledger.cumulative) + monthly_cost = _ledger_cost_summary(cost_ledger.monthly) + monthly_user_costs = { + key: _ledger_cost_summary(value) + for key, value in cost_ledger.monthly_users.items() + } + else: + selected_cost = fallback_selected_cost + cumulative_cost = fallback_cumulative_cost + monthly_cost = fallback_monthly_cost + monthly_user_costs = fallback_monthly_user_costs + stats["cost_usd"] = selected_cost["cost_usd"] + stats["cumulative_cost_usd"] = cumulative_cost["cost_usd"] + stats["monthly_cost_usd"] = monthly_cost["cost_usd"] + stats["provider_cost_usd"] = selected_cost["upstream_cost_usd"] + stats["cumulative_provider_cost_usd"] = cumulative_cost["upstream_cost_usd"] + stats["monthly_provider_cost_usd"] = monthly_cost["upstream_cost_usd"] + stats["estimated_cost_usd"] = selected_cost["estimated_cost_usd"] + stats["cumulative_estimated_cost_usd"] = cumulative_cost["estimated_cost_usd"] + stats["monthly_estimated_cost_usd"] = monthly_cost["estimated_cost_usd"] + stats["cost_records"] = selected_cost["reported_records"] + stats["cost_usage_records"] = selected_cost["usage_records"] + stats["cost_coverage"] = _cost_coverage(selected_cost) + stats["cost_kind"] = ( + "reported" + if selected_cost["cost_usd"] is not None + else "estimated" + if selected_cost["estimated_cost_usd"] is not None + else "unavailable" + if selected_cost["usage_records"] + else "none" + ) + stats["monthly_cost_users"] = sum( + 1 + for summary in monthly_user_costs.values() + if summary.get("cost_usd") is not None + or summary.get("estimated_cost_usd") is not None + ) + stats["average_user_monthly_cost_usd"] = _average_monthly_cost( + monthly_user_costs + ) + stats["cost_available"] = cost_ledger.available or bool( + selected_cost["records"] + ) + stats["cost"] = { + "window_usd": selected_cost["cost_usd"], + "cumulative_usd": cumulative_cost["cost_usd"], + "monthly_usd": monthly_cost["cost_usd"], + "estimated_window_usd": selected_cost["estimated_cost_usd"], + "estimated_cumulative_usd": cumulative_cost["estimated_cost_usd"], + "estimated_monthly_usd": monthly_cost["estimated_cost_usd"], + "average_user_monthly_usd": stats["average_user_monthly_cost_usd"], + "monthly_users": stats["monthly_cost_users"], + "coverage": stats["cost_coverage"], + "kind": stats["cost_kind"], + "source": "ledger" if cost_ledger.available else "event_metadata", + } selected_log_items = [ item for item in log_items @@ -1319,6 +1687,7 @@ async def get_overview(self, window: str) -> JsonObject: warnings.extend(_scan_warning(log_scan, missing=MISSING_TELEMETRY_WARNING)) warnings.extend(_scan_warning(trace_scan, missing=MISSING_TRACES_WARNING)) warnings.extend(window_warnings) + warnings.extend(cost_ledger.warnings) warnings = list(dict.fromkeys(warnings)) result: JsonObject = { "window": normalized_window, @@ -1346,6 +1715,13 @@ async def get_overview(self, window: str) -> JsonObject: "requests", "tokens", "errors", + "cost_usd", + "cumulative_cost_usd", + "monthly_cost_usd", + "estimated_cost_usd", + "cumulative_estimated_cost_usd", + "monthly_estimated_cost_usd", + "average_user_monthly_cost_usd", ): result[key] = stats[key] result["error_rate"] = stats["error_rate"] @@ -1354,6 +1730,18 @@ async def get_overview(self, window: str) -> JsonObject: async def list_users(self, search: str, limit: int, offset: int) -> JsonObject: snapshot = await self._snapshot() + now = datetime.now(tz=UTC).timestamp() + month_start = _month_start_timestamp(now) + cost_ledger = await self._cost_ledger( + selected_since=None, + selected_until=now, + ) + fallback_user_costs, fallback_monthly_user_costs = _group_event_costs( + snapshot.events, + group_key="user_id", + month_start=month_start, + now=now, + ) identities = ( await asyncio.to_thread( _load_telegram_identities_sync, self.identity_db_path @@ -1367,6 +1755,7 @@ async def list_users(self, search: str, limit: int, offset: int) -> JsonObject: user_ids = sorted( {session.user_id for session in snapshot.sessions} | {event.user_id for event in snapshot.events} + | (set(cost_ledger.users) if cost_ledger.available else set()) ) items: list[JsonObject] = [] for user_id in user_ids: @@ -1393,6 +1782,25 @@ async def list_users(self, search: str, limit: int, offset: int) -> JsonObject: sessions.sort(key=_session_sort_key, reverse=True) events = [event for event in snapshot.events if event.user_id == user_id] stats = _event_stats(events) + if cost_ledger.available: + cumulative_cost = _ledger_cost_summary( + cost_ledger.users.get(user_id, LedgerSummary()) + ) + monthly_cost = _ledger_cost_summary( + cost_ledger.monthly_users.get(user_id, LedgerSummary()) + ) + else: + cumulative_cost = fallback_user_costs.get( + user_id, _event_cost_summary(events, until=now) + ) + monthly_cost = fallback_monthly_user_costs.get( + user_id, + _event_cost_summary( + events, + since=month_start, + until=now, + ), + ) latest = sessions[0] if sessions else None versions = [ version @@ -1417,9 +1825,11 @@ async def list_users(self, search: str, limit: int, offset: int) -> JsonObject: "invocation_count": stats["invocations"], "tokens": stats["tokens"], "error_count": stats["errors"], + **_cost_fields(cumulative_cost, monthly_cost), } ) warnings = list(snapshot.warnings) + warnings.extend(cost_ledger.warnings) return page_result( items[page_offset : page_offset + page_limit], total=len(items), @@ -1430,6 +1840,18 @@ async def list_users(self, search: str, limit: int, offset: int) -> JsonObject: async def list_sessions(self, user_id: str, limit: int, offset: int) -> JsonObject: snapshot = await self._snapshot() + now = datetime.now(tz=UTC).timestamp() + month_start = _month_start_timestamp(now) + cost_ledger = await self._cost_ledger( + selected_since=None, + selected_until=now, + ) + fallback_session_costs, fallback_monthly_session_costs = _group_event_costs( + snapshot.events, + group_key="session_id", + month_start=month_start, + now=now, + ) page_limit = clamp_limit(limit) page_offset = clamp_offset(offset) selected = [ @@ -1445,15 +1867,46 @@ async def list_sessions(self, user_id: str, limit: int, offset: int) -> JsonObje grouped.get((user_id, session.session_id), []), is_current=session.session_id == latest, retained_session_ids=retained_ids, + cost_fields=_cost_fields( + _ledger_cost_summary( + cost_ledger.sessions.get( + (user_id, session.session_id), LedgerSummary() + ) + ) + if cost_ledger.available + else fallback_session_costs.get( + f"{user_id}\x00{session.session_id}", + _event_cost_summary( + grouped.get((user_id, session.session_id), []), + until=now, + ), + ), + _ledger_cost_summary( + cost_ledger.monthly_sessions.get( + (user_id, session.session_id), LedgerSummary() + ) + ) + if cost_ledger.available + else fallback_monthly_session_costs.get( + f"{user_id}\x00{session.session_id}", + _event_cost_summary( + grouped.get((user_id, session.session_id), []), + since=month_start, + until=now, + ), + ), + ), ) for session in selected ] + warnings = list(snapshot.warnings) + warnings.extend(cost_ledger.warnings) return page_result( items[page_offset : page_offset + page_limit], total=len(items), limit=page_limit, offset=page_offset, - warnings=list(snapshot.warnings), + warnings=warnings, ) async def get_session(self, user_id: str, session_id: str) -> JsonObject | None: @@ -1481,17 +1934,40 @@ async def get_session(self, user_id: str, session_id: str) -> JsonObject | None: selected_events = _session_events( snapshot.events, user_id=user_id, session_id=session_id ) + now = datetime.now(tz=UTC).timestamp() + month_start = _month_start_timestamp(now) + cost_ledger = await self._cost_ledger( + selected_since=None, + selected_until=now, + ) + if cost_ledger.available: + cumulative_cost = _ledger_cost_summary( + cost_ledger.sessions.get((user_id, session_id), LedgerSummary()) + ) + monthly_cost = _ledger_cost_summary( + cost_ledger.monthly_sessions.get((user_id, session_id), LedgerSummary()) + ) + else: + cumulative_cost = _event_cost_summary(selected_events, until=now) + monthly_cost = _event_cost_summary( + selected_events, + since=month_start, + until=now, + ) item = _session_item( session, selected_events, is_current=session.session_id == latest, retained_session_ids=retained_ids, + cost_fields=_cost_fields(cumulative_cost, monthly_cost), ) item["messages"] = [ message for event in selected_events for message in _event_messages(event) ] item["degraded"] = bool(snapshot.warnings) - item["warnings"] = list(snapshot.warnings) + warnings = list(snapshot.warnings) + warnings.extend(cost_ledger.warnings) + item["warnings"] = warnings return item async def list_logs(self, level: str | None, search: str, limit: int) -> JsonObject: diff --git a/src/blacki/dashboard/models.py b/src/blacki/dashboard/models.py index 87a3dfa..69d81ba 100644 --- a/src/blacki/dashboard/models.py +++ b/src/blacki/dashboard/models.py @@ -50,6 +50,11 @@ "gen_ai.usage.completion_tokens", "gen_ai.usage.prompt_token_count", "gen_ai.usage.candidates_token_count", + "gen_ai.usage.cost", + "gen_ai.usage.cost_estimate", + "gen_ai.cost.upstream_inference_cost", + "gen_ai.cost.source", + "gen_ai.cost.kind", "llm.token_count.input", "llm.token_count.output", "llm.token_count.total", diff --git a/src/blacki/dashboard/routes.py b/src/blacki/dashboard/routes.py index 3fb64af..2601858 100644 --- a/src/blacki/dashboard/routes.py +++ b/src/blacki/dashboard/routes.py @@ -24,6 +24,7 @@ from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse, Response +from ..usage_ledger import default_usage_ledger_path from ..utils.config import ServerEnv from ..utils.observability import get_log_dir from .data import DashboardStore @@ -245,6 +246,7 @@ def create_dashboard_router( get_log_dir(), "blacki", _tools_db_path(env), + default_usage_ledger_path(env.agent_dir), ) except Exception: logger.exception("Dashboard store initialization failed") diff --git a/src/blacki/dashboard/static/dashboard.css b/src/blacki/dashboard/static/dashboard.css index 7fd17c8..8128b2b 100644 --- a/src/blacki/dashboard/static/dashboard.css +++ b/src/blacki/dashboard/static/dashboard.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-black:#000;--spacing:.25rem;--breakpoint-2xl:96rem;--container-xl:36rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-black:900;--tracking-tight:-.025em;--tracking-wide:.025em;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}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;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}@media (prefers-color-scheme:dark){:root:not([data-theme]){color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}}:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=dark]:checked),[data-theme=dark]{color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root{--fx-noise:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.34' numOctaves='4' stitchTiles='stitch'%3E%3C/feTurbulence%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23a)' opacity='0.2'%3E%3C/rect%3E%3C/svg%3E");scrollbar-color:currentColor #0000}@supports (color:color-mix(in lab, red, red)){:root{scrollbar-color:color-mix(in oklch,currentColor 35%,#0000)#0000}}@property --radialprogress{syntax: ""; inherits: true; initial-value: 0%;}:root:not(span){overflow:var(--page-overflow)}:root{--page-scroll-bg-on:linear-gradient(var(--root-bg),var(--root-bg))var(--root-bg)}@supports (color:color-mix(in lab, red, red)){:root{--page-scroll-bg-on:linear-gradient(var(--root-bg),var(--root-bg))color-mix(in srgb,var(--root-bg),oklch(0% 0 0) calc(var(--page-has-backdrop,0)*40%))}}:root{--page-scroll-transition-on:background-color .3s ease-out;transition:var(--page-scroll-transition);scrollbar-gutter:var(--page-scroll-gutter,unset);scrollbar-gutter:if(style(--page-has-scroll: 1): var(--page-scroll-gutter,unset); else: unset)}:root:root{background:var(--page-scroll-bg,var(--root-bg,var(--color-base-100)))}@keyframes set-page-has-scroll{0%,to{--page-has-scroll:1}}:root,[data-theme]{background-color:var(--root-bg,var(--color-base-100));color:var(--color-base-content)}:where(:root,[data-theme]){--root-bg:var(--color-base-100)}}@layer components;@layer utilities{@layer daisyui.l1.l2.l3{.modal{pointer-events:none;visibility:hidden;width:100%;max-width:none;height:100%;max-height:none;color:inherit;transition:visibility .3s allow-discrete,background-color .3s ease-out,opacity .1s ease-out;overscroll-behavior:contain;z-index:999;scrollbar-gutter:auto;background-color:#0000;place-items:center;margin:0;padding:0;display:grid;position:fixed;inset:0;overflow:clip}.modal::backdrop{display:none}.tab{cursor:pointer;appearance:none;text-align:center;webkit-user-select:none;-webkit-user-select:none;user-select:none;flex-wrap:wrap;justify-content:center;align-items:center;display:inline-flex;position:relative}@media (hover:hover){.tab:hover{color:var(--color-base-content)}}.tab{--tab-p:.75rem;--tab-bg:var(--color-base-100);--tab-border-color:var(--color-base-300);--tab-radius-ss:0;--tab-radius-se:0;--tab-radius-es:0;--tab-radius-ee:0;--tab-order:0;--tab-radius-min:calc(.75rem - var(--border));--tab-radius-limit:min(var(--radius-field),var(--tab-radius-min));--tab-radius-grad:#0000 calc(69% - var(--border)),var(--tab-border-color)calc(calc(69% - var(--border)) + .25px),var(--tab-border-color)calc(calc(69% - var(--border)) + var(--border)),var(--tab-bg)calc(calc(69% - var(--border)) + var(--border) + .25px);order:var(--tab-order);height:var(--tab-height);border-color:#0000;padding-inline-start:var(--tab-p);padding-inline-end:var(--tab-p);font-size:.875rem}.tab:is(input[type=radio]){min-width:fit-content}.tab:is(input[type=radio]):after{--tw-content:attr(aria-label);content:var(--tw-content)}.tab:is(label){position:relative}.tab:is(label) input{cursor:pointer;appearance:none;opacity:0;position:absolute;inset:0}:is(.tab:checked,.tab:is(label:has(:checked)),.tab:is(.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]))+.tab-content{display:block}.tab:not(:checked,label:has(:checked),:hover,.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]){color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.tab:not(:checked,label:has(:checked),:hover,.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]){color:color-mix(in oklab,var(--color-base-content)50%,transparent)}}.tab:not(input):empty{cursor:default;flex-grow:1}.tab:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.tab:focus{outline-offset:2px;outline:2px solid #0000}}.tab:focus-visible,.tab:is(label:has(:checked:focus-visible)){outline-offset:-5px;outline:2px solid}.tab[disabled]{pointer-events:none;opacity:.4}.menu{--menu-active-fg:var(--color-neutral-content);--menu-active-bg:var(--color-neutral);flex-flow:column wrap;width:fit-content;padding:.5rem;font-size:.875rem;display:flex}.menu :where(li ul){white-space:nowrap;margin-inline-start:1rem;padding-inline-start:.5rem;position:relative}.menu :where(li ul):before{background-color:var(--color-base-content);opacity:.1;width:var(--border);content:"";inset-inline-start:0;position:absolute;top:.75rem;bottom:.75rem}.menu :where(li>.menu-dropdown:not(.menu-dropdown-show)){display:none}.menu :where(li:not(.menu-title)>:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--radius-field);text-align:start;text-wrap:balance;-webkit-user-select:none;user-select:none;grid-auto-columns:minmax(auto,max-content) auto max-content;grid-auto-flow:column;align-content:flex-start;align-items:center;gap:.5rem;padding-block:.375rem;padding-inline:.75rem;transition-property:color,background-color,box-shadow;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);display:grid}.menu :where(li>details>summary){--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li>details>summary){outline-offset:2px;outline:2px solid #0000}}.menu :where(li>details>summary)::-webkit-details-marker{display:none}:is(.menu :where(li>details>summary),.menu :where(li>.menu-dropdown-toggle)):after{content:"";transform-origin:50%;pointer-events:none;justify-self:flex-end;width:.375rem;height:.375rem;transition-property:rotate,translate;transition-duration:.2s;display:block;translate:0 -1px;rotate:-135deg;box-shadow:inset 2px 2px}.menu details{interpolate-size:allow-keywords;overflow:hidden}.menu details::details-content{block-size:0}@media (prefers-reduced-motion:no-preference){.menu details::details-content{transition-behavior:allow-discrete;transition-property:block-size,content-visibility;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}}.menu details[open]::details-content{block-size:auto}.menu :where(li>details[open]>summary):after,.menu :where(li>.menu-dropdown-toggle.menu-dropdown-show):after{translate:0 1px;rotate:45deg}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{background-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{color:var(--color-base-content);--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{outline-offset:2px;outline:2px solid #0000}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){background-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){outline-offset:2px;outline:2px solid #0000}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){box-shadow:inset 0 1px oklch(0% 0 0/.01),inset 0 -1px oklch(100% 0 0/.01)}.menu :where(li:empty){background-color:var(--color-base-content);opacity:.1;height:1px;margin:.5rem 1rem}.menu :where(li){flex-flow:column wrap;flex-shrink:0;align-items:stretch;display:flex;position:relative}.menu :where(li) .badge{justify-self:flex-end}.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{outline-offset:2px;outline:2px solid #0000}}.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{color:var(--menu-active-fg);background-color:var(--menu-active-bg);background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise)}:is(.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active):not(:is(.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active):active){box-shadow:0 2px calc(var(--depth)*3px)-2px var(--menu-active-bg)}.menu :where(li).menu-disabled{pointer-events:none;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li).menu-disabled{color:color-mix(in oklab,var(--color-base-content)20%,transparent)}}.menu .dropdown:focus-within .menu-dropdown-toggle:after{translate:0 1px;rotate:45deg}.menu .dropdown-content{margin-top:.5rem;padding:.5rem}.menu .dropdown-content:before{display:none}.dropdown{position-area:var(--anchor-v,bottom)var(--anchor-h,span-right);display:inline-block;position:relative}.dropdown>:not(:has(~[class*=dropdown-content])):focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.dropdown>:not(:has(~[class*=dropdown-content])):focus{outline-offset:2px;outline:2px solid #0000}}.dropdown .dropdown-content{position:absolute}.dropdown.dropdown-close .dropdown-content,.dropdown:not(details,.dropdown-open,.dropdown-hover:hover,:focus-within) .dropdown-content,.dropdown.dropdown-hover:not(:hover) [tabindex]:first-child:focus:not(:focus-visible)~.dropdown-content{transform-origin:top;opacity:0;display:none;scale:95%}.dropdown[popover],.dropdown .dropdown-content{z-index:999}@media (prefers-reduced-motion:no-preference){.dropdown[popover],.dropdown .dropdown-content{transition-behavior:allow-discrete;transition-property:opacity,scale,display;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);animation:.2s dropdown}}@starting-style{.dropdown[popover],.dropdown .dropdown-content{opacity:0;scale:95%}}:is(.dropdown:not(.dropdown-close).dropdown-open,.dropdown:not(.dropdown-close):not(.dropdown-hover):focus,.dropdown:not(.dropdown-close):focus-within)>[tabindex]:first-child{pointer-events:none}:is(.dropdown:not(.dropdown-close).dropdown-open,.dropdown:not(.dropdown-close):not(.dropdown-hover):focus,.dropdown:not(.dropdown-close):focus-within) .dropdown-content,.dropdown:not(.dropdown-close).dropdown-hover:hover .dropdown-content{opacity:1;scale:100%}.dropdown:is(details) summary::-webkit-details-marker{display:none}.dropdown:where([popover]){background:0 0}.dropdown[popover]{color:inherit;position:fixed}@supports not (position-area:bottom){.dropdown[popover]{margin:auto}.dropdown[popover].dropdown-close{transform-origin:top;opacity:0;display:none;scale:95%}.dropdown[popover].dropdown-open:not(:popover-open){transform-origin:top;opacity:0;display:none;scale:95%}.dropdown[popover]::backdrop{background-color:oklab(0% none none/.3)}}:is(.dropdown[popover].dropdown-close,.dropdown[popover]:not(.dropdown-open,:popover-open)){transform-origin:top;opacity:0;display:none;scale:95%}:where(.btn){width:unset}.btn{cursor:pointer;text-align:center;vertical-align:middle;outline-offset:2px;webkit-user-select:none;-webkit-user-select:none;user-select:none;padding-inline:var(--btn-p);color:var(--btn-fg);--tw-prose-links:var(--btn-fg);height:var(--size);font-size:var(--fontsize,.875rem);outline-color:var(--btn-color,var(--color-base-content));background-color:var(--btn-bg);background-size:auto,calc(var(--noise)*100%);background-image:none,var(--btn-noise);border-width:var(--border);border-style:solid;border-color:var(--btn-border);text-shadow:0 .5px oklch(100% 0 0/calc(var(--depth)*.15));touch-action:manipulation;box-shadow:0 .5px 0 .5px oklch(100% 0 0/calc(var(--depth)*6%))inset,var(--btn-shadow);--size:calc(var(--size-field,.25rem)*10);--btn-bg:var(--btn-color,var(--color-base-200));--btn-fg:var(--color-base-content);--btn-p:1rem;--btn-border:var(--btn-bg);border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-wrap:nowrap;flex-shrink:0;justify-content:center;align-items:center;gap:.375rem;font-weight:600;transition-property:color,background-color,border-color,box-shadow;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);display:inline-flex}@supports (color:color-mix(in lab, red, red)){.btn{--btn-border:color-mix(in oklab,var(--btn-bg),#000 calc(var(--depth)*5%))}}.btn{--btn-shadow:0 3px 2px -2px var(--btn-bg),0 4px 3px -2px var(--btn-bg)}@supports (color:color-mix(in lab, red, red)){.btn{--btn-shadow:0 3px 2px -2px color-mix(in oklab,var(--btn-bg)calc(var(--depth)*30%),#0000),0 4px 3px -2px color-mix(in oklab,var(--btn-bg)calc(var(--depth)*30%),#0000)}}.btn{--btn-noise:var(--fx-noise)}@media (hover:hover){.btn:hover{--btn-bg:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn:hover{--btn-bg:color-mix(in oklab,var(--btn-color,var(--color-base-200)),#000 7%)}}}.btn:focus-visible,.btn:has(:focus-visible){isolation:isolate;outline-width:2px;outline-style:solid}.btn:active:not(.btn-active){--btn-bg:var(--btn-color,var(--color-base-200));translate:0 .5px}@supports (color:color-mix(in lab, red, red)){.btn:active:not(.btn-active){--btn-bg:color-mix(in oklab,var(--btn-color,var(--color-base-200)),#000 5%)}}.btn:active:not(.btn-active){--btn-border:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn:active:not(.btn-active){--btn-border:color-mix(in oklab,var(--btn-color,var(--color-base-200)),#000 7%)}}.btn:active:not(.btn-active){--btn-shadow:0 0 0 0 oklch(0% 0 0/0),0 0 0 0 oklch(0% 0 0/0)}.btn:is(input[type=checkbox],input[type=radio]){appearance:none}.btn:is(input[type=checkbox],input[type=radio])[aria-label]:after{--tw-content:attr(aria-label);content:var(--tw-content)}.btn:where(input:checked:not(.filter .btn)){--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content);isolation:isolate}.loading{pointer-events:none;aspect-ratio:1;vertical-align:middle;width:calc(var(--size-selector,.25rem)*6);background-color:currentColor;display:inline-block;-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:100%;mask-size:100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.list{flex-direction:column;font-size:.875rem;display:flex}.list .list-row{--list-grid-cols:minmax(0,auto)1fr;border-radius:var(--radius-box);word-break:break-word;grid-auto-flow:column;grid-template-columns:var(--list-grid-cols);gap:1rem;padding:1rem;display:grid;position:relative}:is(.list>:not(:last-child).list-row,.list>:not(:last-child) .list-row):after{content:"";border-bottom:var(--border)solid;inset-inline:var(--radius-box);border-color:var(--color-base-content);position:absolute;bottom:0}@supports (color:color-mix(in lab, red, red)){:is(.list>:not(:last-child).list-row,.list>:not(:last-child) .list-row):after{border-color:color-mix(in oklab,var(--color-base-content)5%,transparent)}}.toggle{border:var(--border)solid currentColor;color:var(--input-color);cursor:pointer;appearance:none;vertical-align:middle;webkit-user-select:none;-webkit-user-select:none;user-select:none;--radius-selector-max:calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector));border-radius:calc(var(--radius-selector) + min(var(--toggle-p),var(--radius-selector-max)) + min(var(--border),var(--radius-selector-max)));padding:var(--toggle-p);flex-shrink:0;grid-template-columns:0fr 1fr 1fr;place-content:center;display:inline-grid;position:relative;box-shadow:inset 0 1px}@supports (color:color-mix(in lab, red, red)){.toggle{box-shadow:0 1px color-mix(in oklab,currentColor calc(var(--depth)*10%),#0000)inset}}.toggle{--input-color:var(--color-base-content);transition:color .3s,grid-template-columns .2s}@supports (color:color-mix(in lab, red, red)){.toggle{--input-color:color-mix(in oklab,var(--color-base-content)50%,#0000)}}.toggle{--toggle-p:calc(var(--size)*.125);--size:calc(var(--size-selector,.25rem)*6);width:calc((var(--size)*2) - (var(--border) + var(--toggle-p))*2);height:var(--size)}.toggle>*{z-index:1;cursor:pointer;appearance:none;background-color:#0000;border:none;grid-column:2/span 1;grid-row-start:1;height:100%;padding:.125rem;transition:opacity .2s,rotate .4s}.toggle>:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.toggle>:focus{outline-offset:2px;outline:2px solid #0000}}.toggle>:nth-child(2){color:var(--color-base-100);rotate:none}.toggle>:nth-child(3){color:var(--color-base-100);opacity:0;rotate:-15deg}.toggle:has(:checked)>:nth-child(2){opacity:0;rotate:15deg}.toggle:has(:checked)>:nth-child(3){opacity:1;rotate:none}.toggle:before{aspect-ratio:1;border-radius:var(--radius-selector);--tw-content:"";content:var(--tw-content);height:100%;box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px currentColor;background-color:currentColor;grid-row-start:1;grid-column-start:2;transition:background-color .1s,translate .2s,inset-inline-start .2s;position:relative;inset-inline-start:0;translate:0}@supports (color:color-mix(in lab, red, red)){.toggle:before{box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px color-mix(in oklab,currentColor calc(var(--depth)*10%),#0000)}}.toggle:before{background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise)}@media (forced-colors:active){.toggle:before{outline-style:var(--tw-outline-style);outline-offset:calc(1px*-1);outline-width:1px}}@media print{.toggle:before{outline-offset:-1rem;outline:.25rem solid}}.toggle:focus-visible,.toggle:has(:focus-visible){outline-offset:2px;outline:2px solid}.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked){background-color:var(--color-base-100);--input-color:var(--color-base-content);grid-template-columns:1fr 1fr 0fr}:is(.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked)):before{background-color:currentColor}@starting-style{:is(.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked)):before{opacity:0}}.toggle:indeterminate{grid-template-columns:.5fr 1fr .5fr}.toggle:disabled{cursor:not-allowed;opacity:.3}.toggle:disabled:before{border:var(--border)solid currentColor;background-color:#0000}.input{cursor:text;border:var(--border)solid #0000;appearance:none;background-color:var(--color-base-100);vertical-align:middle;white-space:nowrap;width:clamp(3rem,20rem,100%);height:var(--size);font-size:max(var(--font-size,.875rem),.875rem);touch-action:manipulation;border-color:var(--input-color);box-shadow:0 1px var(--input-color)inset,0 -1px oklch(100% 0 0/calc(var(--depth)*.1))inset;border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-shrink:1;align-items:center;gap:.5rem;padding-inline:.75rem;display:inline-flex;position:relative}@supports (color:color-mix(in lab, red, red)){.input{box-shadow:0 1px color-mix(in oklab,var(--input-color)calc(var(--depth)*10%),#0000)inset,0 -1px oklch(100% 0 0/calc(var(--depth)*.1))inset}}.input{--size:calc(var(--size-field,.25rem)*10);--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.input{--input-color:color-mix(in oklab,var(--color-base-content)20%,#0000)}}.input:where(input){display:inline-flex}.input :where(input){appearance:none;background-color:#0000;border:none;width:100%;height:100%;display:inline-flex}.input :where(input):focus,.input :where(input):focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.input :where(input):focus,.input :where(input):focus-within{outline-offset:2px;outline:2px solid #0000}}.input :where(input[type=url]),.input :where(input[type=email]){direction:ltr}.input :where(input[type=date]){display:inline-flex}.input:focus,.input:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.input:focus,.input:focus-within{box-shadow:0 1px color-mix(in oklab,var(--input-color)calc(var(--depth)*10%),#0000)}}.input:focus,.input:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate;z-index:1}@media (pointer:coarse){@supports (-webkit-touch-callout:none){.input:focus,.input:focus-within{--font-size:1rem}}}.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{color:color-mix(in oklab,var(--color-base-content)40%,transparent)}}:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input)::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input)::placeholder{color:color-mix(in oklab,var(--color-base-content)20%,transparent)}}.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{box-shadow:none}.input:has(>input[disabled])>input[disabled]{cursor:not-allowed}.input::-webkit-date-and-time-value{text-align:inherit}.input[type=number]::-webkit-inner-spin-button{margin-block:-.75rem;margin-inline-end:-.75rem}.input::-webkit-calendar-picker-indicator{position:absolute;inset-inline-end:.75em}.input:has(>input[type=date]) :where(input[type=date]){webkit-appearance:none;appearance:none;display:inline-flex}.input:has(>input[type=date]) input[type=date]::-webkit-calendar-picker-indicator{cursor:pointer;width:1em;height:1em;position:absolute;inset-inline-end:.75em}.indicator{width:max-content;display:inline-flex;position:relative}.indicator :where(.indicator-item){z-index:1;white-space:nowrap;top:var(--indicator-t,0);bottom:var(--indicator-b,auto);left:var(--indicator-s,auto);right:var(--indicator-e,0);translate:var(--indicator-x,50%)var(--indicator-y,-50%);position:absolute}.table{border-collapse:separate;--tw-border-spacing-x:calc(.25rem*0);--tw-border-spacing-y:calc(.25rem*0);width:100%;border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y);border-radius:var(--radius-box);text-align:left;font-size:.875rem;position:relative}.table:where(:dir(rtl),[dir=rtl],[dir=rtl] *){text-align:right}@media (hover:hover){:is(.table tr.row-hover,.table tr.row-hover:nth-child(2n)):hover{background-color:var(--color-base-200)}}.table :where(th,td){vertical-align:middle;padding-block:.75rem;padding-inline:1rem}.table :where(thead,tfoot){white-space:nowrap;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(thead,tfoot){color:color-mix(in oklab,var(--color-base-content)60%,transparent)}}.table :where(thead,tfoot){font-size:.875rem;font-weight:600}.table :where(tfoot tr:first-child :is(td,th)){border-top:var(--border)solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(tfoot tr:first-child :is(td,th)){border-top:var(--border)solid color-mix(in oklch,var(--color-base-content)5%,#0000)}}.table :where(.table-pin-rows thead tr){z-index:1;background-color:var(--color-base-100);position:sticky;top:0}.table :where(.table-pin-rows tfoot tr){z-index:1;background-color:var(--color-base-100);position:sticky;bottom:0}.table :where(.table-pin-cols tr th){background-color:var(--color-base-100);position:sticky;left:0;right:0}.table :where(thead tr :is(td,th),tbody tr:not(:last-child) :is(td,th)){border-bottom:var(--border)solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(thead tr :is(td,th),tbody tr:not(:last-child) :is(td,th)){border-bottom:var(--border)solid color-mix(in oklch,var(--color-base-content)5%,#0000)}}.steps{counter-reset:step;grid-auto-columns:1fr;grid-auto-flow:column;display:inline-grid;overflow:auto hidden}.steps .step{text-align:center;--step-bg:var(--color-base-300);--step-fg:var(--color-base-content);grid-template-rows:40px 1fr;grid-template-columns:auto;place-items:center;min-width:4rem;display:grid}.steps .step:before{width:100%;height:.5rem;color:var(--step-bg);background-color:var(--step-bg);content:"";border:1px solid;grid-row-start:1;grid-column-start:1;margin-inline-start:-100%;top:0}.steps .step>.step-icon,.steps .step:not(:has(.step-icon)):after{--tw-content:counter(step);content:var(--tw-content);counter-increment:step;z-index:1;color:var(--step-fg);background-color:var(--step-bg);border:1px solid var(--step-bg);border-radius:3.40282e38px;grid-row-start:1;grid-column-start:1;place-self:center;place-items:center;width:2rem;height:2rem;display:grid;position:relative}.steps .step:first-child:before{--tw-content:none;content:var(--tw-content)}.steps .step[data-content]:after{--tw-content:attr(data-content);content:var(--tw-content)}.range{appearance:none;webkit-appearance:none;--range-thumb:var(--color-base-100);--range-thumb-size:calc(var(--size-selector,.25rem)*6);--range-progress:currentColor;--range-fill:1;--range-p:.25rem;--range-bg:currentColor}@supports (color:color-mix(in lab, red, red)){.range{--range-bg:color-mix(in oklab,currentColor 10%,#0000)}}.range{cursor:pointer;vertical-align:middle;--radius-selector-max:calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector));border-radius:calc(var(--radius-selector) + min(var(--range-p),var(--radius-selector-max)));width:clamp(3rem,20rem,100%);height:var(--range-thumb-size);background-color:#0000;border:none;overflow:hidden}[dir=rtl] .range{--range-dir:-1}.range:focus{outline:none}.range:focus-visible{outline-offset:2px;outline:2px solid}.range::-webkit-slider-runnable-track{background-color:var(--range-bg);border-radius:var(--radius-selector);width:100%;height:calc(var(--range-thumb-size)*.5)}@media (forced-colors:active){.range::-webkit-slider-runnable-track{border:1px solid}.range::-moz-range-track{border:1px solid}}.range::-webkit-slider-thumb{box-sizing:border-box;border-radius:calc(var(--radius-selector) + min(var(--range-p),var(--radius-selector-max)));background-color:var(--range-thumb);height:var(--range-thumb-size);width:var(--range-thumb-size);border:var(--range-p)solid;appearance:none;webkit-appearance:none;color:var(--range-progress);box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px currentColor,0 0 0 2rem var(--range-thumb)inset,calc((var(--range-dir,1)*-100rem) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100rem*var(--range-fill));position:relative;top:50%;transform:translateY(-50%)}@supports (color:color-mix(in lab, red, red)){.range::-webkit-slider-thumb{box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px color-mix(in oklab,currentColor calc(var(--depth)*10%),#0000),0 0 0 2rem var(--range-thumb)inset,calc((var(--range-dir,1)*-100rem) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100rem*var(--range-fill))}}.range::-moz-range-track{background-color:var(--range-bg);border-radius:var(--radius-selector);width:100%;height:calc(var(--range-thumb-size)*.5)}.range::-moz-range-thumb{box-sizing:border-box;border-radius:calc(var(--radius-selector) + min(var(--range-p),var(--radius-selector-max)));height:var(--range-thumb-size);width:var(--range-thumb-size);border:var(--range-p)solid;color:var(--range-progress);box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px currentColor,0 0 0 2rem var(--range-thumb)inset,calc((var(--range-dir,1)*-100rem) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100rem*var(--range-fill));background-color:currentColor;position:relative;top:50%}@supports (color:color-mix(in lab, red, red)){.range::-moz-range-thumb{box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px color-mix(in oklab,currentColor calc(var(--depth)*10%),#0000),0 0 0 2rem var(--range-thumb)inset,calc((var(--range-dir,1)*-100rem) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100rem*var(--range-fill))}}.range:disabled{cursor:not-allowed;opacity:.3}.chat-bubble{border-radius:var(--radius-field);background-color:var(--color-base-300);width:fit-content;color:var(--color-base-content);grid-row-end:3;min-width:2.5rem;max-width:90%;min-height:2rem;padding-block:.5rem;padding-inline:1rem;display:block;position:relative}.chat-bubble:before{background-color:inherit;content:"";width:.75rem;height:.75rem;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-image:var(--mask-chat);-webkit-mask-image:var(--mask-chat);mask-image:var(--mask-chat);position:absolute;bottom:0;-webkit-mask-position:0 -1px;mask-position:0 -1px;-webkit-mask-size:.8125rem;mask-size:.8125rem}.select{border:var(--border)solid #0000;appearance:none;background-color:var(--color-base-100);vertical-align:middle;width:clamp(3rem,20rem,100%);height:var(--size);touch-action:manipulation;white-space:nowrap;text-overflow:ellipsis;box-shadow:0 1px var(--input-color)inset,0 -1px oklch(100% 0 0/calc(var(--depth)*.1))inset;background-image:linear-gradient(45deg,#0000 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,#0000 50%);background-position:calc(100% - 20px) calc(1px + 50%),calc(100% - 16.1px) calc(1px + 50%);background-repeat:no-repeat;background-size:4px 4px,4px 4px;border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-shrink:1;align-items:center;gap:.375rem;padding-inline:.75rem 1.75rem;font-size:.875rem;display:inline-flex;position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.select{box-shadow:0 1px color-mix(in oklab,var(--input-color)calc(var(--depth)*10%),#0000)inset,0 -1px oklch(100% 0 0/calc(var(--depth)*.1))inset}}.select{border-color:var(--input-color);--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.select{--input-color:color-mix(in oklab,var(--color-base-content)20%,#0000)}}.select{--size:calc(var(--size-field,.25rem)*10)}[dir=rtl] .select{background-position:12px calc(1px + 50%),16px calc(1px + 50%)}[dir=rtl] .select::picker(select){translate:.5rem}[dir=rtl] .select select::picker(select){translate:.5rem}.select[multiple]{background-image:none;height:auto;padding-block:.75rem;padding-inline-end:.75rem;overflow:auto}.select select{appearance:none;width:calc(100% + 2.75rem);height:calc(100% - calc(var(--border)*2));background:inherit;border-radius:inherit;border-style:none;align-items:center;margin-inline:-.75rem -1.75rem;padding-inline:.75rem 1.75rem}.select select:focus,.select select:focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.select select:focus,.select select:focus-within{outline-offset:2px;outline:2px solid #0000}}.select select:not(:last-child){background-image:none;margin-inline-end:-1.375rem}.select:focus,.select:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.select:focus,.select:focus-within{box-shadow:0 1px color-mix(in oklab,var(--input-color)calc(var(--depth)*10%),#0000)}}.select:focus,.select:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate;z-index:1}.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select{cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select{color:color-mix(in oklab,var(--color-base-content)40%,transparent)}}:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select)::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select)::placeholder{color:color-mix(in oklab,var(--color-base-content)20%,transparent)}}.select:has(>select[disabled])>select[disabled]{cursor:not-allowed}@supports (appearance:base-select){.select,.select select{appearance:base-select}:is(.select,.select select)::picker(select){appearance:base-select}}:is(.select,.select select)::picker(select){color:inherit;border:var(--border)solid var(--color-base-200);border-radius:var(--radius-box);background-color:inherit;max-height:min(24rem,70dvh);box-shadow:0 2px calc(var(--depth)*3px)-2px oklch(0% 0 0/.2);box-shadow:0 20px 25px -5px rgb(0 0 0/calc(var(--depth)*.1)),0 8px 10px -6px rgb(0 0 0/calc(var(--depth)*.1));margin-block:.5rem;margin-inline:.5rem;padding:.5rem;translate:-.5rem}:is(.select,.select select)::picker-icon{display:none}:is(.select,.select select) optgroup{padding-top:.5em}:is(.select,.select select) optgroup option:first-child{margin-top:.5em}:is(.select,.select select) option{border-radius:var(--radius-field);white-space:normal;padding-block:.375rem;padding-inline:.75rem;transition-property:color,background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{background-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{outline-offset:2px;outline:2px solid #0000}}:is(.select,.select select) option:not(:disabled):active{background-color:var(--color-neutral);color:var(--color-neutral-content);box-shadow:0 2px calc(var(--depth)*3px)-2px var(--color-neutral)}.timeline{display:flex;position:relative}.timeline>li{grid-template-rows:var(--timeline-row-start,minmax(0,1fr))auto var(--timeline-row-end,minmax(0,1fr));grid-template-columns:var(--timeline-col-start,minmax(0,1fr))auto var(--timeline-col-end,minmax(0,1fr));flex-shrink:0;align-items:center;display:grid;position:relative}.timeline>li>hr{border:none;width:100%}.timeline>li>hr:first-child{grid-row-start:2;grid-column-start:1}.timeline>li>hr:last-child{grid-area:2/3/auto/none}@media print{.timeline>li>hr{border:.1px solid var(--color-base-300)}}.timeline :where(hr){background-color:var(--color-base-300);height:.25rem}.timeline:has(.timeline-middle hr):first-child{border-start-start-radius:0;border-start-end-radius:var(--radius-selector);border-end-end-radius:var(--radius-selector);border-end-start-radius:0}.timeline:has(.timeline-middle hr):last-child,.timeline:not(:has(.timeline-middle)) :first-child hr:last-child{border-start-start-radius:var(--radius-selector);border-start-end-radius:0;border-end-end-radius:0;border-end-start-radius:var(--radius-selector)}.timeline:not(:has(.timeline-middle)) :last-child hr:first-child{border-start-start-radius:0;border-start-end-radius:var(--radius-selector);border-end-end-radius:var(--radius-selector);border-end-start-radius:0}.swap{cursor:pointer;vertical-align:middle;webkit-user-select:none;-webkit-user-select:none;user-select:none;place-content:center;display:inline-grid;position:relative}.swap input{appearance:none;border:none}.swap>*{grid-row-start:1;grid-column-start:1}@media (prefers-reduced-motion:no-preference){.swap>*{transition-property:transform,rotate,opacity;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}}.swap .swap-on,.swap .swap-indeterminate,.swap input:indeterminate~.swap-on,.swap input:is(:checked,:indeterminate)~.swap-off{opacity:0}.swap input:checked~.swap-on,.swap input:indeterminate~.swap-indeterminate{opacity:1;backface-visibility:visible}.checkbox{border:var(--border)solid var(--input-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.checkbox{border:var(--border)solid var(--input-color,color-mix(in oklab,var(--color-base-content)20%,#0000))}}.checkbox{cursor:pointer;appearance:none;border-radius:var(--radius-selector);vertical-align:middle;color:var(--color-base-content);box-shadow:0 1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 0 #0000 inset,0 0 #0000;--size:calc(var(--size-selector,.25rem)*6);width:var(--size);height:var(--size);background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise);flex-shrink:0;padding:.25rem;transition:background-color .2s,box-shadow .2s;display:inline-block;position:relative}.checkbox:before{--tw-content:"";content:var(--tw-content);opacity:0;clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 80%,70% 80%,70% 100%);width:100%;height:100%;box-shadow:0px 3px 0 0px oklch(100% 0 0/calc(var(--depth)*.1))inset;background-color:currentColor;font-size:1rem;line-height:.75;transition:clip-path .3s .1s,opacity .1s .1s,rotate .3s .1s,translate .3s .1s;display:block;rotate:45deg}.checkbox:focus-visible{outline:2px solid var(--input-color,currentColor);outline-offset:2px}.checkbox:checked,.checkbox[aria-checked=true]{background-color:var(--input-color,#0000);box-shadow:0 0 #0000 inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px oklch(0% 0 0/calc(var(--depth)*.1))}:is(.checkbox:checked,.checkbox[aria-checked=true]):before{clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 0%,70% 0%,70% 100%);opacity:1}@media (forced-colors:active){:is(.checkbox:checked,.checkbox[aria-checked=true]):before{--tw-content:"✔︎";clip-path:none;background-color:#0000;rotate:none}}@media print{:is(.checkbox:checked,.checkbox[aria-checked=true]):before{--tw-content:"✔︎";clip-path:none;background-color:#0000;rotate:none}}.checkbox:indeterminate{background-color:var(--input-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.checkbox:indeterminate{background-color:var(--input-color,color-mix(in oklab,var(--color-base-content)20%,#0000))}}.checkbox:indeterminate:before{opacity:1;clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 80%,80% 80%,80% 100%);translate:0 -35%;rotate:none}.radio{cursor:pointer;appearance:none;vertical-align:middle;border:var(--border)solid var(--input-color,currentColor);border-radius:3.40282e38px;flex-shrink:0;padding:.25rem;display:inline-block;position:relative}@supports (color:color-mix(in lab, red, red)){.radio{border:var(--border)solid var(--input-color,color-mix(in srgb,currentColor 20%,#0000))}}.radio{box-shadow:0 1px oklch(0% 0 0/calc(var(--depth)*.1))inset;--size:calc(var(--size-selector,.25rem)*6);width:var(--size);height:var(--size);color:var(--input-color,currentColor)}.radio:before{--tw-content:"";content:var(--tw-content);background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise);border-radius:3.40282e38px;width:100%;height:100%;display:block}.radio:focus-visible{outline:2px solid}.radio:checked,.radio[aria-checked=true]{background-color:var(--color-base-100);border-color:currentColor}@media (prefers-reduced-motion:no-preference){.radio:checked,.radio[aria-checked=true]{animation:.2s ease-out radio}}:is(.radio:checked,.radio[aria-checked=true]):before{box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px oklch(0% 0 0/calc(var(--depth)*.1));background-color:currentColor}@media (forced-colors:active){:is(.radio:checked,.radio[aria-checked=true]):before{outline-style:var(--tw-outline-style);outline-offset:calc(1px*-1);outline-width:1px}}@media print{:is(.radio:checked,.radio[aria-checked=true]):before{outline-offset:-1rem;outline:.25rem solid}}.navbar{align-items:center;width:100%;min-height:4rem;padding:.5rem;display:flex}.card{border-radius:var(--radius-box);outline-offset:2px;outline:0 solid #0000;flex-direction:column;transition:outline .2s ease-in-out;display:flex;position:relative}.card:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.card:focus{outline-offset:2px;outline:2px solid #0000}}.card:focus-visible{outline-color:currentColor}.card :where(figure:first-child){border-start-start-radius:inherit;border-start-end-radius:inherit;border-end-end-radius:unset;border-end-start-radius:unset;overflow:hidden}.card :where(figure:last-child){border-start-start-radius:unset;border-start-end-radius:unset;border-end-end-radius:inherit;border-end-start-radius:inherit;overflow:hidden}.card figure{justify-content:center;align-items:center;display:flex}.card:has(>input:is(input[type=checkbox],input[type=radio])){cursor:pointer;-webkit-user-select:none;user-select:none}.card:has(>:checked){outline:2px solid}.stats{border-radius:var(--radius-box);grid-auto-flow:column;display:inline-grid;position:relative;overflow-x:auto}.progress{appearance:none;border-radius:var(--radius-box);background-color:currentColor;width:100%;height:.5rem;position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.progress{background-color:color-mix(in oklab,currentcolor 20%,transparent)}}.progress{color:var(--color-base-content)}.progress:indeterminate{background-image:repeating-linear-gradient(90deg,currentColor -1% 10%,#0000 10% 90%);background-position-x:15%;background-size:200%}@media (prefers-reduced-motion:no-preference){.progress:indeterminate{animation:5s ease-in-out infinite progress}}@supports ((-moz-appearance:none)){.progress:indeterminate::-moz-progress-bar{background-color:#0000}@media (prefers-reduced-motion:no-preference){.progress:indeterminate::-moz-progress-bar{background-image:repeating-linear-gradient(90deg,currentColor -1% 10%,#0000 10% 90%);background-position-x:15%;background-size:200%;animation:5s ease-in-out infinite progress}}.progress::-moz-progress-bar{border-radius:var(--radius-box);background-color:currentColor}}@supports ((-webkit-appearance:none)){.progress::-webkit-progress-bar{border-radius:var(--radius-box);background-color:#0000}.progress::-webkit-progress-value{border-radius:var(--radius-box);background-color:currentColor}}.stack{grid-template-rows:3px 4px 1fr 4px 3px;grid-template-columns:3px 4px 1fr 4px 3px;display:inline-grid}.stack>*{width:100%;height:100%}.stack>:nth-child(n+2){opacity:.7;width:100%}.stack>:nth-child(2){z-index:2;opacity:.9}.stack>:first-child{z-index:3;width:100%}.timeline-end{grid-area:3/1/4/4;place-self:flex-start center;margin:.25rem}.timeline-start{grid-area:1/1/2/4;place-self:flex-end center;margin:.25rem}.timeline-middle{grid-row-start:2;grid-column-start:2}.stat-value{white-space:nowrap;grid-column-start:1;font-size:2rem;font-weight:800}.stat-desc{white-space:nowrap;color:var(--color-base-content);grid-column-start:1}@supports (color:color-mix(in lab, red, red)){.stat-desc{color:color-mix(in oklab,var(--color-base-content)60%,transparent)}}.stat-desc{font-size:.75rem}.stat-title{white-space:nowrap;color:var(--color-base-content);grid-column-start:1}@supports (color:color-mix(in lab, red, red)){.stat-title{color:color-mix(in oklab,var(--color-base-content)60%,transparent)}}.stat-title{font-size:.75rem}.chat-footer{grid-row-start:3;gap:.25rem;font-size:.6875rem;display:flex}.chat-header{grid-row-start:1;gap:.25rem;font-size:.6875rem;display:flex}.filter{flex-wrap:wrap;display:flex}.filter input[type=radio]{width:auto}.filter input{opacity:1;transition:margin .1s,opacity .3s,padding .3s,border-width .1s;overflow:hidden;scale:1}.filter input:not(:last-child){margin-inline-end:.25rem}.filter input.filter-reset{aspect-ratio:1}.filter input.filter-reset:after{--tw-content:"×";content:var(--tw-content)}.filter:not(:has(input:checked:not(.filter-reset))) .filter-reset,.filter:not(:has(input:checked:not(.filter-reset))) input[type=reset],.filter:has(input:checked:not(.filter-reset)) input:not(:checked,.filter-reset,input[type=reset]){opacity:0;border-width:0;width:0;margin-inline:0;padding-inline:0;scale:0}.label{white-space:nowrap;color:currentColor;align-items:center;gap:.375rem;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.label{color:color-mix(in oklab,currentcolor 60%,transparent)}}.label:has(input){cursor:pointer}.label:is(.input>*,.select>*){white-space:nowrap;height:calc(100% - .5rem);font-size:inherit;align-items:center;padding-inline:.75rem;display:flex}.label:is(.input>*,.select>*):first-child{border-inline-end:var(--border)solid currentColor;margin-inline:-.75rem .75rem}@supports (color:color-mix(in lab, red, red)){.label:is(.input>*,.select>*):first-child{border-inline-end:var(--border)solid color-mix(in oklab,currentColor 10%,#0000)}}.label:is(.input>*,.select>*):last-child{border-inline-start:var(--border)solid currentColor;margin-inline:.75rem -.75rem}@supports (color:color-mix(in lab, red, red)){.label:is(.input>*,.select>*):last-child{border-inline-start:var(--border)solid color-mix(in oklab,currentColor 10%,#0000)}}.fieldset-legend{color:var(--color-base-content);justify-content:space-between;align-items:center;gap:.5rem;margin-bottom:-.25rem;padding-block:.5rem;font-weight:600;display:flex}.status{aspect-ratio:1;border-radius:var(--radius-selector);background-color:var(--color-base-content);width:.5rem;height:.5rem;display:inline-block}@supports (color:color-mix(in lab, red, red)){.status{background-color:color-mix(in oklab,var(--color-base-content)20%,transparent)}}.status{vertical-align:middle;color:#0000004d;background-position:50%;background-repeat:no-repeat}@supports (color:color-mix(in lab, red, red)){.status{color:color-mix(in oklab,var(--color-black)30%,transparent)}}.status{background-image:radial-gradient(circle at 35% 30%,oklch(1 0 0/calc(var(--depth)*.5)),#0000);box-shadow:0 2px 3px -1px}@supports (color:color-mix(in lab, red, red)){.status{box-shadow:0 2px 3px -1px color-mix(in oklab,currentColor calc(var(--depth)*100%),#0000)}}.badge{border-radius:var(--radius-selector);vertical-align:middle;color:var(--badge-fg);border:var(--border)solid var(--badge-color,var(--color-base-200));width:fit-content;padding-inline:calc(.25rem*3 - var(--border));background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise);background-color:var(--badge-bg);--badge-bg:var(--badge-color,var(--color-base-100));--badge-fg:var(--color-base-content);--size:calc(var(--size-selector,.25rem)*6);height:var(--size);justify-content:center;align-items:center;gap:.5rem;font-size:.875rem;display:inline-flex}.tabs{--tabs-height:auto;--tabs-direction:row;--tab-height:calc(var(--size-field,.25rem)*10);height:var(--tabs-height);flex-wrap:wrap;flex-direction:var(--tabs-direction);display:flex}.footer{grid-auto-flow:row;place-items:start;gap:2.5rem 1rem;width:100%;font-size:.875rem;line-height:1.25rem;display:grid}.footer>*{place-items:start;gap:.5rem;display:grid}.footer.footer-center{text-align:center;grid-auto-flow:column dense;place-items:center}.footer.footer-center>*{place-items:center}.stat{grid-template-columns:repeat(1,1fr);column-gap:1rem;width:100%;padding-block:1rem;padding-inline:1.5rem;display:inline-grid}.stat:not(:last-child){border-inline-end:var(--border)dashed currentColor}@supports (color:color-mix(in lab, red, red)){.stat:not(:last-child){border-inline-end:var(--border)dashed color-mix(in oklab,currentColor 10%,#0000)}}.stat:not(:last-child){border-block-end:none}.navbar-end{justify-content:flex-end;align-items:center;width:50%;display:inline-flex}.navbar-start{justify-content:flex-start;align-items:center;width:50%;display:inline-flex}.card-body{padding:var(--card-p,1.5rem);font-size:var(--card-fs,.875rem);flex-direction:column;flex:auto;gap:.5rem;display:flex}.card-body :where(p){flex-grow:1}.alert{--alert-border-color:var(--color-base-200);border-radius:var(--radius-box);color:var(--color-base-content);background-color:var(--alert-color,var(--color-base-200));text-align:start;background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise);box-shadow:0 3px 0 -2px oklch(100% 0 0/calc(var(--depth)*.08))inset,0 1px #000,0 4px 3px -2px oklch(0% 0 0/calc(var(--depth)*.08));border-style:solid;grid-template-columns:auto;grid-auto-flow:column;justify-content:start;place-items:center start;gap:1rem;padding-block:.75rem;padding-inline:1rem;font-size:.875rem;line-height:1.25rem;display:grid}@supports (color:color-mix(in lab, red, red)){.alert{box-shadow:0 3px 0 -2px oklch(100% 0 0/calc(var(--depth)*.08))inset,0 1px color-mix(in oklab,color-mix(in oklab,#000 20%,var(--alert-color,var(--color-base-200)))calc(var(--depth)*20%),#0000),0 4px 3px -2px oklch(0% 0 0/calc(var(--depth)*.08))}}.alert:has(:nth-child(2)){grid-template-columns:auto minmax(auto,1fr)}.fieldset{grid-template-columns:1fr;grid-auto-rows:max-content;gap:.375rem;padding-block:.25rem;font-size:.75rem;display:grid}.card-title{font-size:var(--cardtitle-fs,1.125rem);align-items:center;gap:.5rem;font-weight:600;display:flex}.chat{--mask-chat:url("data:image/svg+xml,%3csvg width='13' height='13' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='M0 11.5004C0 13.0004 2 13.0004 2 13.0004H12H13V0.00036329L12.5 0C12.5 0 11.977 2.09572 11.8581 2.50033C11.6075 3.35237 10.9149 4.22374 9 5.50036C6 7.50036 0 10.0004 0 11.5004Z'/%3e%3c/svg%3e");column-gap:.75rem;padding-block:.25rem;display:grid}.link{cursor:pointer;text-decoration-line:underline}.link:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.link:focus{outline-offset:2px;outline:2px solid #0000}}.link:focus-visible{outline-offset:2px;outline:2px solid}.btn-primary{--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content)}}@layer daisyui.l1.l2{.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal{pointer-events:auto;visibility:visible;opacity:1;transition:visibility 0s allow-discrete,background-color .3s ease-out,opacity .1s ease-out;background-color:oklch(0% 0 0/.4)}:is(.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal) .modal-box{opacity:1;translate:0;scale:1}:root:has(:is(.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal)){--page-has-backdrop:1;--page-overflow:hidden;--page-scroll-bg:var(--page-scroll-bg-on);--page-scroll-gutter:stable;--page-scroll-transition:var(--page-scroll-transition-on);animation:forwards set-page-has-scroll;animation-timeline:scroll()}@starting-style{.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal{opacity:0}}.btn:disabled:not(.btn-link,.btn-ghost){background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn:disabled:not(.btn-link,.btn-ghost){background-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.btn:disabled:not(.btn-link,.btn-ghost){box-shadow:none}.btn:disabled{pointer-events:none;--btn-border:#0000;--btn-noise:none;--btn-fg:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn:disabled{--btn-fg:color-mix(in oklch,var(--color-base-content)20%,#0000)}}.btn[disabled]:not(.btn-link,.btn-ghost){background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn[disabled]:not(.btn-link,.btn-ghost){background-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.btn[disabled]:not(.btn-link,.btn-ghost){box-shadow:none}.btn[disabled]{pointer-events:none;--btn-border:#0000;--btn-noise:none;--btn-fg:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn[disabled]{--btn-fg:color-mix(in oklch,var(--color-base-content)20%,#0000)}}.list .list-row:has(.list-col-grow:first-child){--list-grid-cols:1fr}.list .list-row:has(.list-col-grow:nth-child(2)){--list-grid-cols:minmax(0,auto)1fr}.list .list-row:has(.list-col-grow:nth-child(3)){--list-grid-cols:minmax(0,auto)minmax(0,auto)1fr}.list .list-row:has(.list-col-grow:nth-child(4)){--list-grid-cols:minmax(0,auto)minmax(0,auto)minmax(0,auto)1fr}.list .list-row:has(.list-col-grow:nth-child(5)){--list-grid-cols:minmax(0,auto)minmax(0,auto)minmax(0,auto)minmax(0,auto)1fr}.list .list-row:has(.list-col-grow:nth-child(6)){--list-grid-cols:minmax(0,auto)minmax(0,auto)minmax(0,auto)minmax(0,auto)minmax(0,auto)1fr}.list .list-row>:not(.list-col-wrap){grid-row-start:1}.steps .step-neutral+.step-neutral:before,.steps .step-neutral:after,.steps .step-neutral>.step-icon{--step-bg:var(--color-neutral);--step-fg:var(--color-neutral-content)}.steps .step-primary+.step-primary:before,.steps .step-primary:after,.steps .step-primary>.step-icon{--step-bg:var(--color-primary);--step-fg:var(--color-primary-content)}.steps .step-secondary+.step-secondary:before,.steps .step-secondary:after,.steps .step-secondary>.step-icon{--step-bg:var(--color-secondary);--step-fg:var(--color-secondary-content)}.steps .step-accent+.step-accent:before,.steps .step-accent:after,.steps .step-accent>.step-icon{--step-bg:var(--color-accent);--step-fg:var(--color-accent-content)}.steps .step-info+.step-info:before,.steps .step-info:after,.steps .step-info>.step-icon{--step-bg:var(--color-info);--step-fg:var(--color-info-content)}.steps .step-success+.step-success:before,.steps .step-success:after,.steps .step-success>.step-icon{--step-bg:var(--color-success);--step-fg:var(--color-success-content)}.steps .step-warning+.step-warning:before,.steps .step-warning:after,.steps .step-warning>.step-icon{--step-bg:var(--color-warning);--step-fg:var(--color-warning-content)}.steps .step-error+.step-error:before,.steps .step-error:after,.steps .step-error>.step-icon{--step-bg:var(--color-error);--step-fg:var(--color-error-content)}.checkbox:disabled,.radio:disabled{cursor:not-allowed;opacity:.2}:where(.navbar){position:relative}.chat-end{grid-template-columns:1fr auto;place-items:end}.chat-end .chat-header,.chat-end .chat-footer{grid-column-start:1}.chat-end .chat-image{grid-column-start:2}.chat-end .chat-bubble{border-end-end-radius:0;grid-column-start:1}.chat-end .chat-bubble:before{inset-inline-start:100%;transform:rotateY(180deg)}[dir=rtl] :is(.chat-end .chat-bubble):before{transform:rotateY(0)}.chat-start{grid-template-columns:auto 1fr;place-items:start}.chat-start .chat-header,.chat-start .chat-footer{grid-column-start:2}.chat-start .chat-image{grid-column-start:1}.chat-start .chat-bubble{border-end-start-radius:0;grid-column-start:2}.chat-start .chat-bubble:before{inset-inline-start:-.75rem;transform:rotateY(0)}[dir=rtl] :is(.chat-start .chat-bubble):before{transform:rotateY(180deg)}:is(.stack,.stack.stack-bottom)>*{grid-area:3/3/6/4}:is(.stack,.stack.stack-bottom)>:nth-child(2){grid-area:2/2/5/5}:is(.stack,.stack.stack-bottom)>:first-child{grid-area:1/1/4/6}.stack.stack-top>*{grid-area:1/3/4/4}.stack.stack-top>:nth-child(2){grid-area:2/2/5/5}.stack.stack-top>:first-child{grid-area:3/1/6/6}.stack.stack-start>*{grid-area:3/1/4/4}.stack.stack-start>:nth-child(2){grid-area:2/2/5/5}.stack.stack-start>:first-child{grid-area:1/3/6/6}.stack.stack-end>*{grid-area:3/3/4/6}.stack.stack-end>:nth-child(2){grid-area:2/2/5/5}.stack.stack-end>:first-child{grid-area:1/1/6/4}.timeline-vertical{flex-direction:column}.timeline-vertical>li{--timeline-row-start:minmax(0,1fr);--timeline-row-end:minmax(0,1fr);justify-items:center}.timeline-vertical>li>hr{width:.25rem;height:100%}.timeline-vertical>li>hr:first-child{grid-row-start:1;grid-column-start:2}.timeline-vertical>li>hr:last-child{grid-area:3/2/none}.timeline-vertical .timeline-start{grid-area:1/1/4/2;place-self:center flex-end}.timeline-vertical .timeline-end{grid-area:1/3/4/4;place-self:center flex-start}.timeline-vertical:has(.timeline-middle)>li>hr:first-child{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:var(--radius-selector);border-bottom-left-radius:var(--radius-selector)}.timeline-vertical:has(.timeline-middle)>li>hr:last-child,.timeline-vertical:not(:has(.timeline-middle)) :first-child>hr:last-child{border-top-left-radius:var(--radius-selector);border-top-right-radius:var(--radius-selector);border-bottom-right-radius:0;border-bottom-left-radius:0}.timeline-vertical:not(:has(.timeline-middle)) :last-child>hr:first-child{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:var(--radius-selector);border-bottom-left-radius:var(--radius-selector)}.timeline-vertical.timeline-snap-icon>li{--timeline-col-start:minmax(0,1fr);--timeline-row-start:.5rem}.timeline-compact{--timeline-row-start:0}.timeline-compact .timeline-start{grid-area:3/1/4/4;place-self:flex-start center}.timeline-compact li:has(.timeline-start) .timeline-end{grid-row-start:auto;grid-column-start:none}.timeline-compact.timeline-vertical>li{--timeline-col-start:0}.timeline-compact.timeline-vertical .timeline-start{grid-area:1/3/4/4;place-self:center flex-start}.timeline-compact.timeline-vertical li:has(.timeline-start) .timeline-end{grid-row-start:none;grid-column-start:auto}.input-sm{--size:calc(var(--size-field,.25rem)*8);font-size:max(var(--font-size,.75rem),.75rem)}.input-sm[type=number]::-webkit-inner-spin-button{margin-block:-.5rem;margin-inline-end:-.75rem}.tabs-box{background-color:var(--color-base-200);--tabs-box-radius:calc(3*var(--radius-field));border-radius:calc(min(calc(var(--tab-height)/2),var(--radius-field)) + min(.25rem,var(--tabs-box-radius)));box-shadow:0 -.5px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 .5px oklch(0% 0 0/calc(var(--depth)*.05))inset;padding:.25rem}.tabs-box>.tab{border-radius:var(--radius-field);border-style:none}.tabs-box>.tab:focus-visible,.tabs-box>.tab:is(label:has(:checked:focus-visible)){outline-offset:2px}.tabs-box>:is(.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]):not(.tab-disabled,[disabled]),.tabs-box>:is(input:checked),.tabs-box>:is(label:has(:checked)){background-color:var(--tab-bg,var(--color-base-100));box-shadow:0 1px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px 1px -1px var(--color-neutral),0 1px 6px -4px var(--color-neutral)}@supports (color:color-mix(in lab, red, red)){.tabs-box>:is(.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]):not(.tab-disabled,[disabled]),.tabs-box>:is(input:checked),.tabs-box>:is(label:has(:checked)){box-shadow:0 1px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px 1px -1px color-mix(in oklab,var(--color-neutral)calc(var(--depth)*50%),#0000),0 1px 6px -4px color-mix(in oklab,var(--color-neutral)calc(var(--depth)*100%),#0000)}}@media (forced-colors:active){.tabs-box>:is(.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]):not(.tab-disabled,[disabled]),.tabs-box>:is(input:checked),.tabs-box>:is(label:has(:checked)){border:1px solid}}.tabs-box>.tab-content{height:calc(100% - var(--tab-height) + var(--border) - .5rem);border-radius:calc(min(calc(var(--tab-height)/2),var(--radius-field)) + min(.25rem,var(--tabs-box-radius)) - var(--border));margin-top:.25rem}.btn-square{width:var(--size);height:var(--size);padding-inline:0}.loading-sm{width:calc(var(--size-selector,.25rem)*5)}.badge-ghost{border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content);background-image:none}.badge-outline{color:var(--badge-color);--badge-bg:#0000;background-image:none;border-color:currentColor}.chat-bubble-info{background-color:var(--color-info);color:var(--color-info-content)}.chat-bubble-neutral{background-color:var(--color-neutral);color:var(--color-neutral-content)}.status-error{background-color:var(--color-error);color:var(--color-error)}.status-success{background-color:var(--color-success);color:var(--color-success)}.status-warning{background-color:var(--color-warning);color:var(--color-warning)}.table-zebra tbody tr:where(:nth-child(2n)),.table-zebra tbody tr:where(:nth-child(2n)) :where(.table-pin-cols tr th){background-color:var(--color-base-200)}@media (hover:hover){:is(.table-zebra tbody tr.row-hover,.table-zebra tbody tr.row-hover:where(:nth-child(2n))):hover{background-color:var(--color-base-300)}}.loading-spinner{-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E")}.select-sm{--size:calc(var(--size-field,.25rem)*8);font-size:.75rem}.select-sm option{padding-block:.25rem;padding-inline:.625rem}.table-sm :not(thead,tfoot) tr{font-size:.75rem}.table-sm :where(th,td){padding-block:.5rem;padding-inline:.75rem}.badge-sm{--size:calc(var(--size-selector,.25rem)*5);padding-inline:calc(.25rem*2.5 - var(--border));font-size:.75rem}.alert-error{color:var(--color-error-content);--alert-border-color:var(--color-error);--alert-color:var(--color-error)}.alert-info{color:var(--color-info-content);--alert-border-color:var(--color-info);--alert-color:var(--color-info)}.progress-info{color:var(--color-info)}.link-hover{text-decoration-line:none}@media (hover:hover){.link-hover:hover{text-decoration-line:underline}}.btn-sm{--fontsize:.75rem;--btn-p:.75rem;--size:calc(var(--size-field,.25rem)*8)}.badge-error{--badge-color:var(--color-error);--badge-fg:var(--color-error-content)}.badge-info{--badge-color:var(--color-info);--badge-fg:var(--color-info-content)}.badge-success{--badge-color:var(--color-success);--badge-fg:var(--color-success-content)}.badge-warning{--badge-color:var(--color-warning);--badge-fg:var(--color-warning-content)}.timeline-snap-icon>li{--timeline-col-start:.5rem;--timeline-row-start:minmax(0,1fr)}.card-border{border:var(--border)solid var(--color-base-200)}.toggle-sm[type=checkbox],.toggle-sm:has([type=checkbox]){--size:calc(var(--size-selector,.25rem)*5)}}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.top-0{top:calc(var(--spacing)*0)}.isolate{isolation:isolate}.z-20{z-index:20}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-5{margin-top:calc(var(--spacing)*5)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-auto{margin-top:auto}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-1\.5{margin-bottom:calc(var(--spacing)*1.5)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-1{margin-left:calc(var(--spacing)*1)}.alert{border-width:var(--border);border-color:var(--alert-border-color,var(--color-base-200))}.join{--join-ss:0;--join-se:0;--join-es:0;--join-ee:0;align-items:stretch;display:inline-flex}.join :where(.join-item){border-start-start-radius:var(--join-ss,0);border-start-end-radius:var(--join-se,0);border-end-end-radius:var(--join-ee,0);border-end-start-radius:var(--join-es,0)}.join :where(.join-item) *{--join-ss:var(--radius-field);--join-se:var(--radius-field);--join-es:var(--radius-field);--join-ee:var(--radius-field)}.join>.join-item:where(:first-child),.join :first-child:not(:last-child) :where(.join-item){--join-ss:var(--radius-field);--join-se:0;--join-es:var(--radius-field);--join-ee:0}.join>.join-item:where(:last-child),.join :last-child:not(:first-child) :where(.join-item){--join-ss:0;--join-se:var(--radius-field);--join-es:0;--join-ee:var(--radius-field)}.join>.join-item:where(:only-child),.join :only-child :where(.join-item){--join-ss:var(--radius-field);--join-se:var(--radius-field);--join-es:var(--radius-field);--join-ee:var(--radius-field)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.size-2{width:calc(var(--spacing)*2);height:calc(var(--spacing)*2)}.size-10{width:calc(var(--spacing)*10);height:calc(var(--spacing)*10)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-auto{height:auto}.max-h-\[38rem\]{max-height:38rem}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-screen{min-height:100vh}.w-24{width:calc(var(--spacing)*24)}.w-full{width:100%}.w-max{width:max-content}.max-w-2xl{max-width:var(--container-2xl)}.max-w-40{max-width:calc(var(--spacing)*40)}.max-w-56{max-width:calc(var(--spacing)*56)}.max-w-64{max-width:calc(var(--spacing)*64)}.max-w-72{max-width:calc(var(--spacing)*72)}.max-w-\[min\(42rem\,88vw\)\]{max-width:min(42rem,88vw)}.max-w-screen-2xl{max-width:var(--breakpoint-2xl)}.max-w-xl{max-width:var(--container-xl)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-24{min-width:calc(var(--spacing)*24)}.min-w-28{min-width:calc(var(--spacing)*28)}.min-w-64{min-width:calc(var(--spacing)*64)}.min-w-full{min-width:100%}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grid-cols-\[5rem_minmax\(0\,1fr\)_3\.5rem\]{grid-template-columns:5rem minmax(0,1fr) 3.5rem}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{column-gap:calc(var(--spacing)*4)}.gap-y-1{row-gap:calc(var(--spacing)*1)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-base-300>:not(:last-child)){border-color:var(--color-base-300)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-base-300{border-color:var(--color-base-300)}.border-error\/20{border-color:var(--color-error)}@supports (color:color-mix(in lab, red, red)){.border-error\/20{border-color:color-mix(in oklab,var(--color-error)20%,transparent)}}.border-info\/30{border-color:var(--color-info)}@supports (color:color-mix(in lab, red, red)){.border-info\/30{border-color:color-mix(in oklab,var(--color-info)30%,transparent)}}.bg-base-100,.bg-base-100\/70{background-color:var(--color-base-100)}@supports (color:color-mix(in lab, red, red)){.bg-base-100\/70{background-color:color-mix(in oklab,var(--color-base-100)70%,transparent)}}.bg-base-100\/95{background-color:var(--color-base-100)}@supports (color:color-mix(in lab, red, red)){.bg-base-100\/95{background-color:color-mix(in oklab,var(--color-base-100)95%,transparent)}}.bg-base-200,.bg-base-200\/40{background-color:var(--color-base-200)}@supports (color:color-mix(in lab, red, red)){.bg-base-200\/40{background-color:color-mix(in oklab,var(--color-base-200)40%,transparent)}}.bg-base-200\/50{background-color:var(--color-base-200)}@supports (color:color-mix(in lab, red, red)){.bg-base-200\/50{background-color:color-mix(in oklab,var(--color-base-200)50%,transparent)}}.bg-base-200\/60{background-color:var(--color-base-200)}@supports (color:color-mix(in lab, red, red)){.bg-base-200\/60{background-color:color-mix(in oklab,var(--color-base-200)60%,transparent)}}.bg-base-200\/70{background-color:var(--color-base-200)}@supports (color:color-mix(in lab, red, red)){.bg-base-200\/70{background-color:color-mix(in oklab,var(--color-base-200)70%,transparent)}}.bg-error\/10{background-color:var(--color-error)}@supports (color:color-mix(in lab, red, red)){.bg-error\/10{background-color:color-mix(in oklab,var(--color-error)10%,transparent)}}.bg-info,.bg-info\/10{background-color:var(--color-info)}@supports (color:color-mix(in lab, red, red)){.bg-info\/10{background-color:color-mix(in oklab,var(--color-info)10%,transparent)}}.bg-primary{background-color:var(--color-primary)}.p-0{padding:calc(var(--spacing)*0)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-5{padding:calc(var(--spacing)*5)}.p-6{padding:calc(var(--spacing)*6)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-5{padding-block:calc(var(--spacing)*5)}.py-6{padding-block:calc(var(--spacing)*6)}.pb-3{padding-bottom:calc(var(--spacing)*3)}.pb-5{padding-bottom:calc(var(--spacing)*5)}.pl-4{padding-left:calc(var(--spacing)*4)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.68rem\]{font-size:.68rem}.leading-5{--tw-leading:calc(var(--spacing)*5);line-height:calc(var(--spacing)*5)}.leading-6{--tw-leading:calc(var(--spacing)*6);line-height:calc(var(--spacing)*6)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.18em\]{--tw-tracking:.18em;letter-spacing:.18em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-base-content,.text-base-content\/45{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/45{color:color-mix(in oklab,var(--color-base-content)45%,transparent)}}.text-base-content\/55{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/55{color:color-mix(in oklab,var(--color-base-content)55%,transparent)}}.text-base-content\/60{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/60{color:color-mix(in oklab,var(--color-base-content)60%,transparent)}}.text-base-content\/65{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/65{color:color-mix(in oklab,var(--color-base-content)65%,transparent)}}.text-base-content\/70{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/70{color:color-mix(in oklab,var(--color-base-content)70%,transparent)}}.text-base-content\/80{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/80{color:color-mix(in oklab,var(--color-base-content)80%,transparent)}}.text-error{color:var(--color-error)}.text-info{color:var(--color-info)}.text-primary{color:var(--color-primary)}.text-primary-content{color:var(--color-primary-content)}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.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,)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@layer daisyui.l1{.btn-ghost:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn)){--btn-shadow:"";--btn-bg:#0000;--btn-border:#0000;--btn-noise:none}.btn-ghost:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn)):not(:disabled,[disabled],.btn-disabled){--btn-fg:var(--btn-color,currentColor);outline-color:currentColor}@media (hover:none){.btn-ghost:not(.btn-active,:active,:focus-visible,input:checked:not(.filter .btn)):hover{--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color,currentColor);--btn-border:#0000;--btn-noise:none;outline-color:currentColor}}}.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);-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,)}.first\:pt-0:first-child{padding-top:calc(var(--spacing)*0)}.last\:pb-0:last-child{padding-bottom:calc(var(--spacing)*0)}@media (hover:hover){.hover\:bg-base-200:hover{background-color:var(--color-base-200)}}@media (min-width:40rem){.sm\:mb-8{margin-bottom:calc(var(--spacing)*8)}.sm\:block{display:block}.sm\:min-w-0{min-width:calc(var(--spacing)*0)}@layer daisyui.l1.l2{.sm\:alert-horizontal{text-align:start;grid-template-columns:auto;grid-auto-flow:column;justify-content:start;justify-items:start}.sm\:alert-horizontal:has(:nth-child(2)){grid-template-columns:auto minmax(auto,1fr)}}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-\[9rem_minmax\(14rem\,1fr\)_auto\]{grid-template-columns:9rem minmax(14rem,1fr) auto}.sm\:grid-cols-\[10rem_minmax\(14rem\,1fr\)_auto\]{grid-template-columns:10rem minmax(14rem,1fr) auto}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}.sm\:items-start{align-items:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:gap-3{gap:calc(var(--spacing)*3)}.sm\:p-5{padding:calc(var(--spacing)*5)}.sm\:p-6{padding:calc(var(--spacing)*6)}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}.sm\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.sm\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media (min-width:64rem){.lg\:flex{display:flex}.lg\:flex-row{flex-direction:row}.lg\:items-end{align-items:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:px-8{padding-inline:calc(var(--spacing)*8)}.lg\:py-8{padding-block:calc(var(--spacing)*8)}}@media (min-width:80rem){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-\[minmax\(0\,1\.55fr\)_minmax\(20rem\,0\.85fr\)\]{grid-template-columns:minmax(0,1.55fr) minmax(20rem,.85fr)}.xl\:grid-cols-\[minmax\(17rem\,0\.75fr\)_minmax\(0\,1\.7fr\)\]{grid-template-columns:minmax(17rem,.75fr) minmax(0,1.7fr)}}}:root{color-scheme:light}[data-theme=dark]{color-scheme:dark}body{text-rendering:optimizeLegibility}button,a,input,select{-webkit-tap-highlight-color:transparent}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[role=tabpanel]:focus-visible{outline:2px solid var(--color-primary);outline-offset:3px}.dashboard-shell{background-image:radial-gradient(circle at 84% 0%,var(--color-info),transparent 30rem)}@supports (color:color-mix(in lab, red, red)){.dashboard-shell{background-image:radial-gradient(circle at 84% 0%,color-mix(in oklab,var(--color-info)9%,transparent),transparent 30rem)}}[data-theme=dark] .dashboard-shell{background-image:radial-gradient(circle at 84% 0%,var(--color-info),transparent 30rem)}@supports (color:color-mix(in lab, red, red)){[data-theme=dark] .dashboard-shell{background-image:radial-gradient(circle at 84% 0%,color-mix(in oklab,var(--color-info)12%,transparent),transparent 30rem)}}.card-border{border-width:1px}.timeline [data-depth="1"]{margin-left:1.25rem}@media (max-width:639px){.timeline .timeline-start{width:4.5rem}.timeline .timeline-end{padding-left:.75rem}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}@keyframes rating{0%,40%{filter:brightness(1.05)contrast(1.05);scale:1.1}}@keyframes dropdown{0%{opacity:0}}@keyframes radio{0%{padding:5px}50%{padding:3px}}@keyframes toast{0%{opacity:0;scale:.9}to{opacity:1;scale:1}}@keyframes rotator{89.9999%,to{--first-item-position:0 0%}90%,99.9999%{--first-item-position:0 calc(var(--items)*100%)}to{translate:0 -100%}}@keyframes skeleton{0%{background-position:150%}to{background-position:-50%}}@keyframes menu{0%{opacity:0}}@keyframes progress{50%{background-position-x:-115%}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false} +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-black:#000;--spacing:.25rem;--breakpoint-2xl:96rem;--container-xl:36rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-black:900;--tracking-tight:-.025em;--tracking-wide:.025em;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}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;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}@media (prefers-color-scheme:dark){:root:not([data-theme]){color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}}:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=dark]:checked),[data-theme=dark]{color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root{--fx-noise:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.34' numOctaves='4' stitchTiles='stitch'%3E%3C/feTurbulence%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23a)' opacity='0.2'%3E%3C/rect%3E%3C/svg%3E");scrollbar-color:currentColor #0000}@supports (color:color-mix(in lab, red, red)){:root{scrollbar-color:color-mix(in oklch,currentColor 35%,#0000)#0000}}@property --radialprogress{syntax: ""; inherits: true; initial-value: 0%;}:root:not(span){overflow:var(--page-overflow)}:root{--page-scroll-bg-on:linear-gradient(var(--root-bg),var(--root-bg))var(--root-bg)}@supports (color:color-mix(in lab, red, red)){:root{--page-scroll-bg-on:linear-gradient(var(--root-bg),var(--root-bg))color-mix(in srgb,var(--root-bg),oklch(0% 0 0) calc(var(--page-has-backdrop,0)*40%))}}:root{--page-scroll-transition-on:background-color .3s ease-out;transition:var(--page-scroll-transition);scrollbar-gutter:var(--page-scroll-gutter,unset);scrollbar-gutter:if(style(--page-has-scroll: 1): var(--page-scroll-gutter,unset); else: unset)}:root:root{background:var(--page-scroll-bg,var(--root-bg,var(--color-base-100)))}@keyframes set-page-has-scroll{0%,to{--page-has-scroll:1}}:root,[data-theme]{background-color:var(--root-bg,var(--color-base-100));color:var(--color-base-content)}:where(:root,[data-theme]){--root-bg:var(--color-base-100)}}@layer components;@layer utilities{@layer daisyui.l1.l2.l3{.modal{pointer-events:none;visibility:hidden;width:100%;max-width:none;height:100%;max-height:none;color:inherit;transition:visibility .3s allow-discrete,background-color .3s ease-out,opacity .1s ease-out;overscroll-behavior:contain;z-index:999;scrollbar-gutter:auto;background-color:#0000;place-items:center;margin:0;padding:0;display:grid;position:fixed;inset:0;overflow:clip}.modal::backdrop{display:none}.tab{cursor:pointer;appearance:none;text-align:center;webkit-user-select:none;-webkit-user-select:none;user-select:none;flex-wrap:wrap;justify-content:center;align-items:center;display:inline-flex;position:relative}@media (hover:hover){.tab:hover{color:var(--color-base-content)}}.tab{--tab-p:.75rem;--tab-bg:var(--color-base-100);--tab-border-color:var(--color-base-300);--tab-radius-ss:0;--tab-radius-se:0;--tab-radius-es:0;--tab-radius-ee:0;--tab-order:0;--tab-radius-min:calc(.75rem - var(--border));--tab-radius-limit:min(var(--radius-field),var(--tab-radius-min));--tab-radius-grad:#0000 calc(69% - var(--border)),var(--tab-border-color)calc(calc(69% - var(--border)) + .25px),var(--tab-border-color)calc(calc(69% - var(--border)) + var(--border)),var(--tab-bg)calc(calc(69% - var(--border)) + var(--border) + .25px);order:var(--tab-order);height:var(--tab-height);border-color:#0000;padding-inline-start:var(--tab-p);padding-inline-end:var(--tab-p);font-size:.875rem}.tab:is(input[type=radio]){min-width:fit-content}.tab:is(input[type=radio]):after{--tw-content:attr(aria-label);content:var(--tw-content)}.tab:is(label){position:relative}.tab:is(label) input{cursor:pointer;appearance:none;opacity:0;position:absolute;inset:0}:is(.tab:checked,.tab:is(label:has(:checked)),.tab:is(.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]))+.tab-content{display:block}.tab:not(:checked,label:has(:checked),:hover,.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]){color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.tab:not(:checked,label:has(:checked),:hover,.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]){color:color-mix(in oklab,var(--color-base-content)50%,transparent)}}.tab:not(input):empty{cursor:default;flex-grow:1}.tab:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.tab:focus{outline-offset:2px;outline:2px solid #0000}}.tab:focus-visible,.tab:is(label:has(:checked:focus-visible)){outline-offset:-5px;outline:2px solid}.tab[disabled]{pointer-events:none;opacity:.4}.menu{--menu-active-fg:var(--color-neutral-content);--menu-active-bg:var(--color-neutral);flex-flow:column wrap;width:fit-content;padding:.5rem;font-size:.875rem;display:flex}.menu :where(li ul){white-space:nowrap;margin-inline-start:1rem;padding-inline-start:.5rem;position:relative}.menu :where(li ul):before{background-color:var(--color-base-content);opacity:.1;width:var(--border);content:"";inset-inline-start:0;position:absolute;top:.75rem;bottom:.75rem}.menu :where(li>.menu-dropdown:not(.menu-dropdown-show)){display:none}.menu :where(li:not(.menu-title)>:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--radius-field);text-align:start;text-wrap:balance;-webkit-user-select:none;user-select:none;grid-auto-columns:minmax(auto,max-content) auto max-content;grid-auto-flow:column;align-content:flex-start;align-items:center;gap:.5rem;padding-block:.375rem;padding-inline:.75rem;transition-property:color,background-color,box-shadow;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);display:grid}.menu :where(li>details>summary){--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li>details>summary){outline-offset:2px;outline:2px solid #0000}}.menu :where(li>details>summary)::-webkit-details-marker{display:none}:is(.menu :where(li>details>summary),.menu :where(li>.menu-dropdown-toggle)):after{content:"";transform-origin:50%;pointer-events:none;justify-self:flex-end;width:.375rem;height:.375rem;transition-property:rotate,translate;transition-duration:.2s;display:block;translate:0 -1px;rotate:-135deg;box-shadow:inset 2px 2px}.menu details{interpolate-size:allow-keywords;overflow:hidden}.menu details::details-content{block-size:0}@media (prefers-reduced-motion:no-preference){.menu details::details-content{transition-behavior:allow-discrete;transition-property:block-size,content-visibility;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}}.menu details[open]::details-content{block-size:auto}.menu :where(li>details[open]>summary):after,.menu :where(li>.menu-dropdown-toggle.menu-dropdown-show):after{translate:0 1px;rotate:45deg}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{background-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{color:var(--color-base-content);--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{outline-offset:2px;outline:2px solid #0000}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){background-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){outline-offset:2px;outline:2px solid #0000}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){box-shadow:inset 0 1px oklch(0% 0 0/.01),inset 0 -1px oklch(100% 0 0/.01)}.menu :where(li:empty){background-color:var(--color-base-content);opacity:.1;height:1px;margin:.5rem 1rem}.menu :where(li){flex-flow:column wrap;flex-shrink:0;align-items:stretch;display:flex;position:relative}.menu :where(li) .badge{justify-self:flex-end}.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{outline-offset:2px;outline:2px solid #0000}}.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{color:var(--menu-active-fg);background-color:var(--menu-active-bg);background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise)}:is(.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active):not(:is(.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active):active){box-shadow:0 2px calc(var(--depth)*3px)-2px var(--menu-active-bg)}.menu :where(li).menu-disabled{pointer-events:none;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li).menu-disabled{color:color-mix(in oklab,var(--color-base-content)20%,transparent)}}.menu .dropdown:focus-within .menu-dropdown-toggle:after{translate:0 1px;rotate:45deg}.menu .dropdown-content{margin-top:.5rem;padding:.5rem}.menu .dropdown-content:before{display:none}.dropdown{position-area:var(--anchor-v,bottom)var(--anchor-h,span-right);display:inline-block;position:relative}.dropdown>:not(:has(~[class*=dropdown-content])):focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.dropdown>:not(:has(~[class*=dropdown-content])):focus{outline-offset:2px;outline:2px solid #0000}}.dropdown .dropdown-content{position:absolute}.dropdown.dropdown-close .dropdown-content,.dropdown:not(details,.dropdown-open,.dropdown-hover:hover,:focus-within) .dropdown-content,.dropdown.dropdown-hover:not(:hover) [tabindex]:first-child:focus:not(:focus-visible)~.dropdown-content{transform-origin:top;opacity:0;display:none;scale:95%}.dropdown[popover],.dropdown .dropdown-content{z-index:999}@media (prefers-reduced-motion:no-preference){.dropdown[popover],.dropdown .dropdown-content{transition-behavior:allow-discrete;transition-property:opacity,scale,display;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);animation:.2s dropdown}}@starting-style{.dropdown[popover],.dropdown .dropdown-content{opacity:0;scale:95%}}:is(.dropdown:not(.dropdown-close).dropdown-open,.dropdown:not(.dropdown-close):not(.dropdown-hover):focus,.dropdown:not(.dropdown-close):focus-within)>[tabindex]:first-child{pointer-events:none}:is(.dropdown:not(.dropdown-close).dropdown-open,.dropdown:not(.dropdown-close):not(.dropdown-hover):focus,.dropdown:not(.dropdown-close):focus-within) .dropdown-content,.dropdown:not(.dropdown-close).dropdown-hover:hover .dropdown-content{opacity:1;scale:100%}.dropdown:is(details) summary::-webkit-details-marker{display:none}.dropdown:where([popover]){background:0 0}.dropdown[popover]{color:inherit;position:fixed}@supports not (position-area:bottom){.dropdown[popover]{margin:auto}.dropdown[popover].dropdown-close{transform-origin:top;opacity:0;display:none;scale:95%}.dropdown[popover].dropdown-open:not(:popover-open){transform-origin:top;opacity:0;display:none;scale:95%}.dropdown[popover]::backdrop{background-color:oklab(0% none none/.3)}}:is(.dropdown[popover].dropdown-close,.dropdown[popover]:not(.dropdown-open,:popover-open)){transform-origin:top;opacity:0;display:none;scale:95%}:where(.btn){width:unset}.btn{cursor:pointer;text-align:center;vertical-align:middle;outline-offset:2px;webkit-user-select:none;-webkit-user-select:none;user-select:none;padding-inline:var(--btn-p);color:var(--btn-fg);--tw-prose-links:var(--btn-fg);height:var(--size);font-size:var(--fontsize,.875rem);outline-color:var(--btn-color,var(--color-base-content));background-color:var(--btn-bg);background-size:auto,calc(var(--noise)*100%);background-image:none,var(--btn-noise);border-width:var(--border);border-style:solid;border-color:var(--btn-border);text-shadow:0 .5px oklch(100% 0 0/calc(var(--depth)*.15));touch-action:manipulation;box-shadow:0 .5px 0 .5px oklch(100% 0 0/calc(var(--depth)*6%))inset,var(--btn-shadow);--size:calc(var(--size-field,.25rem)*10);--btn-bg:var(--btn-color,var(--color-base-200));--btn-fg:var(--color-base-content);--btn-p:1rem;--btn-border:var(--btn-bg);border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-wrap:nowrap;flex-shrink:0;justify-content:center;align-items:center;gap:.375rem;font-weight:600;transition-property:color,background-color,border-color,box-shadow;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);display:inline-flex}@supports (color:color-mix(in lab, red, red)){.btn{--btn-border:color-mix(in oklab,var(--btn-bg),#000 calc(var(--depth)*5%))}}.btn{--btn-shadow:0 3px 2px -2px var(--btn-bg),0 4px 3px -2px var(--btn-bg)}@supports (color:color-mix(in lab, red, red)){.btn{--btn-shadow:0 3px 2px -2px color-mix(in oklab,var(--btn-bg)calc(var(--depth)*30%),#0000),0 4px 3px -2px color-mix(in oklab,var(--btn-bg)calc(var(--depth)*30%),#0000)}}.btn{--btn-noise:var(--fx-noise)}@media (hover:hover){.btn:hover{--btn-bg:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn:hover{--btn-bg:color-mix(in oklab,var(--btn-color,var(--color-base-200)),#000 7%)}}}.btn:focus-visible,.btn:has(:focus-visible){isolation:isolate;outline-width:2px;outline-style:solid}.btn:active:not(.btn-active){--btn-bg:var(--btn-color,var(--color-base-200));translate:0 .5px}@supports (color:color-mix(in lab, red, red)){.btn:active:not(.btn-active){--btn-bg:color-mix(in oklab,var(--btn-color,var(--color-base-200)),#000 5%)}}.btn:active:not(.btn-active){--btn-border:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn:active:not(.btn-active){--btn-border:color-mix(in oklab,var(--btn-color,var(--color-base-200)),#000 7%)}}.btn:active:not(.btn-active){--btn-shadow:0 0 0 0 oklch(0% 0 0/0),0 0 0 0 oklch(0% 0 0/0)}.btn:is(input[type=checkbox],input[type=radio]){appearance:none}.btn:is(input[type=checkbox],input[type=radio])[aria-label]:after{--tw-content:attr(aria-label);content:var(--tw-content)}.btn:where(input:checked:not(.filter .btn)){--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content);isolation:isolate}.loading{pointer-events:none;aspect-ratio:1;vertical-align:middle;width:calc(var(--size-selector,.25rem)*6);background-color:currentColor;display:inline-block;-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:100%;mask-size:100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.collapse{border-radius:var(--radius-box,1rem);isolation:isolate;grid-template-rows:max-content 0fr;grid-template-columns:minmax(0,1fr);width:100%;display:grid;position:relative;overflow:hidden}@media (prefers-reduced-motion:no-preference){.collapse{transition:grid-template-rows .2s}}.collapse>input:is([type=checkbox],[type=radio]){appearance:none;opacity:0;z-index:1;grid-row-start:1;grid-column-start:1;width:100%;min-height:1lh;padding:1rem;padding-inline-end:3rem;transition:background-color .2s ease-out}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close)),.collapse:not(.collapse-close):has(>input:is([type=checkbox],[type=radio]):checked){grid-template-rows:max-content 1fr}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>.collapse-content,.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){content-visibility:visible;min-height:fit-content}@supports not (content-visibility:visible){.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>.collapse-content,.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){visibility:visible}}.collapse:focus-visible,.collapse:has(>input:is([type=checkbox],[type=radio]):focus-visible),.collapse:has(summary:focus-visible){outline-color:var(--color-base-content);outline-offset:2px;outline-width:2px;outline-style:solid}.collapse:not(.collapse-close)>input[type=checkbox],.collapse:not(.collapse-close)>input[type=radio]:not(:checked),.collapse:not(.collapse-close)>.collapse-title{cursor:pointer}:is(.collapse[tabindex]:focus:not(.collapse-close,.collapse[open]),.collapse[tabindex]:focus-within:not(.collapse-close,.collapse[open]))>.collapse-title{cursor:unset}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>:where(.collapse-content),.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){padding-bottom:1rem}.collapse:is(details){width:100%}@media (prefers-reduced-motion:no-preference){.collapse:is(details)::details-content{transition:content-visibility .2s allow-discrete,visibility .2s allow-discrete,padding .2s ease-out,background-color .2s ease-out,height .2s;interpolate-size:allow-keywords;height:0}.collapse:is(details):where([open])::details-content{height:auto}}.collapse:is(details) summary{display:block;position:relative}.collapse:is(details) summary::-webkit-details-marker{display:none}.collapse:is(details)>.collapse-content{content-visibility:visible}.collapse:is(details) summary{outline:none}.list{flex-direction:column;font-size:.875rem;display:flex}.list .list-row{--list-grid-cols:minmax(0,auto)1fr;border-radius:var(--radius-box);word-break:break-word;grid-auto-flow:column;grid-template-columns:var(--list-grid-cols);gap:1rem;padding:1rem;display:grid;position:relative}:is(.list>:not(:last-child).list-row,.list>:not(:last-child) .list-row):after{content:"";border-bottom:var(--border)solid;inset-inline:var(--radius-box);border-color:var(--color-base-content);position:absolute;bottom:0}@supports (color:color-mix(in lab, red, red)){:is(.list>:not(:last-child).list-row,.list>:not(:last-child) .list-row):after{border-color:color-mix(in oklab,var(--color-base-content)5%,transparent)}}.toggle{border:var(--border)solid currentColor;color:var(--input-color);cursor:pointer;appearance:none;vertical-align:middle;webkit-user-select:none;-webkit-user-select:none;user-select:none;--radius-selector-max:calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector));border-radius:calc(var(--radius-selector) + min(var(--toggle-p),var(--radius-selector-max)) + min(var(--border),var(--radius-selector-max)));padding:var(--toggle-p);flex-shrink:0;grid-template-columns:0fr 1fr 1fr;place-content:center;display:inline-grid;position:relative;box-shadow:inset 0 1px}@supports (color:color-mix(in lab, red, red)){.toggle{box-shadow:0 1px color-mix(in oklab,currentColor calc(var(--depth)*10%),#0000)inset}}.toggle{--input-color:var(--color-base-content);transition:color .3s,grid-template-columns .2s}@supports (color:color-mix(in lab, red, red)){.toggle{--input-color:color-mix(in oklab,var(--color-base-content)50%,#0000)}}.toggle{--toggle-p:calc(var(--size)*.125);--size:calc(var(--size-selector,.25rem)*6);width:calc((var(--size)*2) - (var(--border) + var(--toggle-p))*2);height:var(--size)}.toggle>*{z-index:1;cursor:pointer;appearance:none;background-color:#0000;border:none;grid-column:2/span 1;grid-row-start:1;height:100%;padding:.125rem;transition:opacity .2s,rotate .4s}.toggle>:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.toggle>:focus{outline-offset:2px;outline:2px solid #0000}}.toggle>:nth-child(2){color:var(--color-base-100);rotate:none}.toggle>:nth-child(3){color:var(--color-base-100);opacity:0;rotate:-15deg}.toggle:has(:checked)>:nth-child(2){opacity:0;rotate:15deg}.toggle:has(:checked)>:nth-child(3){opacity:1;rotate:none}.toggle:before{aspect-ratio:1;border-radius:var(--radius-selector);--tw-content:"";content:var(--tw-content);height:100%;box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px currentColor;background-color:currentColor;grid-row-start:1;grid-column-start:2;transition:background-color .1s,translate .2s,inset-inline-start .2s;position:relative;inset-inline-start:0;translate:0}@supports (color:color-mix(in lab, red, red)){.toggle:before{box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px color-mix(in oklab,currentColor calc(var(--depth)*10%),#0000)}}.toggle:before{background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise)}@media (forced-colors:active){.toggle:before{outline-style:var(--tw-outline-style);outline-offset:calc(1px*-1);outline-width:1px}}@media print{.toggle:before{outline-offset:-1rem;outline:.25rem solid}}.toggle:focus-visible,.toggle:has(:focus-visible){outline-offset:2px;outline:2px solid}.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked){background-color:var(--color-base-100);--input-color:var(--color-base-content);grid-template-columns:1fr 1fr 0fr}:is(.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked)):before{background-color:currentColor}@starting-style{:is(.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked)):before{opacity:0}}.toggle:indeterminate{grid-template-columns:.5fr 1fr .5fr}.toggle:disabled{cursor:not-allowed;opacity:.3}.toggle:disabled:before{border:var(--border)solid currentColor;background-color:#0000}.input{cursor:text;border:var(--border)solid #0000;appearance:none;background-color:var(--color-base-100);vertical-align:middle;white-space:nowrap;width:clamp(3rem,20rem,100%);height:var(--size);font-size:max(var(--font-size,.875rem),.875rem);touch-action:manipulation;border-color:var(--input-color);box-shadow:0 1px var(--input-color)inset,0 -1px oklch(100% 0 0/calc(var(--depth)*.1))inset;border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-shrink:1;align-items:center;gap:.5rem;padding-inline:.75rem;display:inline-flex;position:relative}@supports (color:color-mix(in lab, red, red)){.input{box-shadow:0 1px color-mix(in oklab,var(--input-color)calc(var(--depth)*10%),#0000)inset,0 -1px oklch(100% 0 0/calc(var(--depth)*.1))inset}}.input{--size:calc(var(--size-field,.25rem)*10);--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.input{--input-color:color-mix(in oklab,var(--color-base-content)20%,#0000)}}.input:where(input){display:inline-flex}.input :where(input){appearance:none;background-color:#0000;border:none;width:100%;height:100%;display:inline-flex}.input :where(input):focus,.input :where(input):focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.input :where(input):focus,.input :where(input):focus-within{outline-offset:2px;outline:2px solid #0000}}.input :where(input[type=url]),.input :where(input[type=email]){direction:ltr}.input :where(input[type=date]){display:inline-flex}.input:focus,.input:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.input:focus,.input:focus-within{box-shadow:0 1px color-mix(in oklab,var(--input-color)calc(var(--depth)*10%),#0000)}}.input:focus,.input:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate;z-index:1}@media (pointer:coarse){@supports (-webkit-touch-callout:none){.input:focus,.input:focus-within{--font-size:1rem}}}.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{color:color-mix(in oklab,var(--color-base-content)40%,transparent)}}:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input)::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input)::placeholder{color:color-mix(in oklab,var(--color-base-content)20%,transparent)}}.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{box-shadow:none}.input:has(>input[disabled])>input[disabled]{cursor:not-allowed}.input::-webkit-date-and-time-value{text-align:inherit}.input[type=number]::-webkit-inner-spin-button{margin-block:-.75rem;margin-inline-end:-.75rem}.input::-webkit-calendar-picker-indicator{position:absolute;inset-inline-end:.75em}.input:has(>input[type=date]) :where(input[type=date]){webkit-appearance:none;appearance:none;display:inline-flex}.input:has(>input[type=date]) input[type=date]::-webkit-calendar-picker-indicator{cursor:pointer;width:1em;height:1em;position:absolute;inset-inline-end:.75em}.indicator{width:max-content;display:inline-flex;position:relative}.indicator :where(.indicator-item){z-index:1;white-space:nowrap;top:var(--indicator-t,0);bottom:var(--indicator-b,auto);left:var(--indicator-s,auto);right:var(--indicator-e,0);translate:var(--indicator-x,50%)var(--indicator-y,-50%);position:absolute}.table{border-collapse:separate;--tw-border-spacing-x:calc(.25rem*0);--tw-border-spacing-y:calc(.25rem*0);width:100%;border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y);border-radius:var(--radius-box);text-align:left;font-size:.875rem;position:relative}.table:where(:dir(rtl),[dir=rtl],[dir=rtl] *){text-align:right}@media (hover:hover){:is(.table tr.row-hover,.table tr.row-hover:nth-child(2n)):hover{background-color:var(--color-base-200)}}.table :where(th,td){vertical-align:middle;padding-block:.75rem;padding-inline:1rem}.table :where(thead,tfoot){white-space:nowrap;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(thead,tfoot){color:color-mix(in oklab,var(--color-base-content)60%,transparent)}}.table :where(thead,tfoot){font-size:.875rem;font-weight:600}.table :where(tfoot tr:first-child :is(td,th)){border-top:var(--border)solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(tfoot tr:first-child :is(td,th)){border-top:var(--border)solid color-mix(in oklch,var(--color-base-content)5%,#0000)}}.table :where(.table-pin-rows thead tr){z-index:1;background-color:var(--color-base-100);position:sticky;top:0}.table :where(.table-pin-rows tfoot tr){z-index:1;background-color:var(--color-base-100);position:sticky;bottom:0}.table :where(.table-pin-cols tr th){background-color:var(--color-base-100);position:sticky;left:0;right:0}.table :where(thead tr :is(td,th),tbody tr:not(:last-child) :is(td,th)){border-bottom:var(--border)solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(thead tr :is(td,th),tbody tr:not(:last-child) :is(td,th)){border-bottom:var(--border)solid color-mix(in oklch,var(--color-base-content)5%,#0000)}}.steps{counter-reset:step;grid-auto-columns:1fr;grid-auto-flow:column;display:inline-grid;overflow:auto hidden}.steps .step{text-align:center;--step-bg:var(--color-base-300);--step-fg:var(--color-base-content);grid-template-rows:40px 1fr;grid-template-columns:auto;place-items:center;min-width:4rem;display:grid}.steps .step:before{width:100%;height:.5rem;color:var(--step-bg);background-color:var(--step-bg);content:"";border:1px solid;grid-row-start:1;grid-column-start:1;margin-inline-start:-100%;top:0}.steps .step>.step-icon,.steps .step:not(:has(.step-icon)):after{--tw-content:counter(step);content:var(--tw-content);counter-increment:step;z-index:1;color:var(--step-fg);background-color:var(--step-bg);border:1px solid var(--step-bg);border-radius:3.40282e38px;grid-row-start:1;grid-column-start:1;place-self:center;place-items:center;width:2rem;height:2rem;display:grid;position:relative}.steps .step:first-child:before{--tw-content:none;content:var(--tw-content)}.steps .step[data-content]:after{--tw-content:attr(data-content);content:var(--tw-content)}.range{appearance:none;webkit-appearance:none;--range-thumb:var(--color-base-100);--range-thumb-size:calc(var(--size-selector,.25rem)*6);--range-progress:currentColor;--range-fill:1;--range-p:.25rem;--range-bg:currentColor}@supports (color:color-mix(in lab, red, red)){.range{--range-bg:color-mix(in oklab,currentColor 10%,#0000)}}.range{cursor:pointer;vertical-align:middle;--radius-selector-max:calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector));border-radius:calc(var(--radius-selector) + min(var(--range-p),var(--radius-selector-max)));width:clamp(3rem,20rem,100%);height:var(--range-thumb-size);background-color:#0000;border:none;overflow:hidden}[dir=rtl] .range{--range-dir:-1}.range:focus{outline:none}.range:focus-visible{outline-offset:2px;outline:2px solid}.range::-webkit-slider-runnable-track{background-color:var(--range-bg);border-radius:var(--radius-selector);width:100%;height:calc(var(--range-thumb-size)*.5)}@media (forced-colors:active){.range::-webkit-slider-runnable-track{border:1px solid}.range::-moz-range-track{border:1px solid}}.range::-webkit-slider-thumb{box-sizing:border-box;border-radius:calc(var(--radius-selector) + min(var(--range-p),var(--radius-selector-max)));background-color:var(--range-thumb);height:var(--range-thumb-size);width:var(--range-thumb-size);border:var(--range-p)solid;appearance:none;webkit-appearance:none;color:var(--range-progress);box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px currentColor,0 0 0 2rem var(--range-thumb)inset,calc((var(--range-dir,1)*-100rem) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100rem*var(--range-fill));position:relative;top:50%;transform:translateY(-50%)}@supports (color:color-mix(in lab, red, red)){.range::-webkit-slider-thumb{box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px color-mix(in oklab,currentColor calc(var(--depth)*10%),#0000),0 0 0 2rem var(--range-thumb)inset,calc((var(--range-dir,1)*-100rem) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100rem*var(--range-fill))}}.range::-moz-range-track{background-color:var(--range-bg);border-radius:var(--radius-selector);width:100%;height:calc(var(--range-thumb-size)*.5)}.range::-moz-range-thumb{box-sizing:border-box;border-radius:calc(var(--radius-selector) + min(var(--range-p),var(--radius-selector-max)));height:var(--range-thumb-size);width:var(--range-thumb-size);border:var(--range-p)solid;color:var(--range-progress);box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px currentColor,0 0 0 2rem var(--range-thumb)inset,calc((var(--range-dir,1)*-100rem) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100rem*var(--range-fill));background-color:currentColor;position:relative;top:50%}@supports (color:color-mix(in lab, red, red)){.range::-moz-range-thumb{box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px color-mix(in oklab,currentColor calc(var(--depth)*10%),#0000),0 0 0 2rem var(--range-thumb)inset,calc((var(--range-dir,1)*-100rem) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100rem*var(--range-fill))}}.range:disabled{cursor:not-allowed;opacity:.3}.chat-bubble{border-radius:var(--radius-field);background-color:var(--color-base-300);width:fit-content;color:var(--color-base-content);grid-row-end:3;min-width:2.5rem;max-width:90%;min-height:2rem;padding-block:.5rem;padding-inline:1rem;display:block;position:relative}.chat-bubble:before{background-color:inherit;content:"";width:.75rem;height:.75rem;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-image:var(--mask-chat);-webkit-mask-image:var(--mask-chat);mask-image:var(--mask-chat);position:absolute;bottom:0;-webkit-mask-position:0 -1px;mask-position:0 -1px;-webkit-mask-size:.8125rem;mask-size:.8125rem}.select{border:var(--border)solid #0000;appearance:none;background-color:var(--color-base-100);vertical-align:middle;width:clamp(3rem,20rem,100%);height:var(--size);touch-action:manipulation;white-space:nowrap;text-overflow:ellipsis;box-shadow:0 1px var(--input-color)inset,0 -1px oklch(100% 0 0/calc(var(--depth)*.1))inset;background-image:linear-gradient(45deg,#0000 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,#0000 50%);background-position:calc(100% - 20px) calc(1px + 50%),calc(100% - 16.1px) calc(1px + 50%);background-repeat:no-repeat;background-size:4px 4px,4px 4px;border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-shrink:1;align-items:center;gap:.375rem;padding-inline:.75rem 1.75rem;font-size:.875rem;display:inline-flex;position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.select{box-shadow:0 1px color-mix(in oklab,var(--input-color)calc(var(--depth)*10%),#0000)inset,0 -1px oklch(100% 0 0/calc(var(--depth)*.1))inset}}.select{border-color:var(--input-color);--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.select{--input-color:color-mix(in oklab,var(--color-base-content)20%,#0000)}}.select{--size:calc(var(--size-field,.25rem)*10)}[dir=rtl] .select{background-position:12px calc(1px + 50%),16px calc(1px + 50%)}[dir=rtl] .select::picker(select){translate:.5rem}[dir=rtl] .select select::picker(select){translate:.5rem}.select[multiple]{background-image:none;height:auto;padding-block:.75rem;padding-inline-end:.75rem;overflow:auto}.select select{appearance:none;width:calc(100% + 2.75rem);height:calc(100% - calc(var(--border)*2));background:inherit;border-radius:inherit;border-style:none;align-items:center;margin-inline:-.75rem -1.75rem;padding-inline:.75rem 1.75rem}.select select:focus,.select select:focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.select select:focus,.select select:focus-within{outline-offset:2px;outline:2px solid #0000}}.select select:not(:last-child){background-image:none;margin-inline-end:-1.375rem}.select:focus,.select:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.select:focus,.select:focus-within{box-shadow:0 1px color-mix(in oklab,var(--input-color)calc(var(--depth)*10%),#0000)}}.select:focus,.select:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate;z-index:1}.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select{cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select{color:color-mix(in oklab,var(--color-base-content)40%,transparent)}}:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select)::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select)::placeholder{color:color-mix(in oklab,var(--color-base-content)20%,transparent)}}.select:has(>select[disabled])>select[disabled]{cursor:not-allowed}@supports (appearance:base-select){.select,.select select{appearance:base-select}:is(.select,.select select)::picker(select){appearance:base-select}}:is(.select,.select select)::picker(select){color:inherit;border:var(--border)solid var(--color-base-200);border-radius:var(--radius-box);background-color:inherit;max-height:min(24rem,70dvh);box-shadow:0 2px calc(var(--depth)*3px)-2px oklch(0% 0 0/.2);box-shadow:0 20px 25px -5px rgb(0 0 0/calc(var(--depth)*.1)),0 8px 10px -6px rgb(0 0 0/calc(var(--depth)*.1));margin-block:.5rem;margin-inline:.5rem;padding:.5rem;translate:-.5rem}:is(.select,.select select)::picker-icon{display:none}:is(.select,.select select) optgroup{padding-top:.5em}:is(.select,.select select) optgroup option:first-child{margin-top:.5em}:is(.select,.select select) option{border-radius:var(--radius-field);white-space:normal;padding-block:.375rem;padding-inline:.75rem;transition-property:color,background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{background-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{outline-offset:2px;outline:2px solid #0000}}:is(.select,.select select) option:not(:disabled):active{background-color:var(--color-neutral);color:var(--color-neutral-content);box-shadow:0 2px calc(var(--depth)*3px)-2px var(--color-neutral)}.timeline{display:flex;position:relative}.timeline>li{grid-template-rows:var(--timeline-row-start,minmax(0,1fr))auto var(--timeline-row-end,minmax(0,1fr));grid-template-columns:var(--timeline-col-start,minmax(0,1fr))auto var(--timeline-col-end,minmax(0,1fr));flex-shrink:0;align-items:center;display:grid;position:relative}.timeline>li>hr{border:none;width:100%}.timeline>li>hr:first-child{grid-row-start:2;grid-column-start:1}.timeline>li>hr:last-child{grid-area:2/3/auto/none}@media print{.timeline>li>hr{border:.1px solid var(--color-base-300)}}.timeline :where(hr){background-color:var(--color-base-300);height:.25rem}.timeline:has(.timeline-middle hr):first-child{border-start-start-radius:0;border-start-end-radius:var(--radius-selector);border-end-end-radius:var(--radius-selector);border-end-start-radius:0}.timeline:has(.timeline-middle hr):last-child,.timeline:not(:has(.timeline-middle)) :first-child hr:last-child{border-start-start-radius:var(--radius-selector);border-start-end-radius:0;border-end-end-radius:0;border-end-start-radius:var(--radius-selector)}.timeline:not(:has(.timeline-middle)) :last-child hr:first-child{border-start-start-radius:0;border-start-end-radius:var(--radius-selector);border-end-end-radius:var(--radius-selector);border-end-start-radius:0}.swap{cursor:pointer;vertical-align:middle;webkit-user-select:none;-webkit-user-select:none;user-select:none;place-content:center;display:inline-grid;position:relative}.swap input{appearance:none;border:none}.swap>*{grid-row-start:1;grid-column-start:1}@media (prefers-reduced-motion:no-preference){.swap>*{transition-property:transform,rotate,opacity;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}}.swap .swap-on,.swap .swap-indeterminate,.swap input:indeterminate~.swap-on,.swap input:is(:checked,:indeterminate)~.swap-off{opacity:0}.swap input:checked~.swap-on,.swap input:indeterminate~.swap-indeterminate{opacity:1;backface-visibility:visible}.checkbox{border:var(--border)solid var(--input-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.checkbox{border:var(--border)solid var(--input-color,color-mix(in oklab,var(--color-base-content)20%,#0000))}}.checkbox{cursor:pointer;appearance:none;border-radius:var(--radius-selector);vertical-align:middle;color:var(--color-base-content);box-shadow:0 1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 0 #0000 inset,0 0 #0000;--size:calc(var(--size-selector,.25rem)*6);width:var(--size);height:var(--size);background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise);flex-shrink:0;padding:.25rem;transition:background-color .2s,box-shadow .2s;display:inline-block;position:relative}.checkbox:before{--tw-content:"";content:var(--tw-content);opacity:0;clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 80%,70% 80%,70% 100%);width:100%;height:100%;box-shadow:0px 3px 0 0px oklch(100% 0 0/calc(var(--depth)*.1))inset;background-color:currentColor;font-size:1rem;line-height:.75;transition:clip-path .3s .1s,opacity .1s .1s,rotate .3s .1s,translate .3s .1s;display:block;rotate:45deg}.checkbox:focus-visible{outline:2px solid var(--input-color,currentColor);outline-offset:2px}.checkbox:checked,.checkbox[aria-checked=true]{background-color:var(--input-color,#0000);box-shadow:0 0 #0000 inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px oklch(0% 0 0/calc(var(--depth)*.1))}:is(.checkbox:checked,.checkbox[aria-checked=true]):before{clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 0%,70% 0%,70% 100%);opacity:1}@media (forced-colors:active){:is(.checkbox:checked,.checkbox[aria-checked=true]):before{--tw-content:"✔︎";clip-path:none;background-color:#0000;rotate:none}}@media print{:is(.checkbox:checked,.checkbox[aria-checked=true]):before{--tw-content:"✔︎";clip-path:none;background-color:#0000;rotate:none}}.checkbox:indeterminate{background-color:var(--input-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.checkbox:indeterminate{background-color:var(--input-color,color-mix(in oklab,var(--color-base-content)20%,#0000))}}.checkbox:indeterminate:before{opacity:1;clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 80%,80% 80%,80% 100%);translate:0 -35%;rotate:none}.radio{cursor:pointer;appearance:none;vertical-align:middle;border:var(--border)solid var(--input-color,currentColor);border-radius:3.40282e38px;flex-shrink:0;padding:.25rem;display:inline-block;position:relative}@supports (color:color-mix(in lab, red, red)){.radio{border:var(--border)solid var(--input-color,color-mix(in srgb,currentColor 20%,#0000))}}.radio{box-shadow:0 1px oklch(0% 0 0/calc(var(--depth)*.1))inset;--size:calc(var(--size-selector,.25rem)*6);width:var(--size);height:var(--size);color:var(--input-color,currentColor)}.radio:before{--tw-content:"";content:var(--tw-content);background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise);border-radius:3.40282e38px;width:100%;height:100%;display:block}.radio:focus-visible{outline:2px solid}.radio:checked,.radio[aria-checked=true]{background-color:var(--color-base-100);border-color:currentColor}@media (prefers-reduced-motion:no-preference){.radio:checked,.radio[aria-checked=true]{animation:.2s ease-out radio}}:is(.radio:checked,.radio[aria-checked=true]):before{box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px oklch(0% 0 0/calc(var(--depth)*.1));background-color:currentColor}@media (forced-colors:active){:is(.radio:checked,.radio[aria-checked=true]):before{outline-style:var(--tw-outline-style);outline-offset:calc(1px*-1);outline-width:1px}}@media print{:is(.radio:checked,.radio[aria-checked=true]):before{outline-offset:-1rem;outline:.25rem solid}}.navbar{align-items:center;width:100%;min-height:4rem;padding:.5rem;display:flex}.card{border-radius:var(--radius-box);outline-offset:2px;outline:0 solid #0000;flex-direction:column;transition:outline .2s ease-in-out;display:flex;position:relative}.card:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.card:focus{outline-offset:2px;outline:2px solid #0000}}.card:focus-visible{outline-color:currentColor}.card :where(figure:first-child){border-start-start-radius:inherit;border-start-end-radius:inherit;border-end-end-radius:unset;border-end-start-radius:unset;overflow:hidden}.card :where(figure:last-child){border-start-start-radius:unset;border-start-end-radius:unset;border-end-end-radius:inherit;border-end-start-radius:inherit;overflow:hidden}.card figure{justify-content:center;align-items:center;display:flex}.card:has(>input:is(input[type=checkbox],input[type=radio])){cursor:pointer;-webkit-user-select:none;user-select:none}.card:has(>:checked){outline:2px solid}.stats{border-radius:var(--radius-box);grid-auto-flow:column;display:inline-grid;position:relative;overflow-x:auto}.progress{appearance:none;border-radius:var(--radius-box);background-color:currentColor;width:100%;height:.5rem;position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.progress{background-color:color-mix(in oklab,currentcolor 20%,transparent)}}.progress{color:var(--color-base-content)}.progress:indeterminate{background-image:repeating-linear-gradient(90deg,currentColor -1% 10%,#0000 10% 90%);background-position-x:15%;background-size:200%}@media (prefers-reduced-motion:no-preference){.progress:indeterminate{animation:5s ease-in-out infinite progress}}@supports ((-moz-appearance:none)){.progress:indeterminate::-moz-progress-bar{background-color:#0000}@media (prefers-reduced-motion:no-preference){.progress:indeterminate::-moz-progress-bar{background-image:repeating-linear-gradient(90deg,currentColor -1% 10%,#0000 10% 90%);background-position-x:15%;background-size:200%;animation:5s ease-in-out infinite progress}}.progress::-moz-progress-bar{border-radius:var(--radius-box);background-color:currentColor}}@supports ((-webkit-appearance:none)){.progress::-webkit-progress-bar{border-radius:var(--radius-box);background-color:#0000}.progress::-webkit-progress-value{border-radius:var(--radius-box);background-color:currentColor}}.stack{grid-template-rows:3px 4px 1fr 4px 3px;grid-template-columns:3px 4px 1fr 4px 3px;display:inline-grid}.stack>*{width:100%;height:100%}.stack>:nth-child(n+2){opacity:.7;width:100%}.stack>:nth-child(2){z-index:2;opacity:.9}.stack>:first-child{z-index:3;width:100%}.timeline-end{grid-area:3/1/4/4;place-self:flex-start center;margin:.25rem}.timeline-start{grid-area:1/1/2/4;place-self:flex-end center;margin:.25rem}.timeline-middle{grid-row-start:2;grid-column-start:2}.stat-value{white-space:nowrap;grid-column-start:1;font-size:2rem;font-weight:800}.stat-desc{white-space:nowrap;color:var(--color-base-content);grid-column-start:1}@supports (color:color-mix(in lab, red, red)){.stat-desc{color:color-mix(in oklab,var(--color-base-content)60%,transparent)}}.stat-desc{font-size:.75rem}.stat-title{white-space:nowrap;color:var(--color-base-content);grid-column-start:1}@supports (color:color-mix(in lab, red, red)){.stat-title{color:color-mix(in oklab,var(--color-base-content)60%,transparent)}}.stat-title{font-size:.75rem}.chat-footer{grid-row-start:3;gap:.25rem;font-size:.6875rem;display:flex}.chat-header{grid-row-start:1;gap:.25rem;font-size:.6875rem;display:flex}.filter{flex-wrap:wrap;display:flex}.filter input[type=radio]{width:auto}.filter input{opacity:1;transition:margin .1s,opacity .3s,padding .3s,border-width .1s;overflow:hidden;scale:1}.filter input:not(:last-child){margin-inline-end:.25rem}.filter input.filter-reset{aspect-ratio:1}.filter input.filter-reset:after{--tw-content:"×";content:var(--tw-content)}.filter:not(:has(input:checked:not(.filter-reset))) .filter-reset,.filter:not(:has(input:checked:not(.filter-reset))) input[type=reset],.filter:has(input:checked:not(.filter-reset)) input:not(:checked,.filter-reset,input[type=reset]){opacity:0;border-width:0;width:0;margin-inline:0;padding-inline:0;scale:0}.label{white-space:nowrap;color:currentColor;align-items:center;gap:.375rem;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.label{color:color-mix(in oklab,currentcolor 60%,transparent)}}.label:has(input){cursor:pointer}.label:is(.input>*,.select>*){white-space:nowrap;height:calc(100% - .5rem);font-size:inherit;align-items:center;padding-inline:.75rem;display:flex}.label:is(.input>*,.select>*):first-child{border-inline-end:var(--border)solid currentColor;margin-inline:-.75rem .75rem}@supports (color:color-mix(in lab, red, red)){.label:is(.input>*,.select>*):first-child{border-inline-end:var(--border)solid color-mix(in oklab,currentColor 10%,#0000)}}.label:is(.input>*,.select>*):last-child{border-inline-start:var(--border)solid currentColor;margin-inline:.75rem -.75rem}@supports (color:color-mix(in lab, red, red)){.label:is(.input>*,.select>*):last-child{border-inline-start:var(--border)solid color-mix(in oklab,currentColor 10%,#0000)}}.fieldset-legend{color:var(--color-base-content);justify-content:space-between;align-items:center;gap:.5rem;margin-bottom:-.25rem;padding-block:.5rem;font-weight:600;display:flex}.status{aspect-ratio:1;border-radius:var(--radius-selector);background-color:var(--color-base-content);width:.5rem;height:.5rem;display:inline-block}@supports (color:color-mix(in lab, red, red)){.status{background-color:color-mix(in oklab,var(--color-base-content)20%,transparent)}}.status{vertical-align:middle;color:#0000004d;background-position:50%;background-repeat:no-repeat}@supports (color:color-mix(in lab, red, red)){.status{color:color-mix(in oklab,var(--color-black)30%,transparent)}}.status{background-image:radial-gradient(circle at 35% 30%,oklch(1 0 0/calc(var(--depth)*.5)),#0000);box-shadow:0 2px 3px -1px}@supports (color:color-mix(in lab, red, red)){.status{box-shadow:0 2px 3px -1px color-mix(in oklab,currentColor calc(var(--depth)*100%),#0000)}}.badge{border-radius:var(--radius-selector);vertical-align:middle;color:var(--badge-fg);border:var(--border)solid var(--badge-color,var(--color-base-200));width:fit-content;padding-inline:calc(.25rem*3 - var(--border));background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise);background-color:var(--badge-bg);--badge-bg:var(--badge-color,var(--color-base-100));--badge-fg:var(--color-base-content);--size:calc(var(--size-selector,.25rem)*6);height:var(--size);justify-content:center;align-items:center;gap:.5rem;font-size:.875rem;display:inline-flex}.tabs{--tabs-height:auto;--tabs-direction:row;--tab-height:calc(var(--size-field,.25rem)*10);height:var(--tabs-height);flex-wrap:wrap;flex-direction:var(--tabs-direction);display:flex}.footer{grid-auto-flow:row;place-items:start;gap:2.5rem 1rem;width:100%;font-size:.875rem;line-height:1.25rem;display:grid}.footer>*{place-items:start;gap:.5rem;display:grid}.footer.footer-center{text-align:center;grid-auto-flow:column dense;place-items:center}.footer.footer-center>*{place-items:center}.stat{grid-template-columns:repeat(1,1fr);column-gap:1rem;width:100%;padding-block:1rem;padding-inline:1.5rem;display:inline-grid}.stat:not(:last-child){border-inline-end:var(--border)dashed currentColor}@supports (color:color-mix(in lab, red, red)){.stat:not(:last-child){border-inline-end:var(--border)dashed color-mix(in oklab,currentColor 10%,#0000)}}.stat:not(:last-child){border-block-end:none}.navbar-end{justify-content:flex-end;align-items:center;width:50%;display:inline-flex}.navbar-start{justify-content:flex-start;align-items:center;width:50%;display:inline-flex}.card-body{padding:var(--card-p,1.5rem);font-size:var(--card-fs,.875rem);flex-direction:column;flex:auto;gap:.5rem;display:flex}.card-body :where(p){flex-grow:1}.alert{--alert-border-color:var(--color-base-200);border-radius:var(--radius-box);color:var(--color-base-content);background-color:var(--alert-color,var(--color-base-200));text-align:start;background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise);box-shadow:0 3px 0 -2px oklch(100% 0 0/calc(var(--depth)*.08))inset,0 1px #000,0 4px 3px -2px oklch(0% 0 0/calc(var(--depth)*.08));border-style:solid;grid-template-columns:auto;grid-auto-flow:column;justify-content:start;place-items:center start;gap:1rem;padding-block:.75rem;padding-inline:1rem;font-size:.875rem;line-height:1.25rem;display:grid}@supports (color:color-mix(in lab, red, red)){.alert{box-shadow:0 3px 0 -2px oklch(100% 0 0/calc(var(--depth)*.08))inset,0 1px color-mix(in oklab,color-mix(in oklab,#000 20%,var(--alert-color,var(--color-base-200)))calc(var(--depth)*20%),#0000),0 4px 3px -2px oklch(0% 0 0/calc(var(--depth)*.08))}}.alert:has(:nth-child(2)){grid-template-columns:auto minmax(auto,1fr)}.fieldset{grid-template-columns:1fr;grid-auto-rows:max-content;gap:.375rem;padding-block:.25rem;font-size:.75rem;display:grid}.card-title{font-size:var(--cardtitle-fs,1.125rem);align-items:center;gap:.5rem;font-weight:600;display:flex}.chat{--mask-chat:url("data:image/svg+xml,%3csvg width='13' height='13' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='M0 11.5004C0 13.0004 2 13.0004 2 13.0004H12H13V0.00036329L12.5 0C12.5 0 11.977 2.09572 11.8581 2.50033C11.6075 3.35237 10.9149 4.22374 9 5.50036C6 7.50036 0 10.0004 0 11.5004Z'/%3e%3c/svg%3e");column-gap:.75rem;padding-block:.25rem;display:grid}.link{cursor:pointer;text-decoration-line:underline}.link:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.link:focus{outline-offset:2px;outline:2px solid #0000}}.link:focus-visible{outline-offset:2px;outline:2px solid}.btn-primary{--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content)}}@layer daisyui.l1.l2{.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal{pointer-events:auto;visibility:visible;opacity:1;transition:visibility 0s allow-discrete,background-color .3s ease-out,opacity .1s ease-out;background-color:oklch(0% 0 0/.4)}:is(.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal) .modal-box{opacity:1;translate:0;scale:1}:root:has(:is(.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal)){--page-has-backdrop:1;--page-overflow:hidden;--page-scroll-bg:var(--page-scroll-bg-on);--page-scroll-gutter:stable;--page-scroll-transition:var(--page-scroll-transition-on);animation:forwards set-page-has-scroll;animation-timeline:scroll()}@starting-style{.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal{opacity:0}}.btn:disabled:not(.btn-link,.btn-ghost){background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn:disabled:not(.btn-link,.btn-ghost){background-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.btn:disabled:not(.btn-link,.btn-ghost){box-shadow:none}.btn:disabled{pointer-events:none;--btn-border:#0000;--btn-noise:none;--btn-fg:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn:disabled{--btn-fg:color-mix(in oklch,var(--color-base-content)20%,#0000)}}.btn[disabled]:not(.btn-link,.btn-ghost){background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn[disabled]:not(.btn-link,.btn-ghost){background-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.btn[disabled]:not(.btn-link,.btn-ghost){box-shadow:none}.btn[disabled]{pointer-events:none;--btn-border:#0000;--btn-noise:none;--btn-fg:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn[disabled]{--btn-fg:color-mix(in oklch,var(--color-base-content)20%,#0000)}}@media (prefers-reduced-motion:no-preference){.collapse[open].collapse-arrow>.collapse-title:after,.collapse.collapse-open.collapse-arrow>.collapse-title:after{transform:translateY(-50%)rotate(225deg)}}.collapse.collapse-open.collapse-plus>.collapse-title:after{--tw-content:"−";content:var(--tw-content)}:is(.collapse[tabindex].collapse-arrow:focus:not(.collapse-close),.collapse.collapse-arrow[tabindex]:focus-within:not(.collapse-close))>.collapse-title:after,.collapse.collapse-arrow:not(.collapse-close)>input:is([type=checkbox],[type=radio]):checked~.collapse-title:after{transform:translateY(-50%)rotate(225deg)}.collapse[open].collapse-plus>.collapse-title:after,.collapse[tabindex].collapse-plus:focus:not(.collapse-close)>.collapse-title:after,.collapse.collapse-plus:not(.collapse-close)>input:is([type=checkbox],[type=radio]):checked~.collapse-title:after{--tw-content:"−";content:var(--tw-content)}.list .list-row:has(.list-col-grow:first-child){--list-grid-cols:1fr}.list .list-row:has(.list-col-grow:nth-child(2)){--list-grid-cols:minmax(0,auto)1fr}.list .list-row:has(.list-col-grow:nth-child(3)){--list-grid-cols:minmax(0,auto)minmax(0,auto)1fr}.list .list-row:has(.list-col-grow:nth-child(4)){--list-grid-cols:minmax(0,auto)minmax(0,auto)minmax(0,auto)1fr}.list .list-row:has(.list-col-grow:nth-child(5)){--list-grid-cols:minmax(0,auto)minmax(0,auto)minmax(0,auto)minmax(0,auto)1fr}.list .list-row:has(.list-col-grow:nth-child(6)){--list-grid-cols:minmax(0,auto)minmax(0,auto)minmax(0,auto)minmax(0,auto)minmax(0,auto)1fr}.list .list-row>:not(.list-col-wrap){grid-row-start:1}.steps .step-neutral+.step-neutral:before,.steps .step-neutral:after,.steps .step-neutral>.step-icon{--step-bg:var(--color-neutral);--step-fg:var(--color-neutral-content)}.steps .step-primary+.step-primary:before,.steps .step-primary:after,.steps .step-primary>.step-icon{--step-bg:var(--color-primary);--step-fg:var(--color-primary-content)}.steps .step-secondary+.step-secondary:before,.steps .step-secondary:after,.steps .step-secondary>.step-icon{--step-bg:var(--color-secondary);--step-fg:var(--color-secondary-content)}.steps .step-accent+.step-accent:before,.steps .step-accent:after,.steps .step-accent>.step-icon{--step-bg:var(--color-accent);--step-fg:var(--color-accent-content)}.steps .step-info+.step-info:before,.steps .step-info:after,.steps .step-info>.step-icon{--step-bg:var(--color-info);--step-fg:var(--color-info-content)}.steps .step-success+.step-success:before,.steps .step-success:after,.steps .step-success>.step-icon{--step-bg:var(--color-success);--step-fg:var(--color-success-content)}.steps .step-warning+.step-warning:before,.steps .step-warning:after,.steps .step-warning>.step-icon{--step-bg:var(--color-warning);--step-fg:var(--color-warning-content)}.steps .step-error+.step-error:before,.steps .step-error:after,.steps .step-error>.step-icon{--step-bg:var(--color-error);--step-fg:var(--color-error-content)}.checkbox:disabled,.radio:disabled{cursor:not-allowed;opacity:.2}:where(.navbar){position:relative}.chat-end{grid-template-columns:1fr auto;place-items:end}.chat-end .chat-header,.chat-end .chat-footer{grid-column-start:1}.chat-end .chat-image{grid-column-start:2}.chat-end .chat-bubble{border-end-end-radius:0;grid-column-start:1}.chat-end .chat-bubble:before{inset-inline-start:100%;transform:rotateY(180deg)}[dir=rtl] :is(.chat-end .chat-bubble):before{transform:rotateY(0)}.chat-start{grid-template-columns:auto 1fr;place-items:start}.chat-start .chat-header,.chat-start .chat-footer{grid-column-start:2}.chat-start .chat-image{grid-column-start:1}.chat-start .chat-bubble{border-end-start-radius:0;grid-column-start:2}.chat-start .chat-bubble:before{inset-inline-start:-.75rem;transform:rotateY(0)}[dir=rtl] :is(.chat-start .chat-bubble):before{transform:rotateY(180deg)}:is(.stack,.stack.stack-bottom)>*{grid-area:3/3/6/4}:is(.stack,.stack.stack-bottom)>:nth-child(2){grid-area:2/2/5/5}:is(.stack,.stack.stack-bottom)>:first-child{grid-area:1/1/4/6}.stack.stack-top>*{grid-area:1/3/4/4}.stack.stack-top>:nth-child(2){grid-area:2/2/5/5}.stack.stack-top>:first-child{grid-area:3/1/6/6}.stack.stack-start>*{grid-area:3/1/4/4}.stack.stack-start>:nth-child(2){grid-area:2/2/5/5}.stack.stack-start>:first-child{grid-area:1/3/6/6}.stack.stack-end>*{grid-area:3/3/4/6}.stack.stack-end>:nth-child(2){grid-area:2/2/5/5}.stack.stack-end>:first-child{grid-area:1/1/6/4}.timeline-vertical{flex-direction:column}.timeline-vertical>li{--timeline-row-start:minmax(0,1fr);--timeline-row-end:minmax(0,1fr);justify-items:center}.timeline-vertical>li>hr{width:.25rem;height:100%}.timeline-vertical>li>hr:first-child{grid-row-start:1;grid-column-start:2}.timeline-vertical>li>hr:last-child{grid-area:3/2/none}.timeline-vertical .timeline-start{grid-area:1/1/4/2;place-self:center flex-end}.timeline-vertical .timeline-end{grid-area:1/3/4/4;place-self:center flex-start}.timeline-vertical:has(.timeline-middle)>li>hr:first-child{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:var(--radius-selector);border-bottom-left-radius:var(--radius-selector)}.timeline-vertical:has(.timeline-middle)>li>hr:last-child,.timeline-vertical:not(:has(.timeline-middle)) :first-child>hr:last-child{border-top-left-radius:var(--radius-selector);border-top-right-radius:var(--radius-selector);border-bottom-right-radius:0;border-bottom-left-radius:0}.timeline-vertical:not(:has(.timeline-middle)) :last-child>hr:first-child{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:var(--radius-selector);border-bottom-left-radius:var(--radius-selector)}.timeline-vertical.timeline-snap-icon>li{--timeline-col-start:minmax(0,1fr);--timeline-row-start:.5rem}.timeline-compact{--timeline-row-start:0}.timeline-compact .timeline-start{grid-area:3/1/4/4;place-self:flex-start center}.timeline-compact li:has(.timeline-start) .timeline-end{grid-row-start:auto;grid-column-start:none}.timeline-compact.timeline-vertical>li{--timeline-col-start:0}.timeline-compact.timeline-vertical .timeline-start{grid-area:1/3/4/4;place-self:center flex-start}.timeline-compact.timeline-vertical li:has(.timeline-start) .timeline-end{grid-row-start:none;grid-column-start:auto}.input-sm{--size:calc(var(--size-field,.25rem)*8);font-size:max(var(--font-size,.75rem),.75rem)}.input-sm[type=number]::-webkit-inner-spin-button{margin-block:-.5rem;margin-inline-end:-.75rem}.tabs-box{background-color:var(--color-base-200);--tabs-box-radius:calc(3*var(--radius-field));border-radius:calc(min(calc(var(--tab-height)/2),var(--radius-field)) + min(.25rem,var(--tabs-box-radius)));box-shadow:0 -.5px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 .5px oklch(0% 0 0/calc(var(--depth)*.05))inset;padding:.25rem}.tabs-box>.tab{border-radius:var(--radius-field);border-style:none}.tabs-box>.tab:focus-visible,.tabs-box>.tab:is(label:has(:checked:focus-visible)){outline-offset:2px}.tabs-box>:is(.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]):not(.tab-disabled,[disabled]),.tabs-box>:is(input:checked),.tabs-box>:is(label:has(:checked)){background-color:var(--tab-bg,var(--color-base-100));box-shadow:0 1px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px 1px -1px var(--color-neutral),0 1px 6px -4px var(--color-neutral)}@supports (color:color-mix(in lab, red, red)){.tabs-box>:is(.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]):not(.tab-disabled,[disabled]),.tabs-box>:is(input:checked),.tabs-box>:is(label:has(:checked)){box-shadow:0 1px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px 1px -1px color-mix(in oklab,var(--color-neutral)calc(var(--depth)*50%),#0000),0 1px 6px -4px color-mix(in oklab,var(--color-neutral)calc(var(--depth)*100%),#0000)}}@media (forced-colors:active){.tabs-box>:is(.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]):not(.tab-disabled,[disabled]),.tabs-box>:is(input:checked),.tabs-box>:is(label:has(:checked)){border:1px solid}}.tabs-box>.tab-content{height:calc(100% - var(--tab-height) + var(--border) - .5rem);border-radius:calc(min(calc(var(--tab-height)/2),var(--radius-field)) + min(.25rem,var(--tabs-box-radius)) - var(--border));margin-top:.25rem}.btn-square{width:var(--size);height:var(--size);padding-inline:0}.loading-sm{width:calc(var(--size-selector,.25rem)*5)}.badge-ghost{border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content);background-image:none}.badge-outline{color:var(--badge-color);--badge-bg:#0000;background-image:none;border-color:currentColor}.chat-bubble-info{background-color:var(--color-info);color:var(--color-info-content)}.chat-bubble-neutral{background-color:var(--color-neutral);color:var(--color-neutral-content)}.status-error{background-color:var(--color-error);color:var(--color-error)}.status-success{background-color:var(--color-success);color:var(--color-success)}.status-warning{background-color:var(--color-warning);color:var(--color-warning)}.table-zebra tbody tr:where(:nth-child(2n)),.table-zebra tbody tr:where(:nth-child(2n)) :where(.table-pin-cols tr th){background-color:var(--color-base-200)}@media (hover:hover){:is(.table-zebra tbody tr.row-hover,.table-zebra tbody tr.row-hover:where(:nth-child(2n))):hover{background-color:var(--color-base-300)}}.loading-spinner{-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E")}.select-sm{--size:calc(var(--size-field,.25rem)*8);font-size:.75rem}.select-sm option{padding-block:.25rem;padding-inline:.625rem}.table-sm :not(thead,tfoot) tr{font-size:.75rem}.table-sm :where(th,td){padding-block:.5rem;padding-inline:.75rem}.badge-sm{--size:calc(var(--size-selector,.25rem)*5);padding-inline:calc(.25rem*2.5 - var(--border));font-size:.75rem}.alert-error{color:var(--color-error-content);--alert-border-color:var(--color-error);--alert-color:var(--color-error)}.alert-info{color:var(--color-info-content);--alert-border-color:var(--color-info);--alert-color:var(--color-info)}.progress-info{color:var(--color-info)}.link-hover{text-decoration-line:none}@media (hover:hover){.link-hover:hover{text-decoration-line:underline}}.btn-sm{--fontsize:.75rem;--btn-p:.75rem;--size:calc(var(--size-field,.25rem)*8)}.badge-error{--badge-color:var(--color-error);--badge-fg:var(--color-error-content)}.badge-info{--badge-color:var(--color-info);--badge-fg:var(--color-info-content)}.badge-success{--badge-color:var(--color-success);--badge-fg:var(--color-success-content)}.badge-warning{--badge-color:var(--color-warning);--badge-fg:var(--color-warning-content)}.timeline-snap-icon>li{--timeline-col-start:.5rem;--timeline-row-start:minmax(0,1fr)}.card-border{border:var(--border)solid var(--color-base-200)}.toggle-sm[type=checkbox],.toggle-sm:has([type=checkbox]){--size:calc(var(--size-selector,.25rem)*5)}}.collapse:not(td,tr,colgroup){visibility:revert-layer}.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.top-0{top:calc(var(--spacing)*0)}.isolate{isolation:isolate}.z-20{z-index:20}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-5{margin-top:calc(var(--spacing)*5)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-auto{margin-top:auto}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-1\.5{margin-bottom:calc(var(--spacing)*1.5)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-1{margin-left:calc(var(--spacing)*1)}.alert{border-width:var(--border);border-color:var(--alert-border-color,var(--color-base-200))}.join{--join-ss:0;--join-se:0;--join-es:0;--join-ee:0;align-items:stretch;display:inline-flex}.join :where(.join-item){border-start-start-radius:var(--join-ss,0);border-start-end-radius:var(--join-se,0);border-end-end-radius:var(--join-ee,0);border-end-start-radius:var(--join-es,0)}.join :where(.join-item) *{--join-ss:var(--radius-field);--join-se:var(--radius-field);--join-es:var(--radius-field);--join-ee:var(--radius-field)}.join>.join-item:where(:first-child),.join :first-child:not(:last-child) :where(.join-item){--join-ss:var(--radius-field);--join-se:0;--join-es:var(--radius-field);--join-ee:0}.join>.join-item:where(:last-child),.join :last-child:not(:first-child) :where(.join-item){--join-ss:0;--join-se:var(--radius-field);--join-es:0;--join-ee:var(--radius-field)}.join>.join-item:where(:only-child),.join :only-child :where(.join-item){--join-ss:var(--radius-field);--join-se:var(--radius-field);--join-es:var(--radius-field);--join-ee:var(--radius-field)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.size-2{width:calc(var(--spacing)*2);height:calc(var(--spacing)*2)}.size-10{width:calc(var(--spacing)*10);height:calc(var(--spacing)*10)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-auto{height:auto}.max-h-\[38rem\]{max-height:38rem}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-screen{min-height:100vh}.w-24{width:calc(var(--spacing)*24)}.w-full{width:100%}.w-max{width:max-content}.max-w-2xl{max-width:var(--container-2xl)}.max-w-40{max-width:calc(var(--spacing)*40)}.max-w-56{max-width:calc(var(--spacing)*56)}.max-w-64{max-width:calc(var(--spacing)*64)}.max-w-72{max-width:calc(var(--spacing)*72)}.max-w-\[min\(42rem\,88vw\)\]{max-width:min(42rem,88vw)}.max-w-screen-2xl{max-width:var(--breakpoint-2xl)}.max-w-xl{max-width:var(--container-xl)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-24{min-width:calc(var(--spacing)*24)}.min-w-28{min-width:calc(var(--spacing)*28)}.min-w-64{min-width:calc(var(--spacing)*64)}.min-w-full{min-width:100%}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.grid-cols-\[5rem_minmax\(0\,1fr\)_3\.5rem\]{grid-template-columns:5rem minmax(0,1fr) 3.5rem}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing)*1)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{column-gap:calc(var(--spacing)*4)}.gap-y-1{row-gap:calc(var(--spacing)*1)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-base-300>:not(:last-child)){border-color:var(--color-base-300)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-base-300{border-color:var(--color-base-300)}.border-error\/20{border-color:var(--color-error)}@supports (color:color-mix(in lab, red, red)){.border-error\/20{border-color:color-mix(in oklab,var(--color-error)20%,transparent)}}.border-info\/30{border-color:var(--color-info)}@supports (color:color-mix(in lab, red, red)){.border-info\/30{border-color:color-mix(in oklab,var(--color-info)30%,transparent)}}.bg-base-100,.bg-base-100\/70{background-color:var(--color-base-100)}@supports (color:color-mix(in lab, red, red)){.bg-base-100\/70{background-color:color-mix(in oklab,var(--color-base-100)70%,transparent)}}.bg-base-100\/95{background-color:var(--color-base-100)}@supports (color:color-mix(in lab, red, red)){.bg-base-100\/95{background-color:color-mix(in oklab,var(--color-base-100)95%,transparent)}}.bg-base-200,.bg-base-200\/40{background-color:var(--color-base-200)}@supports (color:color-mix(in lab, red, red)){.bg-base-200\/40{background-color:color-mix(in oklab,var(--color-base-200)40%,transparent)}}.bg-base-200\/50{background-color:var(--color-base-200)}@supports (color:color-mix(in lab, red, red)){.bg-base-200\/50{background-color:color-mix(in oklab,var(--color-base-200)50%,transparent)}}.bg-base-200\/60{background-color:var(--color-base-200)}@supports (color:color-mix(in lab, red, red)){.bg-base-200\/60{background-color:color-mix(in oklab,var(--color-base-200)60%,transparent)}}.bg-base-200\/70{background-color:var(--color-base-200)}@supports (color:color-mix(in lab, red, red)){.bg-base-200\/70{background-color:color-mix(in oklab,var(--color-base-200)70%,transparent)}}.bg-error\/10{background-color:var(--color-error)}@supports (color:color-mix(in lab, red, red)){.bg-error\/10{background-color:color-mix(in oklab,var(--color-error)10%,transparent)}}.bg-info,.bg-info\/10{background-color:var(--color-info)}@supports (color:color-mix(in lab, red, red)){.bg-info\/10{background-color:color-mix(in oklab,var(--color-info)10%,transparent)}}.bg-primary{background-color:var(--color-primary)}.p-0{padding:calc(var(--spacing)*0)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-5{padding:calc(var(--spacing)*5)}.p-6{padding:calc(var(--spacing)*6)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-5{padding-block:calc(var(--spacing)*5)}.py-6{padding-block:calc(var(--spacing)*6)}.pb-3{padding-bottom:calc(var(--spacing)*3)}.pb-5{padding-bottom:calc(var(--spacing)*5)}.pl-4{padding-left:calc(var(--spacing)*4)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.68rem\]{font-size:.68rem}.leading-5{--tw-leading:calc(var(--spacing)*5);line-height:calc(var(--spacing)*5)}.leading-6{--tw-leading:calc(var(--spacing)*6);line-height:calc(var(--spacing)*6)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.18em\]{--tw-tracking:.18em;letter-spacing:.18em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-base-content,.text-base-content\/45{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/45{color:color-mix(in oklab,var(--color-base-content)45%,transparent)}}.text-base-content\/55{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/55{color:color-mix(in oklab,var(--color-base-content)55%,transparent)}}.text-base-content\/60{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/60{color:color-mix(in oklab,var(--color-base-content)60%,transparent)}}.text-base-content\/65{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/65{color:color-mix(in oklab,var(--color-base-content)65%,transparent)}}.text-base-content\/70{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/70{color:color-mix(in oklab,var(--color-base-content)70%,transparent)}}.text-base-content\/80{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/80{color:color-mix(in oklab,var(--color-base-content)80%,transparent)}}.text-error{color:var(--color-error)}.text-info{color:var(--color-info)}.text-primary{color:var(--color-primary)}.text-primary-content{color:var(--color-primary-content)}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.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,)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@layer daisyui.l1{.btn-ghost:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn)){--btn-shadow:"";--btn-bg:#0000;--btn-border:#0000;--btn-noise:none}.btn-ghost:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn)):not(:disabled,[disabled],.btn-disabled){--btn-fg:var(--btn-color,currentColor);outline-color:currentColor}@media (hover:none){.btn-ghost:not(.btn-active,:active,:focus-visible,input:checked:not(.filter .btn)):hover{--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color,currentColor);--btn-border:#0000;--btn-noise:none;outline-color:currentColor}}}.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);-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,)}.first\:pt-0:first-child{padding-top:calc(var(--spacing)*0)}.last\:pb-0:last-child{padding-bottom:calc(var(--spacing)*0)}@media (hover:hover){.hover\:bg-base-200:hover{background-color:var(--color-base-200)}}@media (min-width:40rem){.sm\:mb-8{margin-bottom:calc(var(--spacing)*8)}.sm\:block{display:block}.sm\:min-w-0{min-width:calc(var(--spacing)*0)}@layer daisyui.l1.l2{.sm\:alert-horizontal{text-align:start;grid-template-columns:auto;grid-auto-flow:column;justify-content:start;justify-items:start}.sm\:alert-horizontal:has(:nth-child(2)){grid-template-columns:auto minmax(auto,1fr)}}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-\[9rem_minmax\(14rem\,1fr\)_auto\]{grid-template-columns:9rem minmax(14rem,1fr) auto}.sm\:grid-cols-\[10rem_minmax\(14rem\,1fr\)_auto\]{grid-template-columns:10rem minmax(14rem,1fr) auto}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}.sm\:items-start{align-items:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:gap-3{gap:calc(var(--spacing)*3)}.sm\:p-5{padding:calc(var(--spacing)*5)}.sm\:p-6{padding:calc(var(--spacing)*6)}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}.sm\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.sm\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media (min-width:64rem){.lg\:flex{display:flex}.lg\:flex-row{flex-direction:row}.lg\:items-end{align-items:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:px-8{padding-inline:calc(var(--spacing)*8)}.lg\:py-8{padding-block:calc(var(--spacing)*8)}}@media (min-width:80rem){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-\[minmax\(0\,1\.55fr\)_minmax\(20rem\,0\.85fr\)\]{grid-template-columns:minmax(0,1.55fr) minmax(20rem,.85fr)}.xl\:grid-cols-\[minmax\(17rem\,0\.75fr\)_minmax\(0\,1\.7fr\)\]{grid-template-columns:minmax(17rem,.75fr) minmax(0,1.7fr)}}}:root{color-scheme:light}[data-theme=dark]{color-scheme:dark}body{text-rendering:optimizeLegibility}button,a,input,select{-webkit-tap-highlight-color:transparent}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[role=tabpanel]:focus-visible{outline:2px solid var(--color-primary);outline-offset:3px}.dashboard-shell{background-image:radial-gradient(circle at 84% 0%,var(--color-info),transparent 30rem)}@supports (color:color-mix(in lab, red, red)){.dashboard-shell{background-image:radial-gradient(circle at 84% 0%,color-mix(in oklab,var(--color-info)9%,transparent),transparent 30rem)}}[data-theme=dark] .dashboard-shell{background-image:radial-gradient(circle at 84% 0%,var(--color-info),transparent 30rem)}@supports (color:color-mix(in lab, red, red)){[data-theme=dark] .dashboard-shell{background-image:radial-gradient(circle at 84% 0%,color-mix(in oklab,var(--color-info)12%,transparent),transparent 30rem)}}.card-border{border-width:1px}.timeline [data-depth="1"]{margin-left:1.25rem}@media (max-width:639px){.timeline .timeline-start{width:4.5rem}.timeline .timeline-end{padding-left:.75rem}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}@keyframes rating{0%,40%{filter:brightness(1.05)contrast(1.05);scale:1.1}}@keyframes dropdown{0%{opacity:0}}@keyframes radio{0%{padding:5px}50%{padding:3px}}@keyframes toast{0%{opacity:0;scale:.9}to{opacity:1;scale:1}}@keyframes rotator{89.9999%,to{--first-item-position:0 0%}90%,99.9999%{--first-item-position:0 calc(var(--items)*100%)}to{translate:0 -100%}}@keyframes skeleton{0%{background-position:150%}to{background-position:-50%}}@keyframes menu{0%{opacity:0}}@keyframes progress{50%{background-position-x:-115%}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false} diff --git a/src/blacki/dashboard/static/dashboard.js b/src/blacki/dashboard/static/dashboard.js index 13b2170..8d24750 100644 --- a/src/blacki/dashboard/static/dashboard.js +++ b/src/blacki/dashboard/static/dashboard.js @@ -68,6 +68,13 @@ return new Intl.NumberFormat().format(number); }; + const formatMoney = (value) => { + const number = Number(value); + if (!Number.isFinite(number)) return "—"; + const decimals = Math.abs(number) < 0.01 && number !== 0 ? 6 : 2; + return `$${number.toFixed(decimals)}`; + }; + const formatPercent = (value) => { const number = Number(value); if (!Number.isFinite(number)) return "—"; @@ -201,6 +208,23 @@ setText("metric-errors-desc", errorCount === undefined ? "Based on available logs" : `${formatCount(errorCount)} records marked error`); setText("metric-tokens", formatCount(first(metrics, ["tokens", "tokens_processed", "total_tokens", "token_count"]))); + const cost = isObject(metrics?.cost) ? metrics.cost : metrics; + const monthlyCost = first(metrics, ["monthly_cost_usd", "monthly_estimated_cost_usd"], first(cost, ["monthly_usd", "estimated_monthly_usd"])); + const cumulativeCost = first(metrics, ["cumulative_cost_usd", "cumulative_estimated_cost_usd", "estimated_cost_usd"], first(cost, ["cumulative_usd", "estimated_cumulative_usd"])); + const averageMonthlyCost = first(metrics, ["average_user_monthly_cost_usd"], first(cost, ["average_user_monthly_usd"])); + const costCoverage = first(metrics, ["cost_coverage"], first(cost, ["coverage"])); + setText("metric-cost-month", formatMoney(monthlyCost)); + setText("metric-cost-total", formatMoney(cumulativeCost)); + setText("metric-cost-average", formatMoney(averageMonthlyCost)); + setText("metric-cost-coverage", formatPercent(costCoverage)); + const costKind = first(metrics, ["cost_kind"], first(cost, ["kind"])); + if (costKind === "estimated") { + setText("performance-note", "Some costs are LiteLLM estimates; provider-reported OpenRouter costs are preferred when available."); + } else if (costKind === "unavailable" || (monthlyCost === undefined && cumulativeCost === undefined)) { + setText("performance-note", "Exact cost is unavailable for records without provider cost metadata. New LiteLLM responses are recorded going forward."); + } else if (costCoverage !== undefined && Number(costCoverage) < 1) { + setText("performance-note", `Provider-reported cost coverage: ${formatPercent(costCoverage)}. Older or incomplete records remain unavailable.`); + } const latency = isObject(first(metrics, ["latency_ms", "latency"])) ? first(metrics, ["latency_ms", "latency"]) : {}; setText("metric-latency", formatMs(first(metrics, ["avg_latency_ms", "average_latency_ms", "mean_latency_ms"], first(latency, ["average", "avg"])))); setText("metric-p95", formatMs(first(metrics, ["p95_latency_ms", "latency_p95_ms", "p95_ms"], first(latency, ["p95", "percentile95"])))); @@ -282,11 +306,26 @@ }); }; + const costDescription = (item) => { + const monthly = first(item, ["monthly_cost_usd", "monthly_estimated_cost_usd", "month_cost_usd"]); + const cumulative = first(item, ["cumulative_cost_usd", "cost_usd", "estimated_cost_usd"]); + if (monthly === undefined && cumulative === undefined) return "Cost unavailable for these records"; + const source = first(item, ["cost_source"]); + const qualifier = source === "estimated" ? " · estimated" : ""; + return `This month ${formatMoney(monthly)} · cumulative ${formatMoney(cumulative)}${qualifier}`; + }; + + const renderSelectedUserSummary = () => { + const item = state.users.find((candidate) => String(first(candidate, ["user_id", "userId", "chat_id", "chatId", "id"], "")) === state.selectedUser); + setText("selected-user-cost", item ? costDescription(item) : "Cost unavailable until a user is selected."); + }; + const renderUsers = () => { const target = byId("users-list"); const summary = byId("users-summary"); if (!target) return; if (summary) summary.textContent = state.users.length ? `${formatCount(state.users.length)} user records` : "No users found"; + renderSelectedUserSummary(); if (!state.users.length) { showEmpty(target, "No matching users", "Try a different stored user or chat ID."); return; @@ -307,7 +346,10 @@ const lastSeen = first(item, ["last_seen", "lastSeen", "updated_at", "updatedAt", "latest_update_at"]); copy.append(el("span", "mt-1 block text-xs text-base-content/55", lastSeen === undefined ? "Stored user ID" : `Seen ${formatDate(lastSeen)}`)); const count = first(item, ["sessions", "session_count", "message_count", "messages"]); - button.append(copy, el("span", "badge badge-ghost shrink-0", count === undefined ? "View" : formatCount(count))); + const summary = el("span", "flex shrink-0 flex-col items-end gap-1"); + summary.append(el("span", "badge badge-ghost", count === undefined ? "View" : formatCount(count))); + summary.append(el("span", "text-right text-[0.68rem] text-base-content/55", formatMoney(first(item, ["monthly_cost_usd"])))); + button.append(copy, summary); button.addEventListener("click", () => selectUser(userId)); target.append(button); }); @@ -327,7 +369,7 @@ const table = el("table", "table table-sm"); const head = el("thead"); const headerRow = el("tr"); - ["Session", "Version", "Messages", "Last activity"].forEach((label) => headerRow.append(el("th", "whitespace-nowrap", label))); + ["Session", "Version", "Messages", "Tokens", "Total cost", "This month", "Last activity"].forEach((label) => headerRow.append(el("th", "whitespace-nowrap", label))); head.append(headerRow); const body = el("tbody"); state.sessions.forEach((item) => { @@ -348,6 +390,9 @@ if (reset === true || String(reset).toLowerCase() === "true" || Number(version) > 1 || generation > 0) versionCell.append(el("span", "badge badge-info badge-sm ml-1", "reset")); row.append(sessionCell, versionCell); row.append(el("td", "tabular-nums", formatCount(first(item, ["message_count", "messages", "turns"])))); + row.append(el("td", "tabular-nums", formatCount(first(item, ["tokens", "total_tokens", "token_count"])))); + row.append(el("td", "whitespace-nowrap tabular-nums", formatMoney(first(item, ["cumulative_cost_usd", "cost_usd"])))); + row.append(el("td", "whitespace-nowrap tabular-nums", formatMoney(first(item, ["monthly_cost_usd", "monthly_estimated_cost_usd", "month_cost_usd"])))); row.append(el("td", "whitespace-nowrap text-xs text-base-content/60", formatDate(first(item, ["updated_at", "updatedAt", "last_seen", "lastSeen"])))); body.append(row); }); @@ -387,11 +432,17 @@ if (!target) return; const root = rootOf(payload); const messages = listFrom(root, ["messages", "turns", "events", "conversation"]); + const summary = el("div", "mb-4 flex flex-wrap items-center gap-2 rounded-xl border border-base-300 bg-base-100/70 px-3 py-2 text-xs"); + summary.append( + el("span", "badge badge-ghost badge-sm", `Cost ${formatMoney(first(root, ["cumulative_cost_usd", "cost_usd", "estimated_cost_usd"]))}`), + el("span", "badge badge-ghost badge-sm", `This month ${formatMoney(first(root, ["monthly_cost_usd", "monthly_estimated_cost_usd"]))}`), + el("span", "badge badge-ghost badge-sm", `Tokens ${formatCount(first(root, ["tokens", "total_tokens", "token_count"]))}`), + ); + target.replaceChildren(summary); if (!messages.length) { - showEmpty(target, "No messages recorded", "This session has no chat events available on disk."); + target.append(el("div", "rounded-xl border border-dashed border-base-300 bg-base-200/40 p-6 text-center", "No messages recorded. This session has no chat events available on disk.")); return; } - target.replaceChildren(); messages.forEach((item) => { const type = normalizeStatus(first(item, ["type", "kind"], "")); const role = normalizeStatus(first(item, ["role", "author", "sender"], "assistant")); @@ -434,7 +485,7 @@ const table = el("table", "table table-zebra table-sm"); const head = el("thead"); const headerRow = el("tr"); - ["Trace", "Status", "Duration", "Spans", "Started"].forEach((label) => headerRow.append(el("th", "whitespace-nowrap", label))); + ["Trace", "Status", "Duration", "Spans", "Cost", "Started"].forEach((label) => headerRow.append(el("th", "whitespace-nowrap", label))); head.append(headerRow); const body = el("tbody"); state.traces.forEach((item) => { @@ -452,6 +503,7 @@ row.cells[1].append(badgeFor(first(item, ["status", "state", "outcome"], "unknown"))); row.append(el("td", "whitespace-nowrap tabular-nums", formatMs(first(item, ["duration_ms", "latency_ms", "duration"])))); row.append(el("td", "tabular-nums", formatCount(first(item, ["span_count", "spans", "events"])))); + row.append(el("td", "whitespace-nowrap tabular-nums", formatMoney(first(item, ["cost_usd", "estimated_cost_usd"])))); row.append(el("td", "whitespace-nowrap text-xs text-base-content/60", formatDate(first(item, ["start_time", "started_at", "timestamp", "created_at"])))); body.append(row); }); @@ -490,6 +542,7 @@ card.append(title); const details = el("div", "mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-base-content/60"); details.append(el("span", "font-mono", spanId), el("span", "tabular-nums", formatMs(first(span, ["duration_ms", "latency_ms", "duration"])))); + details.append(el("span", "tabular-nums", `Cost ${formatMoney(first(span, ["cost_usd", "estimated_cost_usd"]))}`)); const service = first(span, ["service", "component", "source"]); if (service !== undefined) details.append(el("span", "break-all", service)); card.append(details); @@ -566,6 +619,7 @@ const loadSessionDetail = async () => { if (!state.selectedUser || !state.selectedSession) return; const payload = await requestJson(`${API.session}${encodeURIComponent(state.selectedUser)}&session_id=${encodeURIComponent(state.selectedSession)}`); + setText("selected-session-cost", costDescription(rootOf(payload))); renderChat(payload); }; @@ -625,7 +679,9 @@ state.selectedUser = String(userId); state.selectedSession = ""; setText("selected-user", state.selectedUser); + renderSelectedUserSummary(); setText("selected-session", "Select a session to inspect messages."); + setText("selected-session-cost", "Cost details will appear with the session."); showEmpty(byId("chat-view"), "No session selected", "Choose a session to inspect its conversation."); try { setConnection("Loading user sessions", "pending"); @@ -641,6 +697,7 @@ const selectSession = async (sessionId) => { state.selectedSession = String(sessionId); setText("selected-session", state.selectedSession); + setText("selected-session-cost", "Loading session cost…"); showEmpty(byId("chat-view"), "Loading conversation", "Reading this session's messages…"); try { await loadSessionDetail(); diff --git a/src/blacki/dashboard/templates/index.html b/src/blacki/dashboard/templates/index.html index 3fb6105..35b4345 100644 --- a/src/blacki/dashboard/templates/index.html +++ b/src/blacki/dashboard/templates/index.html @@ -120,7 +120,7 @@

Blacki / telemetry

See what your users experience.

- A local view of activity written to disk: sessions, traces, structured logs, latency, and token use. + A local view of activity written to disk: sessions, traces, structured logs, latency, token use, and model cost.

@@ -210,6 +210,22 @@

Performance pulse

Tokens processed
+
+
Cost this month
+
+
+
+
Cumulative cost
+
+
+
+
Average user / month
+
+
+
+
Cost coverage
+
+
Average latency
@@ -220,7 +236,7 @@

Performance pulse

- Metrics are calculated from records currently available on disk. + Metrics are calculated from local records; provider-reported cost is shown when available.
@@ -270,6 +286,7 @@

Users

Selected user

Choose a user

+

Cost unavailable until a user is selected.

No sessions @@ -283,6 +300,7 @@

Choose a use

Session conversation

Select a session to inspect messages.

+

Cost details will appear with the session.

Read-only diff --git a/src/blacki/llm_costs.py b/src/blacki/llm_costs.py new file mode 100644 index 0000000..15a0f1d --- /dev/null +++ b/src/blacki/llm_costs.py @@ -0,0 +1,293 @@ +"""Capture provider-reported LiteLLM usage before ADK reduces the response.""" + +from __future__ import annotations + +import asyncio +import logging +import math +import sqlite3 +import time +import uuid +from collections.abc import AsyncIterator, Mapping +from contextvars import ContextVar +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from google.adk.models.lite_llm import LiteLLMClient +from google.adk.models.llm_response import LlmResponse +from opentelemetry import trace + +from .usage_ledger import UsageRecord, default_usage_ledger_path, write_usage_record + +logger = logging.getLogger(__name__) + +COST_METADATA_KEY = "blacki.cost" + + +CostObservation = dict[str, Any] + + +@dataclass(slots=True) +class _CostCapture: + user_id: str + session_id: str + invocation_id: str + request_key: str + observation: CostObservation | None = None + + +_ACTIVE_CAPTURE: ContextVar[_CostCapture | None] = ContextVar( + "blacki_active_cost_capture", default=None +) + + +def _read_value(value: Any, key: str) -> Any: + if isinstance(value, Mapping): + return value.get(key) + return getattr(value, key, None) + + +def _first_value(value: Any, keys: tuple[str, ...]) -> Any: + for key in keys: + candidate = _read_value(value, key) + if candidate is not None: + return candidate + return None + + +def _nonnegative_number(value: Any) -> float | None: + if value is None or isinstance(value, bool): + return None + try: + number = float(value) + except (TypeError, ValueError, OverflowError): + return None + if not math.isfinite(number) or number < 0: + return None + return number + + +def _nonnegative_integer(value: Any) -> int | None: + number = _nonnegative_number(value) + return int(number) if number is not None else None + + +def extract_cost_observation(response: Any, model: str) -> CostObservation | None: + """Extract raw provider cost and token fields from a LiteLLM response.""" + usage = _read_value(response, "usage") + + reported_cost = _nonnegative_number(_first_value(usage, ("cost", "total_cost"))) + cost_details = _read_value(usage, "cost_details") + upstream_cost = _nonnegative_number( + _first_value( + cost_details, + ("upstream_inference_cost", "upstream_cost"), + ) + ) + hidden_params = _read_value(response, "_hidden_params") + estimated_cost = _nonnegative_number( + _first_value(hidden_params, ("response_cost", "cost")) + ) + if reported_cost is not None: + estimated_cost = None + input_tokens = _nonnegative_integer( + _first_value(usage, ("prompt_tokens", "input_tokens")) + ) + output_tokens = _nonnegative_integer( + _first_value(usage, ("completion_tokens", "output_tokens")) + ) + total_tokens = _nonnegative_integer(_first_value(usage, ("total_tokens",))) + if ( + reported_cost is None + and upstream_cost is None + and estimated_cost is None + and input_tokens is None + and output_tokens is None + and total_tokens is None + ): + return None + + response_id = _first_value(response, ("id", "response_id", "generation_id")) + if response_id is not None: + response_id = str(response_id)[:256] + cost_kind = ( + "reported" + if reported_cost is not None + else "estimated" + if estimated_cost is not None + else "upstream_only" + if upstream_cost is not None + else "unknown" + ) + cost_source = ( + "provider_usage" + if reported_cost is not None or upstream_cost is not None + else "litellm_response_cost" + ) + return { + "model": str(model)[:256], + "provider_response_id": response_id, + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "total_tokens": total_tokens, + "cost_usd": reported_cost, + "upstream_cost_usd": upstream_cost, + "estimated_cost_usd": estimated_cost, + "cost_kind": cost_kind, + "cost_source": cost_source, + "currency": "USD", + } + + +def begin_cost_capture( + *, + user_id: str | None, + session_id: str | None, + invocation_id: str | None, +) -> None: + """Start a request-local capture associated with ADK identity fields.""" + _ACTIVE_CAPTURE.set( + _CostCapture( + user_id=str(user_id or "")[:512], + session_id=str(session_id or "")[:512], + invocation_id=str(invocation_id or "")[:512], + request_key=uuid.uuid4().hex, + ) + ) + + +def _dedupe_key(capture: _CostCapture, observation: CostObservation) -> str: + response_id = observation.get("provider_response_id") + model = str(observation.get("model") or "") + if response_id: + return f"provider:{model}:{response_id}"[:1024] + return f"request:{capture.request_key}" + + +def _set_span_cost_attributes(observation: CostObservation) -> None: + span = trace.get_current_span() + if not span.is_recording(): + return + cost = observation.get("cost_usd") + if cost is not None: + span.set_attribute("gen_ai.usage.cost", cost) + estimated = observation.get("estimated_cost_usd") + if estimated is not None: + span.set_attribute("gen_ai.usage.cost_estimate", estimated) + upstream = observation.get("upstream_cost_usd") + if upstream is not None: + span.set_attribute("gen_ai.cost.upstream_inference_cost", upstream) + span.set_attribute("gen_ai.cost.source", str(observation["cost_source"])) + span.set_attribute("gen_ai.cost.kind", str(observation["cost_kind"])) + + +def _remember_observation(observation: CostObservation) -> _CostCapture | None: + capture = _ACTIVE_CAPTURE.get() + if capture is None: + return None + current = capture.observation + if ( + current is not None + and current.get("cost_usd") is not None + and observation.get("cost_usd") is None + ): + return capture + remembered = dict(observation) + remembered["dedupe_key"] = _dedupe_key(capture, remembered) + capture.observation = remembered + return capture + + +async def _observe_response( + response: Any, + model: str, + ledger_path: Path, +) -> None: + observation = extract_cost_observation(response, model) + if observation is None: + return + _set_span_cost_attributes(observation) + capture = _remember_observation(observation) + if capture is None: + return + remembered = capture.observation + if remembered is None: # pragma: no cover - defensive capture invariant + return + record = UsageRecord( + dedupe_key=str(remembered["dedupe_key"]), + observed_at=time.time(), + user_id=capture.user_id, + session_id=capture.session_id, + invocation_id=capture.invocation_id, + model=str(remembered["model"]), + provider_response_id=remembered.get("provider_response_id"), + input_tokens=remembered.get("input_tokens"), + output_tokens=remembered.get("output_tokens"), + total_tokens=remembered.get("total_tokens"), + cost_usd=remembered.get("cost_usd"), + upstream_cost_usd=remembered.get("upstream_cost_usd"), + estimated_cost_usd=remembered.get("estimated_cost_usd"), + cost_kind=str(remembered["cost_kind"]), + cost_source=str(remembered["cost_source"]), + currency=str(remembered["currency"]), + ) + try: + await asyncio.to_thread(write_usage_record, ledger_path, record) + except (OSError, sqlite3.Error, ValueError): + logger.warning("Unable to persist model usage to the local cost ledger") + + +def attach_cost_metadata(response: LlmResponse) -> CostObservation | None: + """Attach the latest safe cost observation to the ADK response event.""" + capture = _ACTIVE_CAPTURE.get() + if capture is None: + return None + observation = capture.observation + if observation is not None: + metadata = getattr(response, "custom_metadata", None) + if not isinstance(metadata, dict): + metadata = {} + metadata[COST_METADATA_KEY] = dict(observation) + response.custom_metadata = metadata + if not bool(getattr(response, "partial", False)): + _ACTIVE_CAPTURE.set(None) + return dict(observation) if observation is not None else None + + +class CostAwareLiteLLMClient(LiteLLMClient): + """Observe LiteLLM responses while preserving ADK's client contract.""" + + def __init__(self, ledger_path: Path | str | None = None) -> None: + super().__init__() + self.ledger_path = ( + Path(ledger_path) if ledger_path else default_usage_ledger_path() + ) + + async def _observed_stream( + self, + response: Any, + model: str, + ) -> AsyncIterator[Any]: + async for chunk in response: + await _observe_response(chunk, model, self.ledger_path) + yield chunk + + async def acompletion( + self, + model: Any, + messages: Any, + tools: Any, + **kwargs: Any, + ) -> Any: + response = await super().acompletion( + model=model, + messages=messages, + tools=tools, + **kwargs, + ) + model_name = str(model) + if kwargs.get("stream"): + return self._observed_stream(response, model_name) + await _observe_response(response, model_name, self.ledger_path) + return response diff --git a/src/blacki/usage_ledger.py b/src/blacki/usage_ledger.py new file mode 100644 index 0000000..8473d68 --- /dev/null +++ b/src/blacki/usage_ledger.py @@ -0,0 +1,414 @@ +"""Content-free, idempotent local ledger for model usage and cost.""" + +from __future__ import annotations + +import math +import os +import sqlite3 +from dataclasses import dataclass +from decimal import ROUND_HALF_UP, Decimal, InvalidOperation +from pathlib import Path +from typing import Any +from urllib.parse import quote + +LEDGER_TABLE = "llm_usage_ledger" +COST_SCALE = 1_000_000_000 +COST_LEDGER_WARNING = "Cost ledger is temporarily unavailable." + + +@dataclass(frozen=True, slots=True) +class UsageRecord: + """One billable model response without prompts, outputs, or tool data.""" + + dedupe_key: str + observed_at: float + user_id: str + session_id: str + invocation_id: str + model: str + provider_response_id: str | None + input_tokens: int | None + output_tokens: int | None + total_tokens: int | None + cost_usd: float | None + upstream_cost_usd: float | None + estimated_cost_usd: float | None + cost_kind: str + cost_source: str + currency: str = "USD" + + +@dataclass(frozen=True, slots=True) +class LedgerSummary: + """Fixed-point aggregates returned by the usage ledger.""" + + records: int = 0 + reported_records: int = 0 + estimated_records: int = 0 + input_tokens: int = 0 + output_tokens: int = 0 + total_tokens: int = 0 + cost_nano_usd: int | None = None + upstream_cost_nano_usd: int | None = None + estimated_cost_nano_usd: int | None = None + + +@dataclass(frozen=True, slots=True) +class UsageLedgerSnapshot: + """All dashboard aggregates needed for one refresh.""" + + selected: LedgerSummary + cumulative: LedgerSummary + monthly: LedgerSummary + users: dict[str, LedgerSummary] + monthly_users: dict[str, LedgerSummary] + sessions: dict[tuple[str, str], LedgerSummary] + monthly_sessions: dict[tuple[str, str], LedgerSummary] + available: bool + warnings: tuple[str, ...] + + +def default_usage_ledger_path(agent_dir: Path | str | None = None) -> Path: + """Return the configurable content-free usage ledger path.""" + configured = os.environ.get("BLACKI_COST_LEDGER_PATH", "").strip() + if configured: + return Path(configured) + configured_agent_dir = os.environ.get("AGENT_DIR", "").strip() + base = Path(agent_dir) if agent_dir is not None else Path(configured_agent_dir) + if agent_dir is None and not configured_agent_dir: + base = Path(__file__).resolve().parent.parent + return base / ".adk" / "costs.db" + + +def _fixed_cost(value: float | None) -> int | None: + if value is None or isinstance(value, bool) or not math.isfinite(value): + return None + if value < 0: + return None + try: + decimal_value = Decimal(str(value)) * COST_SCALE + return int(decimal_value.quantize(Decimal("1"), rounding=ROUND_HALF_UP)) + except (InvalidOperation, ValueError, OverflowError): + return None + + +def fixed_cost_to_usd(value: int | None) -> float | None: + """Convert fixed-point nanodollars to a JSON-safe USD float.""" + if value is None: + return None + return round(value / COST_SCALE, 9) + + +def _schema_sql() -> str: + return f""" + CREATE TABLE IF NOT EXISTS {LEDGER_TABLE} ( + dedupe_key TEXT PRIMARY KEY, + observed_at REAL NOT NULL, + user_id TEXT NOT NULL, + session_id TEXT NOT NULL, + invocation_id TEXT NOT NULL, + model TEXT NOT NULL, + provider_response_id TEXT, + input_tokens INTEGER, + output_tokens INTEGER, + total_tokens INTEGER, + cost_nano_usd INTEGER, + upstream_cost_nano_usd INTEGER, + estimated_cost_nano_usd INTEGER, + cost_kind TEXT NOT NULL, + cost_source TEXT NOT NULL, + currency TEXT NOT NULL + ) + """ + + +def write_usage_record(path: Path, record: UsageRecord) -> None: + """Upsert one usage record, preserving provider-response idempotency.""" + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + values = ( + record.dedupe_key, + record.observed_at, + record.user_id, + record.session_id, + record.invocation_id, + record.model, + record.provider_response_id, + record.input_tokens, + record.output_tokens, + record.total_tokens, + _fixed_cost(record.cost_usd), + _fixed_cost(record.upstream_cost_usd), + _fixed_cost(record.estimated_cost_usd), + record.cost_kind, + record.cost_source, + record.currency, + ) + connection = sqlite3.connect(path, timeout=5.0) + try: + with connection: + connection.execute("PRAGMA busy_timeout = 5000") + connection.execute("PRAGMA journal_mode = WAL") + connection.execute(_schema_sql()) + connection.execute( # noqa: S608 + f""" + INSERT INTO {LEDGER_TABLE} ( + dedupe_key, observed_at, user_id, session_id, invocation_id, + model, provider_response_id, input_tokens, output_tokens, + total_tokens, cost_nano_usd, upstream_cost_nano_usd, + estimated_cost_nano_usd, cost_kind, cost_source, currency + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(dedupe_key) DO UPDATE SET + provider_response_id = COALESCE( + excluded.provider_response_id, + {LEDGER_TABLE}.provider_response_id + ), + input_tokens = COALESCE( + excluded.input_tokens, {LEDGER_TABLE}.input_tokens + ), + output_tokens = COALESCE( + excluded.output_tokens, {LEDGER_TABLE}.output_tokens + ), + total_tokens = COALESCE( + excluded.total_tokens, {LEDGER_TABLE}.total_tokens + ), + cost_nano_usd = COALESCE( + excluded.cost_nano_usd, {LEDGER_TABLE}.cost_nano_usd + ), + upstream_cost_nano_usd = COALESCE( + excluded.upstream_cost_nano_usd, + {LEDGER_TABLE}.upstream_cost_nano_usd + ), + estimated_cost_nano_usd = COALESCE( + excluded.estimated_cost_nano_usd, + {LEDGER_TABLE}.estimated_cost_nano_usd + ), + cost_kind = CASE + WHEN {LEDGER_TABLE}.cost_kind = 'reported' + THEN {LEDGER_TABLE}.cost_kind + ELSE excluded.cost_kind + END, + cost_source = CASE + WHEN {LEDGER_TABLE}.cost_kind = 'reported' + THEN {LEDGER_TABLE}.cost_source + ELSE excluded.cost_source + END + """, # noqa: S608 + values, + ) + finally: + connection.close() + + +_SUMMARY_COLUMNS = """ + COUNT(*) AS records, + SUM(CASE WHEN cost_nano_usd IS NOT NULL THEN 1 ELSE 0 END) + AS reported_records, + SUM(CASE WHEN estimated_cost_nano_usd IS NOT NULL + AND cost_nano_usd IS NULL THEN 1 ELSE 0 END) AS estimated_records, + COALESCE(SUM(input_tokens), 0) AS input_tokens, + COALESCE(SUM(output_tokens), 0) AS output_tokens, + COALESCE(SUM(total_tokens), 0) AS total_tokens, + SUM(cost_nano_usd) AS cost_nano_usd, + SUM(upstream_cost_nano_usd) AS upstream_cost_nano_usd, + SUM(estimated_cost_nano_usd) AS estimated_cost_nano_usd +""" + + +def _summary_from_row(row: sqlite3.Row | None) -> LedgerSummary: + if row is None: + return LedgerSummary() + return LedgerSummary( + records=int(row["records"] or 0), + reported_records=int(row["reported_records"] or 0), + estimated_records=int(row["estimated_records"] or 0), + input_tokens=int(row["input_tokens"] or 0), + output_tokens=int(row["output_tokens"] or 0), + total_tokens=int(row["total_tokens"] or 0), + cost_nano_usd=( + int(row["cost_nano_usd"]) if row["cost_nano_usd"] is not None else None + ), + upstream_cost_nano_usd=( + int(row["upstream_cost_nano_usd"]) + if row["upstream_cost_nano_usd"] is not None + else None + ), + estimated_cost_nano_usd=( + int(row["estimated_cost_nano_usd"]) + if row["estimated_cost_nano_usd"] is not None + else None + ), + ) + + +def _where_clause( + *, + since: float | None, + until: float | None, + user_id: str | None = None, + session_id: str | None = None, +) -> tuple[str, tuple[Any, ...]]: + clauses: list[str] = [] + values: list[Any] = [] + if since is not None: + clauses.append("observed_at >= ?") + values.append(since) + if until is not None: + clauses.append("observed_at <= ?") + values.append(until) + if user_id is not None: + clauses.append("user_id = ?") + values.append(user_id) + if session_id is not None: + clauses.append("session_id = ?") + values.append(session_id) + return " AND ".join(clauses) or "1 = 1", tuple(values) + + +def _query_summary( + connection: sqlite3.Connection, + *, + since: float | None, + until: float | None, + user_id: str | None = None, + session_id: str | None = None, +) -> LedgerSummary: + where, values = _where_clause( + since=since, + until=until, + user_id=user_id, + session_id=session_id, + ) + row = connection.execute( # noqa: S608 + f"SELECT {_SUMMARY_COLUMNS} FROM {LEDGER_TABLE} WHERE {where}", # noqa: S608 + values, + ).fetchone() + return _summary_from_row(row) + + +def _query_grouped( + connection: sqlite3.Connection, + *, + group_columns: tuple[str, ...], + since: float | None, + until: float | None, +) -> dict[Any, LedgerSummary]: + where, values = _where_clause(since=since, until=until) + group_sql = ", ".join(group_columns) + rows = connection.execute( # noqa: S608 + f""" + SELECT {group_sql}, {_SUMMARY_COLUMNS} + FROM {LEDGER_TABLE} + WHERE {where} + GROUP BY {group_sql} + """, # noqa: S608 + values, + ).fetchall() + result: dict[Any, LedgerSummary] = {} + for row in rows: + key = ( + row[group_columns[0]] + if len(group_columns) == 1 + else tuple(row[column] for column in group_columns) + ) + result[key] = _summary_from_row(row) + return result + + +def _empty_snapshot( + *, available: bool, warnings: tuple[str, ...] = () +) -> UsageLedgerSnapshot: + empty = LedgerSummary() + return UsageLedgerSnapshot( + selected=empty, + cumulative=empty, + monthly=empty, + users={}, + monthly_users={}, + sessions={}, + monthly_sessions={}, + available=available, + warnings=warnings, + ) + + +def read_usage_ledger( + path: Path, + *, + selected_since: float | None, + selected_until: float | None, + month_start: float, + now: float, +) -> UsageLedgerSnapshot: + """Read aggregate usage without exposing ledger rows to the dashboard.""" + path = Path(path) + try: + if not path.is_file(): + return _empty_snapshot(available=False) + uri = f"file:{quote(path.resolve().as_posix(), safe='/')}?mode=ro" + connection = sqlite3.connect(uri, uri=True, timeout=1.0) + try: + connection.row_factory = sqlite3.Row + tables = { + str(row[0]) + for row in connection.execute( + "SELECT name FROM sqlite_master WHERE type = 'table'" + ).fetchall() + } + if LEDGER_TABLE not in tables: + return _empty_snapshot(available=False) + selected = _query_summary( + connection, + since=selected_since, + until=selected_until, + ) + cumulative = _query_summary(connection, since=None, until=now) + monthly = _query_summary( + connection, + since=month_start, + until=now, + ) + users = _query_grouped( + connection, + group_columns=("user_id",), + since=None, + until=now, + ) + monthly_users = _query_grouped( + connection, + group_columns=("user_id",), + since=month_start, + until=now, + ) + sessions = _query_grouped( + connection, + group_columns=("user_id", "session_id"), + since=None, + until=now, + ) + monthly_sessions = _query_grouped( + connection, + group_columns=("user_id", "session_id"), + since=month_start, + until=now, + ) + return UsageLedgerSnapshot( + selected=selected, + cumulative=cumulative, + monthly=monthly, + users={str(key): value for key, value in users.items()}, + monthly_users={str(key): value for key, value in monthly_users.items()}, + sessions={ + (str(key[0]), str(key[1])): value for key, value in sessions.items() + }, + monthly_sessions={ + (str(key[0]), str(key[1])): value + for key, value in monthly_sessions.items() + }, + available=True, + warnings=(), + ) + finally: + connection.close() + except (OSError, sqlite3.Error, ValueError): + return _empty_snapshot(available=False, warnings=(COST_LEDGER_WARNING,)) diff --git a/tests/dashboard/test_assets.py b/tests/dashboard/test_assets.py index a47e9ca..11c846e 100644 --- a/tests/dashboard/test_assets.py +++ b/tests/dashboard/test_assets.py @@ -53,6 +53,30 @@ def test_chat_metadata_events_render_as_safe_rows() -> None: assert 'el("div", `chat ' in script +def test_dashboard_renders_cumulative_and_monthly_cost_signals() -> None: + template = TEMPLATE.read_text(encoding="utf-8") + script = SCRIPT.read_text(encoding="utf-8") + + for marker in ( + "metric-cost-month", + "metric-cost-total", + "metric-cost-average", + "selected-user-cost", + "selected-session-cost", + ): + assert marker in template + for marker in ( + "formatMoney", + "monthly_cost_usd", + "monthly_estimated_cost_usd", + "cumulative_cost_usd", + "cumulative_estimated_cost_usd", + "average_user_monthly_cost_usd", + "cost_coverage", + ): + assert marker in script + + def test_template_exposes_accessible_view_tabs_and_landmarks() -> None: template = TEMPLATE.read_text(encoding="utf-8") diff --git a/tests/dashboard/test_data.py b/tests/dashboard/test_data.py index 8d58e16..88ed0e8 100644 --- a/tests/dashboard/test_data.py +++ b/tests/dashboard/test_data.py @@ -4,7 +4,7 @@ import json import sqlite3 -from datetime import datetime, tzinfo +from datetime import UTC, datetime, tzinfo from pathlib import Path from typing import Any, cast @@ -15,6 +15,9 @@ _activity_series, _allowlisted_attributes, _attachment_item, + _cost_fields, + _event_cost, + _event_cost_summary, _event_is_error, _event_messages, _event_stats, @@ -27,6 +30,7 @@ _load_snapshot_sync, _load_telegram_identities_sync, _log_item, + _money_value, _normalize_epoch_seconds, _number, _parse_json_object, @@ -36,7 +40,9 @@ _scan_jsonl_sync, _scan_warning, _select_table_rows, + _session_item, _session_version, + _SessionRow, _tool_status, _trace_groups, _trace_item, @@ -46,6 +52,7 @@ _window_bounds, ) from blacki.dashboard.models import clamp_limit, clamp_offset +from blacki.usage_ledger import UsageRecord, write_usage_record def _make_db(path: Path) -> None: @@ -279,9 +286,182 @@ async def test_sessions_users_replay_versions_and_overview(tmp_path: Path) -> No assert overview["stats"]["sessions"] == 3 assert overview["stats"]["messages"] == 3 assert overview["stats"]["tokens"] == 11 + assert overview["stats"]["cumulative_cost_usd"] is None assert overview["activity"] +@pytest.mark.asyncio +async def test_cost_ledger_projects_cumulative_monthly_and_average_user_cost( + tmp_path: Path, +) -> None: + db_path = tmp_path / "sessions.db" + ledger_path = tmp_path / "costs.db" + _make_db(db_path) + _insert_session(db_path, "user-1", "session-1") + _insert_session(db_path, "user-2", "session-2") + now = datetime.now(tz=UTC).timestamp() + current_month_event = _text_event( + "model-current", "model", "current", now - 20, invocation_id="inv-1" + ) + current_month_event["usage_metadata"] = { + "promptTokenCount": 10, + "candidatesTokenCount": 5, + "totalTokenCount": 15, + } + _insert_event( + db_path, "model-current", "user-1", "session-1", now - 20, current_month_event + ) + write_usage_record( + ledger_path, + UsageRecord( + dedupe_key="current-user-1", + observed_at=now - 20, + user_id="user-1", + session_id="session-1", + invocation_id="inv-1", + model="openrouter/test", + provider_response_id="generation-1", + input_tokens=10, + output_tokens=5, + total_tokens=15, + cost_usd=0.25, + upstream_cost_usd=0.2, + estimated_cost_usd=None, + cost_kind="reported", + cost_source="provider_usage", + ), + ) + write_usage_record( + ledger_path, + UsageRecord( + dedupe_key="previous-user-1", + observed_at=now - 40 * 86400, + user_id="user-1", + session_id="session-1", + invocation_id="inv-old", + model="openrouter/test", + provider_response_id="generation-old", + input_tokens=20, + output_tokens=10, + total_tokens=30, + cost_usd=0.75, + upstream_cost_usd=0.7, + estimated_cost_usd=None, + cost_kind="reported", + cost_source="provider_usage", + ), + ) + write_usage_record( + ledger_path, + UsageRecord( + dedupe_key="current-user-2", + observed_at=now - 10, + user_id="user-2", + session_id="session-2", + invocation_id="inv-2", + model="openrouter/test", + provider_response_id="generation-2", + input_tokens=8, + output_tokens=4, + total_tokens=12, + cost_usd=0.5, + upstream_cost_usd=0.45, + estimated_cost_usd=None, + cost_kind="reported", + cost_source="provider_usage", + ), + ) + + store = DashboardStore(db_path, tmp_path, cost_ledger_path=ledger_path) + overview = await store.get_overview("all") + assert overview["stats"]["cumulative_cost_usd"] == 1.5 + assert overview["stats"]["monthly_cost_usd"] == 0.75 + assert overview["stats"]["average_user_monthly_cost_usd"] == 0.375 + assert overview["stats"]["monthly_cost_users"] == 2 + assert overview["stats"]["cost_coverage"] == 1.0 + + users = await store.list_users("", 50, 0) + user_one = next(item for item in users["items"] if item["user_id"] == "user-1") + assert user_one["cumulative_cost_usd"] == 1.0 + assert user_one["monthly_cost_usd"] == 0.25 + + sessions = await store.list_sessions("user-1", 50, 0) + assert sessions["items"][0]["cost_usd"] == 1.0 + assert sessions["items"][0]["monthly_cost_usd"] == 0.25 + detail = await store.get_session("user-1", "session-1") + assert detail is not None + assert detail["cumulative_cost_usd"] == 1.0 + assert detail["monthly_cost_usd"] == 0.25 + + +@pytest.mark.asyncio +async def test_estimated_costs_are_visible_when_exact_cost_is_unavailable( + tmp_path: Path, +) -> None: + db_path = tmp_path / "sessions.db" + ledger_path = tmp_path / "costs.db" + _make_db(db_path) + _insert_session(db_path, "user-estimated", "session-estimated") + now = datetime.now(tz=UTC).timestamp() + write_usage_record( + ledger_path, + UsageRecord( + dedupe_key="estimated-only", + observed_at=now - 10, + user_id="user-estimated", + session_id="session-estimated", + invocation_id="inv-estimated", + model="openrouter/test", + provider_response_id="generation-estimated", + input_tokens=10, + output_tokens=5, + total_tokens=15, + cost_usd=None, + upstream_cost_usd=None, + estimated_cost_usd=0.125, + cost_kind="estimated", + cost_source="litellm_response_cost", + ), + ) + + store = DashboardStore(db_path, tmp_path, cost_ledger_path=ledger_path) + overview = await store.get_overview("all") + assert overview["stats"]["cost_kind"] == "estimated" + assert overview["stats"]["monthly_estimated_cost_usd"] == 0.125 + assert overview["stats"]["cumulative_estimated_cost_usd"] == 0.125 + assert overview["stats"]["average_user_monthly_cost_usd"] == 0.125 + assert overview["stats"]["monthly_cost_users"] == 1 + + users = await store.list_users("", 50, 0) + assert users["items"][0]["monthly_estimated_cost_usd"] == 0.125 + sessions = await store.list_sessions("user-estimated", 50, 0) + assert sessions["items"][0]["monthly_estimated_cost_usd"] == 0.125 + + +@pytest.mark.asyncio +async def test_event_metadata_cost_is_used_when_ledger_is_not_present( + tmp_path: Path, +) -> None: + db_path = tmp_path / "sessions.db" + _make_db(db_path) + _insert_session(db_path, "user-1", "session-1") + timestamp = datetime.now(tz=UTC).timestamp() - 10 + event = _text_event("cost-event", "model", "answer", timestamp) + event["usage_metadata"] = {"totalTokenCount": 3} + event["custom_metadata"] = { + "blacki.cost": { + "cost_usd": 0.003, + "upstream_cost_usd": 0.002, + "cost_kind": "reported", + } + } + _insert_event(db_path, "cost-event", "user-1", "session-1", timestamp, event) + + overview = await DashboardStore(db_path, tmp_path).get_overview("all") + assert overview["stats"]["cumulative_cost_usd"] == 0.003 + assert overview["stats"]["cost"]["source"] == "event_metadata" + + @pytest.mark.asyncio async def test_dashboard_labels_private_telegram_users_from_local_identity_db( tmp_path: Path, @@ -761,6 +941,20 @@ def test_defensive_adapters_cover_scalars_windows_and_trace_shapes() -> None: assert str(attrs["service.name"]).startswith(" None: ) assert string_kind_trace is not None assert string_kind_trace["kind"] == "internal" + + +def test_cost_metadata_adapters_cover_invalid_sources_and_session_fallback() -> None: + timestamp = datetime.now(tz=UTC).timestamp() - 1 + + def event(data: dict[str, Any]) -> _EventRow: + return _EventRow("event", "user-1", "session-1", "inv-1", timestamp, data) + + assert _event_cost(event({"custom_metadata": "not-json"})) is None + assert _money_value("not-a-number") is None + assert _money_value(float("nan")) is None + assert _money_value(-1) is None + assert _event_cost(event({"custom_metadata": []})) is None + assert ( + _event_cost( + event({"custom_metadata": json.dumps({"blacki.cost": {"cost_usd": 0.1}})}) + ) + is not None + ) + assert _event_cost(event({"custom_metadata": {"blacki.cost": "not-json"}})) is None + assert _event_cost(event({"custom_metadata": {"blacki.cost": []}})) is None + assert ( + _event_cost( + event({"custom_metadata": {"blacki.cost": {"cost_kind": "invalid"}}}) + ) + is None + ) + assert ( + _event_cost( + event({"custom_metadata": {"blacki.cost": {"cost_kind": "unknown"}}}) + ) + is not None + ) + + direct = _event_cost(event({"custom_metadata": {"cost_usd": 0.2}})) + assert direct is not None + assert direct["cost_usd"] == 0.2 + string_value = _event_cost( + event( + { + "custom_metadata": { + "blacki.cost": json.dumps({"estimated_cost_usd": 0.3}) + } + } + ) + ) + assert string_value is not None + assert string_value["estimated_cost_usd"] == 0.3 + + estimated_event = event( + { + "usage_metadata": {"totalTokenCount": 2}, + "custom_metadata": { + "blacki.cost": {"estimated_cost_usd": 0.3, "cost_kind": "estimated"} + }, + } + ) + summary = _event_cost_summary([estimated_event]) + assert summary["estimated_records"] == 1 + assert summary["usage_records"] == 1 + + assert ( + _cost_fields( + {"cost_usd": None, "estimated_cost_usd": 0.3, "usage_records": 1}, + {"cost_usd": None}, + )["cost_source"] + == "estimated" + ) + assert ( + _cost_fields( + {"cost_usd": None, "estimated_cost_usd": None, "upstream_cost_usd": 0.2}, + {"cost_usd": None}, + )["cost_source"] + == "upstream_only" + ) + assert ( + _cost_fields( + {"cost_usd": None, "estimated_cost_usd": None, "usage_records": 1}, + {"cost_usd": None}, + )["cost_source"] + == "unavailable" + ) + assert ( + _cost_fields( + {"cost_usd": None, "estimated_cost_usd": None, "usage_records": 0}, + {"cost_usd": None}, + )["cost_source"] + == "none" + ) + + session = _session_item( + _SessionRow("user-1", "session-1", timestamp, timestamp), + [estimated_event], + is_current=True, + retained_session_ids=[], + ) + assert session["estimated_cost_usd"] == 0.3 assert _trace_matches({"trace_id": "trace"}, "") is True assert _trace_groups([{"trace_id": ""}]) == [] empty_summary = _trace_summary([{"trace_id": "manual", "attributes": None}]) diff --git a/tests/dashboard/test_routes.py b/tests/dashboard/test_routes.py index 403668b..c8bf574 100644 --- a/tests/dashboard/test_routes.py +++ b/tests/dashboard/test_routes.py @@ -240,6 +240,7 @@ def test_default_store_uses_session_db_log_dir_and_app_name(tmp_path: Path) -> N log_dir, "blacki", tmp_path / ".adk" / "tools.db", + tmp_path / ".adk" / "costs.db", ) diff --git a/tests/test_integration.py b/tests/test_integration.py index 578c303..acd0b4b 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -17,7 +17,9 @@ from blacki.agent import ( AUTO_COMPACTION_EVENT_RETENTION_SIZE, AUTO_COMPACTION_TOKEN_THRESHOLD, + _build_model, ) +from blacki.llm_costs import CostAwareLiteLLMClient class AgentConfigLike(Protocol): @@ -154,3 +156,12 @@ def test_memory_tools_are_registered(self) -> None: assert "save_memory" in tool_names assert "search_memory" in tool_names + + def test_litellm_models_use_cost_aware_client(self, monkeypatch) -> None: + monkeypatch.setenv("OPENROUTER_API_KEY", "test-key") + monkeypatch.setenv("ROOT_AGENT_MODEL", "google/gemini-2.5-flash") + + model = _build_model() + + assert not isinstance(model, str) + assert isinstance(model.llm_client, CostAwareLiteLLMClient) diff --git a/tests/test_llm_costs.py b/tests/test_llm_costs.py new file mode 100644 index 0000000..0856ae9 --- /dev/null +++ b/tests/test_llm_costs.py @@ -0,0 +1,268 @@ +"""Tests for provider cost capture at the LiteLLM boundary.""" + +from __future__ import annotations + +from collections.abc import AsyncIterator +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from google.adk.models.lite_llm import LiteLLMClient +from google.adk.models.llm_response import LlmResponse +from google.genai import types + +from blacki.llm_costs import ( + CostAwareLiteLLMClient, + _observe_response, + _remember_observation, + _set_span_cost_attributes, + attach_cost_metadata, + begin_cost_capture, + extract_cost_observation, +) +from blacki.usage_ledger import read_usage_ledger + + +def test_extract_cost_observation_preserves_provider_and_upstream_costs() -> None: + observation = extract_cost_observation( + { + "id": "generation-1", + "usage": { + "prompt_tokens": 12, + "completion_tokens": 8, + "total_tokens": 20, + "cost": 0.0025, + "cost_details": {"upstream_inference_cost": 0.0022}, + }, + }, + "openrouter/google/gemini-2.5-flash", + ) + + assert observation is not None + assert observation["provider_response_id"] == "generation-1" + assert observation["input_tokens"] == 12 + assert observation["output_tokens"] == 8 + assert observation["cost_usd"] == 0.0025 + assert observation["upstream_cost_usd"] == 0.0022 + assert observation["estimated_cost_usd"] is None + assert observation["cost_kind"] == "reported" + + +def test_extract_cost_observation_distinguishes_estimated_unknown_and_invalid() -> None: + estimated = extract_cost_observation( + { + "usage": {"prompt_tokens": 3, "completion_tokens": 2}, + "_hidden_params": {"response_cost": 0.0004}, + }, + "openai/test", + ) + assert estimated is not None + assert estimated["estimated_cost_usd"] == 0.0004 + assert estimated["cost_kind"] == "estimated" + + unknown = extract_cost_observation( + {"id": "unknown", "usage": {"total_tokens": 5}}, + "openai/test", + ) + assert unknown is not None + assert unknown["cost_usd"] is None + assert unknown["cost_kind"] == "unknown" + + invalid = extract_cost_observation( + {"usage": {"cost": "not-a-number", "total_tokens": 5}}, + "openai/test", + ) + assert invalid is not None + assert invalid["cost_usd"] is None + + assert extract_cost_observation({"usage": {"cost": -1}}, "openai/test") is None + assert extract_cost_observation({}, "openai/test") is None + + +def test_span_cost_attributes_are_recorded_when_span_is_active() -> None: + span = MagicMock() + span.is_recording.return_value = True + with patch("blacki.llm_costs.trace.get_current_span", return_value=span): + _set_span_cost_attributes( + { + "cost_usd": 0.01, + "estimated_cost_usd": 0.02, + "upstream_cost_usd": 0.009, + "cost_source": "provider_usage", + "cost_kind": "reported", + } + ) + + assert span.set_attribute.call_count == 5 + span.set_attribute.assert_any_call("gen_ai.usage.cost", 0.01) + span.set_attribute.assert_any_call("gen_ai.usage.cost_estimate", 0.02) + span.set_attribute.assert_any_call("gen_ai.cost.upstream_inference_cost", 0.009) + with patch("blacki.llm_costs.trace.get_current_span", return_value=span): + _set_span_cost_attributes({"cost_source": "unknown", "cost_kind": "unknown"}) + + +def test_remember_observation_preserves_reported_cost_and_handles_no_capture() -> None: + assert _remember_observation({"cost_usd": 0.01}) is None + + begin_cost_capture(user_id="user-1", session_id="session-1", invocation_id="inv-1") + reported = _remember_observation( + { + "model": "openrouter/test", + "provider_response_id": "generation-1", + "cost_usd": 0.01, + "cost_source": "provider_usage", + "cost_kind": "reported", + "currency": "USD", + } + ) + assert reported is not None + preserved = _remember_observation({"cost_usd": None}) + assert preserved is reported + assert attach_cost_metadata(LlmResponse()) is not None + + +@pytest.mark.asyncio +async def test_observe_response_handles_missing_capture_and_write_errors( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + await _observe_response({}, "openrouter/test", tmp_path / "costs.db") + await _observe_response( + {"usage": {"total_tokens": 1}}, "openrouter/test", tmp_path / "costs.db" + ) + + begin_cost_capture(user_id="user-1", session_id="session-1", invocation_id="inv-1") + caplog.set_level("WARNING") + with patch("blacki.llm_costs.write_usage_record", side_effect=OSError("read-only")): + await _observe_response( + {"usage": {"total_tokens": 1, "cost": 0.001}}, + "openrouter/test", + tmp_path / "costs.db", + ) + assert "Unable to persist model usage" in caplog.text + assert attach_cost_metadata(LlmResponse()) is not None + + +def test_partial_response_keeps_capture_until_final_response() -> None: + begin_cost_capture(user_id="user-1", session_id="session-1", invocation_id="inv-1") + _remember_observation( + { + "model": "openrouter/test", + "provider_response_id": "generation-1", + "cost_usd": 0.01, + "cost_source": "provider_usage", + "cost_kind": "reported", + "currency": "USD", + } + ) + partial = LlmResponse(partial=True) + assert attach_cost_metadata(partial) is not None + final = LlmResponse(partial=False) + final.custom_metadata = {"existing": True} + assert attach_cost_metadata(final) is not None + + +@pytest.mark.asyncio +async def test_non_stream_response_is_ledgered_and_attached_to_adk_event( + tmp_path: Path, +) -> None: + raw_response = { + "id": "generation-1", + "usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + "cost": 0.01, + "cost_details": {"upstream_inference_cost": 0.009}, + }, + } + begin_cost_capture( + user_id="user-1", + session_id="session-1", + invocation_id="inv-1", + ) + client = CostAwareLiteLLMClient(tmp_path / "costs.db") + with patch.object( + LiteLLMClient, + "acompletion", + new=AsyncMock(return_value=raw_response), + ): + returned = await client.acompletion( + model="openrouter/test", + messages=[], + tools=[], + stream=False, + ) + + assert returned is raw_response + response = LlmResponse( + content=types.Content(role="model", parts=[types.Part.from_text(text="ok")]) + ) + metadata = attach_cost_metadata(response) + assert metadata is not None + assert metadata["cost_usd"] == 0.01 + assert response.custom_metadata is not None + assert response.custom_metadata["blacki.cost"]["dedupe_key"].startswith("provider:") + + ledger = read_usage_ledger( + tmp_path / "costs.db", + selected_since=None, + selected_until=10**12, + month_start=0, + now=10**12, + ) + assert ledger.cumulative.records == 1 + assert ledger.cumulative.reported_records == 1 + assert ledger.users["user-1"].cost_nano_usd is not None + + +@pytest.mark.asyncio +async def test_streaming_final_usage_updates_one_idempotent_ledger_row( + tmp_path: Path, +) -> None: + async def response_stream() -> AsyncIterator[dict[str, object]]: + yield {"id": "generation-stream", "usage": {"total_tokens": 4}} + yield { + "id": "generation-stream", + "usage": { + "prompt_tokens": 3, + "completion_tokens": 1, + "total_tokens": 4, + "cost": 0.004, + }, + } + + begin_cost_capture( + user_id="user-stream", + session_id="session-stream", + invocation_id="inv-stream", + ) + client = CostAwareLiteLLMClient(tmp_path / "costs.db") + with patch.object( + LiteLLMClient, + "acompletion", + new=AsyncMock(return_value=response_stream()), + ): + returned = await client.acompletion( + model="openrouter/test", + messages=[], + tools=[], + stream=True, + ) + chunks = [chunk async for chunk in returned] + + assert len(chunks) == 2 + response = LlmResponse() + assert attach_cost_metadata(response)["cost_usd"] == 0.004 # type: ignore[index] + ledger = read_usage_ledger( + tmp_path / "costs.db", + selected_since=None, + selected_until=10**12, + month_start=0, + now=10**12, + ) + assert ledger.cumulative.records == 1 + assert ledger.cumulative.reported_records == 1 + + +def test_attach_cost_metadata_without_active_capture_is_noop() -> None: + assert attach_cost_metadata(LlmResponse()) is None diff --git a/tests/test_logging_callbacks.py b/tests/test_logging_callbacks.py index 61ace1c..e5ce84f 100644 --- a/tests/test_logging_callbacks.py +++ b/tests/test_logging_callbacks.py @@ -288,6 +288,28 @@ def test_after_model_without_llm_content( assert "*** After LLM call" in caplog.text assert "LLM response:" not in caplog.text + def test_after_model_logs_captured_cost( + self, + mock_logging_callback_context: MockLoggingCallbackContext, + mock_llm_response: MockLlmResponse, + caplog: pytest.LogCaptureFixture, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + caplog.set_level(logging.DEBUG) + monkeypatch.setattr( + "blacki.callbacks.attach_cost_metadata", + lambda _response: { + "cost_usd": 0.01, + "upstream_cost_usd": 0.009, + "cost_kind": "reported", + }, + ) + callbacks = LoggingCallbacks() + + callbacks.after_model(mock_logging_callback_context, mock_llm_response) # type: ignore + + assert "LLM cost captured" in caplog.text + class TestToolCallbacks: """Tests for tool invocation callbacks (before_tool and after_tool).""" diff --git a/tests/test_usage_ledger.py b/tests/test_usage_ledger.py new file mode 100644 index 0000000..166e640 --- /dev/null +++ b/tests/test_usage_ledger.py @@ -0,0 +1,205 @@ +"""Tests for the content-free model usage ledger.""" + +from __future__ import annotations + +import sqlite3 +from pathlib import Path + +import pytest + +from blacki.usage_ledger import ( + COST_LEDGER_WARNING, + UsageRecord, + _fixed_cost, + _summary_from_row, + _where_clause, + default_usage_ledger_path, + fixed_cost_to_usd, + read_usage_ledger, + write_usage_record, +) + + +def _record( + key: str, + *, + timestamp: float, + user_id: str = "user-1", + session_id: str = "session-1", + cost: float | None = None, + upstream: float | None = None, + estimate: float | None = None, + kind: str = "reported", +) -> UsageRecord: + return UsageRecord( + dedupe_key=key, + observed_at=timestamp, + user_id=user_id, + session_id=session_id, + invocation_id=f"inv-{key}", + model="openrouter/test-model", + provider_response_id=f"response-{key}", + input_tokens=10, + output_tokens=5, + total_tokens=15, + cost_usd=cost, + upstream_cost_usd=upstream, + estimated_cost_usd=estimate, + cost_kind=kind, + cost_source="provider_usage" if kind == "reported" else "test", + ) + + +def test_ledger_aggregates_fixed_point_costs_and_groups_users(tmp_path: Path) -> None: + path = tmp_path / "costs.db" + write_usage_record( + path, + _record("one", timestamp=100, cost=0.000000001, upstream=0.000000001), + ) + write_usage_record( + path, + _record( + "two", + timestamp=250, + user_id="user-2", + session_id="session-2", + cost=0.25, + upstream=0.2, + ), + ) + write_usage_record( + path, + _record( + "three", + timestamp=300, + user_id="user-1", + session_id="session-1", + estimate=0.5, + kind="estimated", + ), + ) + + snapshot = read_usage_ledger( + path, + selected_since=150, + selected_until=350, + month_start=200, + now=350, + ) + + assert snapshot.available is True + assert snapshot.selected.records == 2 + assert snapshot.cumulative.records == 3 + assert fixed_cost_to_usd(snapshot.cumulative.cost_nano_usd) == 0.250000001 + assert fixed_cost_to_usd(snapshot.monthly.cost_nano_usd) == 0.25 + assert snapshot.cumulative.reported_records == 2 + assert snapshot.cumulative.estimated_records == 1 + assert snapshot.users["user-2"].total_tokens == 15 + assert snapshot.monthly_users["user-2"].records == 1 + assert snapshot.sessions[("user-1", "session-1")].records == 2 + assert snapshot.monthly_sessions[("user-1", "session-1")].records == 1 + + +def test_ledger_upsert_does_not_double_count_stream_chunks(tmp_path: Path) -> None: + path = tmp_path / "costs.db" + write_usage_record( + path, + _record( + "same-response", + timestamp=100, + estimate=0.01, + kind="estimated", + ), + ) + write_usage_record( + path, + _record( + "same-response", + timestamp=100, + cost=0.02, + upstream=0.018, + kind="reported", + ), + ) + + snapshot = read_usage_ledger( + path, + selected_since=None, + selected_until=200, + month_start=1, + now=200, + ) + + assert snapshot.cumulative.records == 1 + assert snapshot.cumulative.reported_records == 1 + assert snapshot.cumulative.estimated_records == 0 + assert fixed_cost_to_usd(snapshot.cumulative.cost_nano_usd) == 0.02 + assert fixed_cost_to_usd(snapshot.cumulative.upstream_cost_nano_usd) == 0.018 + + +def test_ledger_missing_and_corrupt_files_are_non_mutating(tmp_path: Path) -> None: + missing = read_usage_ledger( + tmp_path / "missing.db", + selected_since=None, + selected_until=10, + month_start=1, + now=10, + ) + assert missing.available is False + assert missing.warnings == () + + corrupt_path = tmp_path / "corrupt.db" + corrupt_path.write_text("not sqlite", encoding="utf-8") + corrupt = read_usage_ledger( + corrupt_path, + selected_since=None, + selected_until=10, + month_start=1, + now=10, + ) + assert corrupt.available is False + assert corrupt.warnings == (COST_LEDGER_WARNING,) + + +def test_default_ledger_path_honors_explicit_environment( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + configured = tmp_path / "configured.db" + monkeypatch.setenv("BLACKI_COST_LEDGER_PATH", str(configured)) + assert default_usage_ledger_path() == configured + + monkeypatch.delenv("BLACKI_COST_LEDGER_PATH") + assert default_usage_ledger_path(tmp_path) == tmp_path / ".adk" / "costs.db" + + +def test_ledger_defensive_helpers_cover_invalid_values_and_filters( + tmp_path: Path, +) -> None: + assert _fixed_cost(-1) is None + assert _fixed_cost(1.7e308) is None + assert _summary_from_row(None).records == 0 + + where, values = _where_clause( + since=1, + until=2, + user_id="user-1", + session_id="session-1", + ) + assert where == ( + "observed_at >= ? AND observed_at <= ? AND user_id = ? AND session_id = ?" + ) + assert values == (1, 2, "user-1", "session-1") + assert _where_clause(since=None, until=None) == ("1 = 1", ()) + + empty_path = tmp_path / "empty.db" + connection = sqlite3.connect(empty_path) + connection.close() + empty = read_usage_ledger( + empty_path, + selected_since=None, + selected_until=10, + month_start=1, + now=10, + ) + assert empty.available is False + assert empty.warnings == ()