From a8272966b56a8be8ec89daccc63f937ef823df5f Mon Sep 17 00:00:00 2001 From: Rick Lan Date: Mon, 29 Jun 2026 15:26:28 +0800 Subject: [PATCH] feat(lon): acceleration EQ + accel logger Port the accel-eq feature onto testing: - AccelEq (accel_eq.py): tunable max-accel curve read from the dp_lon_accel_profiles param (validated, stock fallback, mtime-gated, personality-linked), feeding the accel clip. - AccelLogger (accel_logger.py): logs clean human-acceleration samples to /realdata/accel_log.csv. - Wired both into the ACM/AEM/APM longitudinal planner. - Feature README at dragonpilot/accel-eq.md. params_keys.h is generated from the settings entry at build (not committed here). Dashy web UI handled separately. --- dragonpilot/accel-eq.md | 254 +++++++++++++++++ dragonpilot/dashy/serverd.py | 241 +++++++++++++++- dragonpilot/dashy/web/dist/css/styles.css | 2 +- dragonpilot/dashy/web/dist/js/app.js | 55 ++-- .../selfdrive/controls/lib/accel_eq.py | 157 ++++++++++ .../selfdrive/controls/lib/accel_logger.py | 94 ++++++ .../controls/lib/tests/test_accel_eq.py | 269 ++++++++++++++++++ .../controls/lib/tests/test_accel_logger.py | 95 +++++++ .../selfdrive/ui/update_translations.py | 18 ++ dragonpilot/settings/__init__.py | 4 + dragonpilot/settings/min-feat.lon.accel-eq.py | 31 ++ dragonpilot/settings/openpilot.toggles.py | 3 + .../controls/lib/longitudinal_planner.py | 8 +- 13 files changed, 1193 insertions(+), 38 deletions(-) create mode 100644 dragonpilot/accel-eq.md create mode 100644 dragonpilot/selfdrive/controls/lib/accel_eq.py create mode 100644 dragonpilot/selfdrive/controls/lib/accel_logger.py create mode 100644 dragonpilot/selfdrive/controls/lib/tests/test_accel_eq.py create mode 100644 dragonpilot/selfdrive/controls/lib/tests/test_accel_logger.py create mode 100644 dragonpilot/settings/min-feat.lon.accel-eq.py diff --git a/dragonpilot/accel-eq.md b/dragonpilot/accel-eq.md new file mode 100644 index 000000000..87677e089 --- /dev/null +++ b/dragonpilot/accel-eq.md @@ -0,0 +1,254 @@ +# Acceleration EQ + +User-tunable longitudinal acceleration. The driver edits EQ-style **speed → max +acceleration** curves, grouped into named **profiles**, in the dashy web UI; the +longitudinal planner reads the active profile and uses its curve as the accel +ceiling. A companion **logger** records the driver's own acceleration (when they +control the throttle) so the UI can later show "where you actually accelerate". + +The dashy web UI lives in a **separate repo**; this doc covers the +dragonpilot/planner side (the contract, the planner reader, the logger) and +summarizes the dashy side it integrates with. + +--- + +## Architecture + +``` +┌─ dashy web UI (separate repo) ─┐ GET/POST ┌─ serverd ──────────┐ +│ EQ editor · profiles · the │ ──────────► │ params REST API + │ +│ personality→profile links │ /api/... │ /api/accel_eq/config│ +└────────────────────────────────┘ └─────────┬──────────┘ + │ Params + dp_lon_accel_profiles (JSON param) + │ + ┌─ longitudinal_planner.py ────────────────────▼───────────────┐ + │ AccelEq (accel_eq.py): read+cache JSON (mtime-gated), resolve │ + │ the active profile by personality/active → max_accel(v) │ + │ AccelLogger (accel_logger.py): log clean human-accel samples │ + │ → /data/media/0/realdata/accel_log.csv │ + └─────────────────────────────────────────────────────────────────┘ +``` + +Two stores, opposite directions: + +| Store | Holds | Written by | Read by | +|---|---|---|---| +| `dp_lon_accel_profiles` (JSON param) | the whole EQ doc — profiles + curves, the manual `active` selection, the personality links | dashy | planner | +| `/data/media/0/realdata/accel_log.csv` | the driver's natural-acceleration samples (`vEgo,aEgo`) | **planner** (AccelLogger) | dashy overlay (future) | + +The param is declared in `dragonpilot/settings/min-feat.lon.accel-eq.py` +(generated into `common/params_keys.h` at build). The CSV is telemetry, not +config, so it lives on the drive-log partition, not in the params store. + +--- + +## Data contract — `dp_lon_accel_profiles` + +The authoritative shape is what dashy's `serializeDoc` writes and the planner +reads. Example: + +```json +{ + "active": "Sport", + "use_personality": false, + "personality_map": { "0": "Sport", "1": "Stock", "2": "Eco" }, + "profiles": [ + { "name": "Stock", "max": {"bp": [0, 10, 25, 40], "v": [1.6, 1.2, 0.8, 0.6]} }, + { "name": "Eco", "source": "Stock", "max": {"bp": [0, 12, 30, 40], "v": [1.0, 0.8, 0.6, 0.5]} } + ] +} +``` + +- **`profiles`** — ordered list. Each has a non-empty **unique** `name` and a + `max` curve `{bp:[m/s…], v:[m/s²…]}`. Speeds (`bp`) are stored in **m/s** + regardless of the UI display unit. A missing/invalid `max` → that profile uses + stock. +- **`active`** — the manually selected profile name (used when `use_personality` + is off). There is **no separate `active` param** — it lives in this doc. +- **`use_personality`** + **`personality_map`** — the personality link (below). +- **`source`** (optional) — UI lineage only (the duplicate-from profile, drawn + as the grey reference line). The planner ignores it and any other unknown key. + +> **"Stock"** is a UI-protected baseline (not renamable/deletable, read-only in +> the editor). The planner treats it as a **live mirror of the injected stock +> table** — it ignores any stored curve named "Stock", so a planner stock change +> always wins. + +There is **no `version` field and no turn-limit curve**: schema changes are +handled by versioning the release (or, if ever incompatible, the param key); the +turn-accel limit is the planner's stock `_A_TOTAL_MAX_*` and is not tunable. + +--- + +## Profile selection + +Resolved on init, whenever the param mtime changes, and whenever the personality +changes. The rule: + +``` +read/cache the JSON doc + doc invalid / empty / missing → STOCK (regardless of personality) + doc valid: + use_personality == true → personality_map[personality] + use_personality == false → active + → resolve that name to a profile (exact match) and validate its curve + matched + valid → that profile's curve + unmapped / unset / no match / Stock / invalid curve → STOCK +``` + +**Stock is the single universal fallback.** No "first profile" fallback, and +when `use_personality` is on an unmapped personality falls to Stock (not to the +manual `active`). + +| `use_personality` | situation | result | +|---|---|---| +| true | personality mapped → real profile, valid curve | that profile | +| true | personality unmapped | **Stock** | +| false | `active` → real profile, valid curve | that profile | +| false | `active` unset/empty | **Stock** | +| either | name points at a missing profile / is "Stock" / curve invalid | **Stock** | +| either | doc missing / empty / unparseable / not a usable dict | **Stock** | + +### Personality link + +For each openpilot personality the driver links a profile, stored in +`personality_map` keyed by the `LongitudinalPersonality` enum int — **`"0"` +aggressive, `"1"` standard, `"2"` relaxed**. With `use_personality` on, the +existing **personality button** selects the profile (no new onroad control). + +The planner reads the live personality from `selfdriveState.personality.raw` +(the int) — the same source the MPC uses — not from the param. + +> **Coupling, by design:** `LongitudinalPersonality` also drives openpilot's +> follow distance, so a personality link makes the button a single "aggression" +> dial (accel feel + following together). Turn the link off to set them +> independently (manual `active`). + +--- + +## Planner: `AccelEq` (`dragonpilot/selfdrive/controls/lib/accel_eq.py`) + +Instantiated in `LongitudinalPlanner` and pure-observation safe — any failure +falls back to stock and never raises into planning. + +- **Stock is injected**, not hardcoded: the planner owns the canonical + `A_CRUISE_MAX_*` table and passes it in (`AccelEq(A_CRUISE_MAX_BP, + A_CRUISE_MAX_VALS)`). This keeps a single source of truth and lets `accel_eq` + stay a leaf module (it never imports the planner — that would be circular). +- **Read is mtime-gated; the parsed doc is cached.** `maybe_refresh(personality)` + `stat()`s the param each frame; only an **mtime change** triggers a re-read + (`_reload_doc` → `Params.get`). A **personality change** just re-resolves the + cached doc (`_resolve`) — zero I/O. +- **`max_accel(v_ego)`** = `np.interp` over the active curve, feeding the + planner's accel clip. The default personality is a `-1` sentinel so the first + real `maybe_refresh` always resolves. + +`limit_accel_in_turns` is unchanged stock (uses `_A_TOTAL_MAX_*`). + +### Validation is a safety boundary — `_validate_curve` + +The param is externally writable (hand-edit, script, an older/buggy dashy), so +the planner never trusts it. `_validate_curve` re-sorts and bounds every curve +before it can shape the accel ceiling: + +- shape: dict with equal-length `bp`/`v` lists, `MIN_PTS(2) ≤ n ≤ MAX_PTS(12)`, + all finite numbers; +- **sort** pairs by speed (required — `np.interp` needs increasing `bp`); +- clamp speed to `[0, SPEED_CEIL(60)]`; reject if any adjacent gap `< MIN_GAP(0.5)`; +- **clamp value to `[0, MAX_ACCEL_CEIL]`** (`= ACCEL_MAX`, 2.0 m/s²) — the actual + accel cap. + +Anything that fails → that curve is unusable → stock. dashy mirrors these exact +rules client-side (UX) so the editor can't author a curve the planner rejects; +the planner's copy is the guarantee. + +--- + +## Logger: `AccelLogger` (`dragonpilot/selfdrive/controls/lib/accel_logger.py`) + +Logs the driver's **natural** acceleration so the data reflects *their* +preference, not openpilot's. Only "clean free human-acceleration" samples are +kept — `_should_log` requires **all** of: + +| condition | signal | +|---|---| +| op long not active (human owns throttle) | `controlsState.longControlState == off` | +| accelerating on the gas | `gasPressed and aEgo > 0` | +| not braking | `not brakePressed` | +| no blinker (not turning/lane-changing) | `not (leftBlinker or rightBlinker)` | +| in Drive | `gearShifter == drive` | +| moving (not creeping/stopped) | `not standstill and vEgo > 1.0` | +| no near lead | no lead, or `dRel / max(vEgo, 0.1) > 2.0 s` | +| straight (not in a curve) | lateral accel `< 1.0 m/s²` | + +Behavior: buffer matching `(vEgo, aEgo)` in RAM and **append to the CSV once a +minute** (`FLUSH_DT`, frame-counted) — one write/min spares the flash; up to +~1 min of samples is lost on a hard shutdown (negligible for an aggregate). +Fully exception-isolated; on a write failure it drops the buffered rows and +no-ops (e.g. path not writable in dev). The clean-sample gate makes growth slow, +so there is no size cap. CSV columns: `vEgo,aEgo` (m/s, m/s²). + +--- + +## Constants — single source of truth + +`accel_eq.py` owns the contract constants (`SPEED_CEIL`, `MIN_GAP`, `MIN_PTS`, +`MAX_PTS`, `MAX_ACCEL_CEIL`). serverd serves them at `GET /api/accel_eq/config` +and the dashy model applies them over its built-in fallback defaults at load, so +the editor's limits can't drift from the planner. (`MAX_PTS` especially must +match: a curve above the planner cap would be silently rejected → stock.) + +--- + +## Persistence round-trip + +dashy serializes the doc to a **JSON string**; the planner reads a parsed +**dict**. serverd bridges them: + +1. dashy `POST /api/settings/params/dp_lon_accel_profiles` with the JSON string. +2. serverd `_save_param` detects the JSON-typed param and `json.loads`-es the + string, then `Params.put(key, dict)` (the `(dict, JSON)` caster stores it). + Malformed JSON → **400**, not 500. +3. planner `Params.get(...)` → parsed dict → `AccelEq._reload_doc` caches it. + +`dp_lon_accel_profiles` is allowlisted in serverd's `_param_allowed`. + +--- + +## Dashy side (separate repo) + +The web UI implements: the canvas **EQ editor** (drag points, add/remove, +numeric entry, reset, undo/redo, a live "you are here" speed marker), **profile +management** (create/duplicate/rename/delete, quick-switch), the +**personality→profile pickers**, and a client-side **mirror of +`_validate_curve`** so the editor enforces the planner's limits. It talks to +serverd's params REST API and `/api/accel_eq/config`. The display unit follows +`IsMetric`; storage is always m/s. + +--- + +## Files (this repo) + +| File | Role | +|---|---| +| `dragonpilot/selfdrive/controls/lib/accel_eq.py` | `AccelEq` — profile reader/resolver | +| `dragonpilot/selfdrive/controls/lib/accel_logger.py` | `AccelLogger` — accel CSV logger | +| `dragonpilot/selfdrive/controls/lib/tests/test_accel_eq.py` | AccelEq tests | +| `dragonpilot/selfdrive/controls/lib/tests/test_accel_logger.py` | AccelLogger tests | +| `dragonpilot/settings/min-feat.lon.accel-eq.py` | declares `dp_lon_accel_profiles` | +| `selfdrive/controls/lib/longitudinal_planner.py` | owns `A_CRUISE_MAX_*`, wires in `AccelEq` + `AccelLogger` | + +Run tests: `uv run python -m pytest dragonpilot/selfdrive/controls/lib/tests/ -q` + +--- + +## Notes / future + +- **Habit overlay** in dashy — read `accel_log.csv` (server-side aggregate to an + 85th-percentile band) and draw it behind the EQ curve. Needs no new param. +- The **turn-limit channel** is intentionally not tunable (dropped from the UI + and the planner; turn limit stays stock). +- dashy follow-ups to keep in sync with this side: trim serverd's + `ACCEL_EQ_CONFIG` to the constants `accel_eq` still exposes, and mirror the + Stock-fallback selection rule in the dashy model. diff --git a/dragonpilot/dashy/serverd.py b/dragonpilot/dashy/serverd.py index ebac781a1..9219ff53b 100755 --- a/dragonpilot/dashy/serverd.py +++ b/dragonpilot/dashy/serverd.py @@ -51,6 +51,14 @@ from openpilot.system.hardware import PC, HARDWARE from openpilot.system.ui.lib.multilang import multilang as base_multilang from dragonpilot.settings import SETTINGS +# dragonpilot's translation catalog (.mo) is where dashy's JS strings are merged +# by update_translations.py; used to build the web UI's translation map. Falls +# back to base multilang if the dragonpilot wrapper isn't importable. +try: + from dragonpilot.system.ui.lib.multilang import multilang as dp_multilang +except Exception: + dp_multilang = base_multilang + try: from openpilot.system.version import get_build_metadata as _get_build_metadata except Exception: @@ -61,6 +69,26 @@ DEFAULT_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), '..') if WEB_DIST_PATH = os.path.join(os.path.dirname(__file__), "web", "dist") CAR_PARAMS_CACHE_TTL = 30 # seconds +# Accel-EQ contract constants — single source of truth is the planner's +# accel_eq.py. Served to the web UI so its model can't drift from the +# planner (a curve the planner would reject must be un-authorable in the editor). +# Guarded: standalone/dev dashy without the dragonpilot tree falls back to the +# web model's own built-in defaults (ACCEL_EQ_CONFIG stays None → /api endpoint +# 404s → JS keeps defaults). +try: + from dragonpilot.selfdrive.controls.lib import accel_eq as _AC + # Only the scalar contract constants accel_eq still owns. Turn and the + # schema version were dropped; stock is no longer an accel_eq constant — + # it's injected into AccelEq from the planner's A_CRUISE_MAX table, so it + # isn't served here (the dashy model keeps its built-in stock default, + # which matches A_CRUISE_MAX). + ACCEL_EQ_CONFIG = { + "max_pts": _AC.MAX_PTS, "min_pts": _AC.MIN_PTS, "min_gap": _AC.MIN_GAP, + "speed_ceil": _AC.SPEED_CEIL, "max_accel_ceil": _AC.MAX_ACCEL_CEIL, + } +except Exception: + ACCEL_EQ_CONFIG = None + logger = logging.getLogger("dashy") @@ -147,6 +175,14 @@ class AppCache: except Exception: return False + def lang_switch_available(self): + """Whether the dashy language switcher should be offered — always. + + Native language selection isn't device-gated, and the switcher is + harmless even where a native one exists, so dashy offers it on every + device (previously excluded comma 3/3x).""" + return True + def _is_release_channel(self): if _get_build_metadata is None: return False @@ -279,21 +315,70 @@ _CONTROL_PARAMS = { 'dp_dev_go_off_road', # Controls tab: force-offroad toggle 'DoReboot', # Controls tab: reboot button 'ExperimentalMode', # Tesla HUD: tap set-speed circle to toggle + 'LanguageSetting', # Settings tab: language switch (comma-4; "main_") + 'dp_lon_accel_profiles', # Accel EQ: profile library (JSON) — includes the active selection } +# Control params whose value is a string/JSON, not a bool. The generic +# control-param GET path reads bools (get_bool); these must be read as +# their raw string so the web UI gets the actual value back. POST already +# stores them as strings via _save_param's default branch. +_RAW_STRING_PARAMS = { + 'dp_lon_accel_profiles', +} + +assert _RAW_STRING_PARAMS <= _CONTROL_PARAMS, "raw-string params must also be allowlisted in _CONTROL_PARAMS" + def _param_allowed(key): return key in _PARAM_SETTINGS or key in _CONTROL_PARAMS +def _sync_language(params): + """Apply the current LanguageSetting param to multilang. + + The language switch only writes the param; this makes the translation + catalog reflect it (so served strings / the i18n map use the right language).""" + current_lang = params.get("LanguageSetting") + if not current_lang: + return + lang_str = current_lang.decode() if isinstance(current_lang, bytes) else str(current_lang) + lang_str = lang_str.removeprefix("main_") + if lang_str != base_multilang.language and lang_str in base_multilang.languages.values(): + base_multilang._language = lang_str + base_multilang.setup() + + +def _build_i18n_map(): + """Translation map {english: translated} for the active language, consumed + by the web UI's tr(). Sourced from the dragonpilot .mo catalog, where dashy's + JS strings are merged by update_translations.py. Empty for English / when no + catalog is loaded → the web tr() falls back to the original English text.""" + try: + dp_multilang._ensure_loaded() + catalog = getattr(dp_multilang._dragon_translation, '_catalog', None) + except Exception: + catalog = None + if not catalog: + return {} + # GNUTranslations._catalog keys are msgid strings; skip the "" header entry + # and the '\x00'-joined plural keys, and drop empty translations. + return {k: v for k, v in catalog.items() + if isinstance(k, str) and k and '\x00' not in k and v} + + # --- API Endpoints --- @api_handler async def init_api(request): """Provide initial data to the client.""" cache: AppCache = request.app['cache'] + _sync_language(cache.params) return web.json_response({ 'dp_dev_dashy': cache.get_bool_safe("dp_dev_dashy", True), 'isOffroad': cache.get_bool_safe("IsOffroad", False), + # Translation map for the web UI's tr() — shipped at boot so strings are + # localized before the settings panel can open. + 'i18n': _build_i18n_map(), }) @@ -384,14 +469,8 @@ async def get_settings_config_api(request): params = cache.params - # Update language if changed - current_lang = params.get("LanguageSetting") - if current_lang: - lang_str = current_lang.decode() if isinstance(current_lang, bytes) else str(current_lang) - lang_str = lang_str.removeprefix("main_") - if lang_str != base_multilang.language and lang_str in base_multilang.languages.values(): - base_multilang._language = lang_str - base_multilang.setup() + # Apply the current LanguageSetting (the switch only writes the param). + _sync_language(params) context = cache.get_settings_context() settings_with_values = [] @@ -425,7 +504,24 @@ async def get_settings_config_api(request): section_copy['settings'] = settings_list settings_with_values.append(section_copy) - response_data = {'settings': settings_with_values} + # Language switcher metadata rides along with the settings (it's settings- + # adjacent and the UI already fetches this). The list comes from the device's + # translation catalog, not a param; the switch itself writes the + # LanguageSetting param via the generic /api/settings/params endpoint. + # Offered only where lang_switch_available() (not comma 3/3x). + # Labels mirror openpilot's raylib device UI (tr("Change Language") / + # tr("Select a language")) and are translated by the same catalog. + languages = ({'available': True, + 'current': base_multilang.language, + 'list': base_multilang.languages, + 'title': base_multilang.tr('Change Language'), + 'dialog': base_multilang.tr('Select a language')} + if cache.lang_switch_available() else {'available': False}) + + # Refresh the web UI's translation map too, so switching language updates + # dashy's own strings (not just the SETTINGS labels) without a reload. + response_data = {'settings': settings_with_values, 'languages': languages, + 'i18n': _build_i18n_map()} cache._settings_cache = response_data cache._settings_cache_time = now return web.json_response(response_data) @@ -492,7 +588,19 @@ async def save_param_api(request): if 'value' not in data: return web.json_response({'error': 'value is required in body'}, status=400) - _save_param(params, param_name, data['value']) + try: + _save_param(params, param_name, data['value']) + except ValueError as e: + # malformed JSON / un-coercible INT|FLOAT body — a client error, not a 500 + return web.json_response({'error': f'Invalid value for {param_name}: {e}'}, status=400) + + # Mirror upstream openpilot's TogglesLayout: a needs_restart param also + # requests an onroad cycle so the change takes effect (openpilot restarts + # when the car is powered on). The web UI blocks these toggles while engaged + # so this can't fire mid-drive. + if setting is not None and setting.get('needs_restart'): + params.put_bool("OnroadCycleRequested", True) + cache.invalidate() logger.info(f"Param saved: {param_name}={data['value']}") @@ -510,6 +618,12 @@ def _save_param(params, key, value): params.put(key, int(value)) elif param_type == 3: # FLOAT params.put(key, float(value)) + elif param_type == 5: # JSON + # Parse an incoming JSON string to a dict/list so Params.put's + # (dict|list, JSON) caster (json.dumps) stores it; put() has no + # (str, JSON) caster and would raise. Malformed JSON raises here and + # is surfaced to the client. Already-parsed bodies pass through. + params.put(key, json.loads(value) if isinstance(value, str) else value) elif isinstance(value, bool): params.put_bool(key, value) else: @@ -521,12 +635,117 @@ def _save_param(params, key, value): raise +@api_handler +async def get_accel_eq_config_api(request): + """Serve the planner's accel-eq contract constants (the single source of + truth in accel_eq.py) so the web model can't drift from the planner. + 404 when unavailable — the web UI uses this to gate the whole Accel tab. + + "Available" means BOTH: the planner lib imported (constants known) AND the + dp_lon_accel_profiles param is registered on this build (so saves work). + Checking the param registration — not just the import — avoids showing a + tab whose saves would fail on a build where the planner side isn't deployed. + Standalone/dev dashy → 404 → tab hidden, web model keeps built-in defaults.""" + if ACCEL_EQ_CONFIG is None: + return web.json_response({'error': 'accel-eq config unavailable'}, status=404) + params = request.app['cache'].params + try: + params.check_key('dp_lon_accel_profiles') # raises if not in the params manifest + except Exception: + return web.json_response({'error': 'accel-eq param unavailable'}, status=404) + return web.json_response(ACCEL_EQ_CONFIG) + + +@api_handler +async def get_accel_eq_habit_api(request): + """Serve logged (speed, accel) samples for the Accel-EQ 'habit cloud' overlay. + + Reads accel_log.csv from the data dir (where the planner's AccelLogger writes + it) — two columns: speed m/s, accel m/s². Uniform-strided down to ~2000 + points so the density is preserved without shipping tens of thousands of + samples. Missing/empty file → {'points': []} (the overlay just doesn't show).""" + import bisect + path = os.path.join(DEFAULT_DIR, 'accel_log.csv') + samples = [] + try: + with open(path) as f: + for line in f: + parts = line.split(',') + if len(parts) < 2: + continue + try: + samples.append((float(parts[0]), float(parts[1]))) + except ValueError: + continue + except FileNotFoundError: + pass + + # Reference grid: sliding windows over speed that hold enough samples. Thin + # windows are SKIPPED (not bailed on), so a sparse very-low-speed slice (the + # log starts at 1 m/s) or a thin high-speed tail doesn't wipe out the whole + # reference — it just isn't drawn there. + ordered = sorted(samples, key=lambda t: t[0]) + speeds = [t[0] for t in ordered] + accels = [t[1] for t in ordered] + STEP, HALF, MIN_W = 0.5, 1.5, 60 + grid = [] # (speed, sorted accel window) + s = 0.0 + while s <= 40.0: + lo = bisect.bisect_left(speeds, s - HALF) + hi = bisect.bisect_right(speeds, s + HALF) + if hi - lo >= MIN_W: + grid.append((s, sorted(accels[lo:hi]))) + s += STEP + + # Availability probe (?meta=1): sample count + how many reference points we + # could draw, so the UI offers the overlay only when it's actually useful — + # without shipping the dataset. + if request.query.get('meta'): + return web.json_response({'count': len(samples), 'bands': len(grid)}) + + # Cloud: uniform stride to ~2000 samples (preserves the density distribution). + stride = max(1, len(samples) // 2000) + points = [[round(sp, 2), round(a, 3)] for sp, a in samples[::stride]] + + # Envelope: three smooth reference lines for setting a max-accel ceiling — + # usual (p75), brisk (p90) and hardest (p98) of how hard you accelerate at + # each speed. All upper-range: most samples are gentle partial throttle, so + # low percentiles just hug zero and don't inform a ceiling. Moving-average + # smoothed and forced non-increasing (max-accel eases with speed). + def _band(pct): + raw = [(gs, win[min(len(win) - 1, int(pct * len(win)))]) for gs, win in grid] + out, cur = [], float('inf') + for i in range(len(raw)): + a = max(0, i - 2); b = min(len(raw), i + 3) # moving average ±2 + avg = sum(v for _, v in raw[a:b]) / (b - a) + cur = min(cur, avg) # non-increasing + out.append([round(raw[i][0], 1), round(cur, 2)]) + return out + + envelope = {'lower': _band(0.75), 'mid': _band(0.90), 'upper': _band(0.98)} + return web.json_response({'points': points, 'envelope': envelope}) + + def _get_param_value(params, key): """Get a single param value via its declared setting type, or as a bool for control-only params that have no SETTINGS entry.""" setting = _PARAM_SETTINGS.get(key) if setting is not None: return _get_setting_value(params, setting) + if key in _RAW_STRING_PARAMS: + try: + raw = params.get(key) + if raw is None: + return None + # JSON-typed params (e.g. dp_lon_accel_profiles) come back from + # Params.get already json.loads'd into a dict/list — re-serialize so + # the web gets valid JSON, not Python repr (str(dict) → single quotes, + # which JSON.parse can't read → client silently falls back to seed). + if isinstance(raw, (dict, list)): + return json.dumps(raw) + return raw.decode('utf-8', errors='replace') if isinstance(raw, bytes) else str(raw) + except Exception: + return None if key in _CONTROL_PARAMS: try: return params.get_bool(key) @@ -822,6 +1041,8 @@ def setup_aiohttp_app(host: str, port: int, debug: bool): app.router.add_get("/api/play", serve_player_api) app.router.add_get("/api/manifest.m3u8", serve_manifest_api) app.router.add_get("/api/settings", get_settings_config_api) + app.router.add_get("/api/accel_eq/config", get_accel_eq_config_api) + app.router.add_get("/api/accel_eq/habit", get_accel_eq_habit_api) app.router.add_get("/api/settings/params/{param_name}", get_param_api) app.router.add_post("/api/settings/params/{param_name}", save_param_api) app.router.add_get("/api/models", get_model_list_api) diff --git a/dragonpilot/dashy/web/dist/css/styles.css b/dragonpilot/dashy/web/dist/css/styles.css index dd03bf6cc..50103a992 100644 --- a/dragonpilot/dashy/web/dist/css/styles.css +++ b/dragonpilot/dashy/web/dist/css/styles.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-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight: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-duration:initial;--tw-ease: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;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--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);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--radius-lg:.5rem;--radius-2xl:1rem;--ease-out:cubic-bezier(0,0,.2,1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--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{background:var(--page-scroll-bg,var(--root-bg));--page-scroll-bg-on:linear-gradient(var(--root-bg,#0000),var(--root-bg,#0000))var(--root-bg,#0000)}@supports (color:color-mix(in lab, red, red)){:root{--page-scroll-bg-on:linear-gradient(var(--root-bg,#0000),var(--root-bg,#0000))color-mix(in srgb,var(--root-bg,#0000),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)}@keyframes set-page-has-scroll{0%,to{--page-has-scroll:1}}:root,[data-theme]{background:var(--page-scroll-bg,var(--root-bg));color:var(--color-base-content)}:where(:root,[data-theme]){--root-bg:var(--color-base-100)}}@layer components;@layer utilities{@layer daisyui.l1.l2.l3{.diff{webkit-user-select:none;-webkit-user-select:none;user-select:none;direction:ltr;grid-template-rows:1fr 1.8rem 1fr;grid-template-columns:auto 1fr;width:100%;display:grid;position:relative;overflow:hidden;container-type:inline-size}.diff:focus-visible,.diff:has(.diff-item-1:focus-visible),.diff:focus-visible{outline-style:var(--tw-outline-style);outline-offset:1px;outline-width:2px;outline-color:var(--color-base-content)}.diff:focus-visible .diff-resizer{min-width:95cqi;max-width:95cqi}.diff:has(.diff-item-1:focus-visible){outline-style:var(--tw-outline-style);outline-offset:1px;outline-width:2px}.diff:has(.diff-item-1:focus-visible) .diff-resizer{min-width:5cqi;max-width:5cqi}@supports (-webkit-overflow-scrolling:touch) and (overflow:-webkit-paged-x){.diff:focus .diff-resizer{min-width:5cqi;max-width:5cqi}.diff:has(.diff-item-1:focus) .diff-resizer{min-width:95cqi;max-width:95cqi}}.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(69% - var(--border) + .25px),var(--tab-border-color)69%,var(--tab-bg)calc(69% + .25px);order:var(--tab-order);height:var(--tab-height);padding-inline:var(--tab-p);border-color:#0000;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}: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,min-height .2s ease-out allow-discrete,padding .1s ease-out 20ms,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);width:100%;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}@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)}}.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)*-100cqw) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100cqw*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)*-100cqw) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100cqw*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)*-100cqw) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100cqw*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)*-100cqw) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100cqw*var(--range-fill))}}.range:disabled{cursor:not-allowed;opacity:.3}.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}.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}.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%}.hero{background-position:50%;background-size:cover;place-items:center;width:100%;display:grid}.hero>*{grid-row-start:1;grid-column-start:1}.modal-box{background-color:var(--color-base-100);border-top-left-radius:var(--modal-tl,var(--radius-box));border-top-right-radius:var(--modal-tr,var(--radius-box));border-bottom-left-radius:var(--modal-bl,var(--radius-box));border-bottom-right-radius:var(--modal-br,var(--radius-box));opacity:0;overscroll-behavior:contain;grid-row-start:1;grid-column-start:1;width:91.6667%;max-width:32rem;max-height:100vh;padding:1.5rem;transition:translate .3s ease-out,scale .3s ease-out,opacity .2s ease-out 50ms,box-shadow .3s ease-out;overflow-y:auto;scale:95%;box-shadow:0 25px 50px -12px oklch(0% 0 0/.25)}.divider{white-space:nowrap;height:1rem;margin:var(--divider-m,1rem 0);--divider-color:var(--color-base-content);flex-direction:row;align-self:stretch;align-items:center;display:flex}@supports (color:color-mix(in lab, red, red)){.divider{--divider-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.divider:before,.divider:after{content:"";background-color:var(--divider-color);flex-grow:1;width:100%;height:.125rem}@media print{.divider:before,.divider:after{border:.5px solid}}.divider:not(:empty){gap:1rem}.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)}}.modal-action{justify-content:flex-end;gap:.5rem;margin-top:1.5rem;display:flex}.breadcrumbs{max-width:100%;padding-block:.5rem;overflow-x:auto}.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol{white-space:nowrap;align-items:center;min-height:min-content;display:flex}:is(.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol)>li{align-items:center;display:flex}:is(.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol)>li>*{cursor:pointer;align-items:center;gap:.5rem;display:flex}@media (hover:hover){:is(.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol)>li>:hover{text-decoration-line:underline}}:is(.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol)>li>:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){:is(.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol)>li>:focus{outline-offset:2px;outline:2px solid #0000}}:is(.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol)>li>:focus-visible{outline-offset:2px;outline:2px solid}:is(.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol)>li+:before{content:"";opacity:.4;background-color:#0000;border-top:1px solid;border-right:1px solid;width:.375rem;height:.375rem;margin-left:.5rem;margin-right:.75rem;display:block;rotate:45deg}[dir=rtl] :is(:is(.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol)>li)+:before{rotate:-135deg}.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));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);width:fit-content;height:var(--size);padding-inline:calc(var(--size)/2 - var(--border));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}.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)}.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-error{--btn-color:var(--color-error);--btn-fg:var(--color-error-content)}.btn-neutral{--btn-color:var(--color-neutral);--btn-fg:var(--color-neutral-content)}.btn-primary{--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content)}.btn-success{--btn-color:var(--color-success);--btn-fg:var(--color-success-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)}}.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>*{grid-row-start:1}.checkbox:disabled{cursor:not-allowed;opacity:.2}: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}.btn-circle{width:var(--size);height:var(--size);border-radius:3.40282e38px;padding-inline:0}.btn-square{width:var(--size);height:var(--size);padding-inline:0}.loading-lg{width:calc(var(--size-selector,.25rem)*7)}.loading-md{width:calc(var(--size-selector,.25rem)*6)}.loading-sm{width:calc(var(--size-selector,.25rem)*5)}.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")}.link-hover{text-decoration-line:none}@media (hover:hover){.link-hover:hover{text-decoration-line:underline}}.btn-lg{--fontsize:1.125rem;--btn-p:1.25rem;--size:calc(var(--size-field,.25rem)*12)}.btn-md{--fontsize:.875rem;--btn-p:1rem;--size:calc(var(--size-field,.25rem)*10)}.btn-sm{--fontsize:.75rem;--btn-p:.75rem;--size:calc(var(--size-field,.25rem)*8)}.btn-xs{--fontsize:.6875rem;--btn-p:.5rem;--size:calc(var(--size-field,.25rem)*6)}.toggle-lg[type=checkbox],.toggle-lg:has([type=checkbox]){--size:calc(var(--size-selector,.25rem)*7)}.toggle-primary:checked,.toggle-primary[aria-checked=true]{--input-color:var(--color-primary)}}.prose :where(a.btn:not(.btn-link)):not(:where([class~=not-prose],[class~=not-prose] *)){text-decoration-line:none}.pointer-events-none{pointer-events:none}.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}.inset-0{inset:calc(var(--spacing)*0)}.right-0{right:calc(var(--spacing)*0)}.bottom-0{bottom:calc(var(--spacing)*0)}.left-0{left:calc(var(--spacing)*0)}.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)}.join>:where(:focus,:has(:focus)){z-index:1}@media (hover:hover){.join>:where(.btn:hover,:has(.btn:hover)){isolation:isolate}}.z-10{z-index:10}.z-\[300\]{z-index:300}.z-\[400\]{z-index:400}.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-3{margin-inline:calc(var(--spacing)*3)}.mx-auto{margin-inline:auto}.my-1{margin-block:calc(var(--spacing)*1)}.join-item:where(:not(:first-child,:disabled,[disabled],.btn-disabled)){margin-block-start:0;margin-inline-start:calc(var(--border,1px)*-1)}.join-item:where(:is(:disabled,[disabled],.btn-disabled)){border-width:var(--border,1px)0 var(--border,1px)var(--border,1px)}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mr-2{margin-right:calc(var(--spacing)*2)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-4{margin-left:calc(var(--spacing)*4)}.alert{border-width:var(--border);border-color:var(--alert-border-color,var(--color-base-200))}.block{display:block}.flex{display:flex}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.table{display:table}.h-1{height:calc(var(--spacing)*1)}.h-24{height:calc(var(--spacing)*24)}.h-full{height:100%}.max-h-40{max-height:calc(var(--spacing)*40)}.max-h-\[80vh\]{max-height:80vh}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-12{min-height:calc(var(--spacing)*12)}.w-10{width:calc(var(--spacing)*10)}.w-12{width:calc(var(--spacing)*12)}.w-24{width:calc(var(--spacing)*24)}.w-48{width:calc(var(--spacing)*48)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-24{max-width:calc(var(--spacing)*24)}.max-w-sm{max-width:var(--container-sm)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-12{min-width:calc(var(--spacing)*12)}.min-w-24{min-width:calc(var(--spacing)*24)}.min-w-32{min-width:calc(var(--spacing)*32)}.min-w-36{min-width:calc(var(--spacing)*36)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.resize{resize:both}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}: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-content\/10>:not(:last-child)){border-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:where(.divide-base-content\/10>:not(:last-child)){border-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-t-2xl{border-top-left-radius:var(--radius-2xl);border-top-right-radius:var(--radius-2xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-none{--tw-border-style:none;border-style:none}.border-base-content\/10{border-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.border-base-content\/10{border-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.bg-base-100\/50{background-color:var(--color-base-100)}@supports (color:color-mix(in lab, red, red)){.bg-base-100\/50{background-color:color-mix(in oklab,var(--color-base-100)50%,transparent)}}.bg-base-200,.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-300,.bg-base-300\/30{background-color:var(--color-base-300)}@supports (color:color-mix(in lab, red, red)){.bg-base-300\/30{background-color:color-mix(in oklab,var(--color-base-300)30%,transparent)}}.bg-base-300\/50{background-color:var(--color-base-300)}@supports (color:color-mix(in lab, red, red)){.bg-base-300\/50{background-color:color-mix(in oklab,var(--color-base-300)50%,transparent)}}.bg-base-300\/90{background-color:var(--color-base-300)}@supports (color:color-mix(in lab, red, red)){.bg-base-300\/90{background-color:color-mix(in oklab,var(--color-base-300)90%,transparent)}}.bg-base-content\/20{background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.bg-base-content\/20{background-color:color-mix(in oklab,var(--color-base-content)20%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab, red, red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black)60%,transparent)}}.bg-black\/95{background-color:#000000f2}@supports (color:color-mix(in lab, red, red)){.bg-black\/95{background-color:color-mix(in oklab,var(--color-black)95%,transparent)}}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-8{padding:calc(var(--spacing)*8)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-8{padding-block:calc(var(--spacing)*8)}.pt-3{padding-top:calc(var(--spacing)*3)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.pb-3{padding-bottom:calc(var(--spacing)*3)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--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))}.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)}.break-all{word-break:break-all}.whitespace-pre-wrap{white-space:pre-wrap}.text-base-content,.text-base-content\/50{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/50{color:color-mix(in oklab,var(--color-base-content)50%,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\/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-primary,.text-primary\/70{color:var(--color-primary)}@supports (color:color-mix(in lab, red, red)){.text-primary\/70{color:color-mix(in oklab,var(--color-primary)70%,transparent)}}.text-white\/70{color:#ffffffb3}@supports (color:color-mix(in lab, red, red)){.text-white\/70{color:color-mix(in oklab,var(--color-white)70%,transparent)}}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.shadow{--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)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px 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)}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);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}}.btn-outline:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn),:disabled,[disabled],.btn-disabled){--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color);--btn-border:var(--btn-color);--btn-noise:none}@media (hover:none){.btn-outline:not(.btn-active,:active,:focus-visible,input:checked:not(.filter .btn)):hover{--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color);--btn-border:var(--btn-color);--btn-noise:none}}}.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,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.placeholder\:text-base-content\/50::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.placeholder\:text-base-content\/50::placeholder{color:color-mix(in oklab,var(--color-base-content)50%,transparent)}}@media (hover:hover){.hover\:border-primary\/30:hover{border-color:var(--color-primary)}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/30:hover{border-color:color-mix(in oklab,var(--color-primary)30%,transparent)}}.hover\:bg-base-100:hover{background-color:var(--color-base-100)}.hover\:bg-base-200:hover{background-color:var(--color-base-200)}.hover\:bg-base-300\/50:hover{background-color:var(--color-base-300)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-base-300\/50:hover{background-color:color-mix(in oklab,var(--color-base-300)50%,transparent)}}.hover\:bg-error\/10:hover{background-color:var(--color-error)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-error\/10:hover{background-color:color-mix(in oklab,var(--color-error)10%,transparent)}}}.focus\:ring-0:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:bg-base-100\/50:active{background-color:var(--color-base-100)}@supports (color:color-mix(in lab, red, red)){.active\:bg-base-100\/50:active{background-color:color-mix(in oklab,var(--color-base-100)50%,transparent)}}.active\:bg-base-300:active{background-color:var(--color-base-300)}@media (min-width:48rem){@layer daisyui.l1.l2{.md\:btn-lg{--fontsize:1.125rem;--btn-p:1.25rem;--size:calc(var(--size-field,.25rem)*12)}}}}@font-face{font-family:Geist Mono;src:url(/fonts/GeistMono-Variable.woff2)format("woff2-variations");font-weight:100 900;font-display:block}[data-theme=dark]{--color-primary:oklch(55% .15 230);--color-primary-content:oklch(100% 0 0)}:root{--hud-tesla-blue:#5fbef0;--hud-surface-deep:#0a0e15}.hud-mono{letter-spacing:.08em;font-family:Geist Mono,monospace}.hud-mono-tight{letter-spacing:.02em;font-family:Geist Mono,monospace}.hud-sidebar-active{color:var(--hud-tesla-blue);background:#5fbef014;position:relative;box-shadow:inset 0 0 24px #5fbef00f}.hud-sidebar-active:before{content:"";background:var(--hud-tesla-blue);width:2px;position:absolute;top:0;bottom:0;left:0;box-shadow:0 0 8px #5fbef099}.hud-panel-surface{background:var(--hud-surface-deep,#0a0e15)}.hud-tab-active{box-shadow:inset 0 -2px 0 var(--hud-tesla-blue);position:relative;color:var(--hud-tesla-blue)!important;background:0 0!important;font-weight:600!important}.hud-tab-active:after{content:"";background:var(--hud-tesla-blue);height:2px;position:absolute;bottom:-2px;left:0;right:0;box-shadow:0 0 8px #5fbef099}.hud-section-title{letter-spacing:.18em;text-transform:uppercase;color:var(--hud-tesla-blue);opacity:.85;font-family:Geist Mono,monospace;font-size:.75rem;font-weight:600}.hud-panel-surface ::-webkit-scrollbar{width:6px;height:6px}.hud-panel-surface ::-webkit-scrollbar-track{background:0 0}.hud-panel-surface ::-webkit-scrollbar-thumb{background:#5fbef040;border-radius:3px}.hud-panel-surface ::-webkit-scrollbar-thumb:hover{background:#5fbef073}html,body{height:100%;margin:0;padding:0;overflow:hidden}@media (pointer:coarse){html{font-size:18px}}@media (pointer:coarse) and (max-height:500px){html{font-size:20px}}.hud-page{background:var(--hud-surface-deep,#0a0e15);position:fixed;inset:0}.tesla-boot-overlay{z-index:50;pointer-events:none;background:var(--hud-surface-deep,#0a0e15);flex-direction:column;justify-content:center;align-items:center;animation:.9s cubic-bezier(.22,.61,.36,1) forwards tesla-boot-fade;display:flex;position:absolute;inset:0}.tesla-boot-logo{mix-blend-mode:screen;filter:drop-shadow(0 0 18px #5fbef059);width:clamp(180px,28vw,360px);height:auto}.tesla-boot-subtitle{color:#ffffff73;letter-spacing:.5em;text-transform:uppercase;margin-top:1.2em;padding-left:.5em;font-family:Geist Mono,monospace;font-size:clamp(.7rem,1.4vw,1rem);font-weight:300}@keyframes tesla-boot-fade{0%{opacity:0;transform:scale(.94)}28%{opacity:1;transform:scale(1)}66%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.06)}}#hud-page-content.hud-mode{transform:scaleY(-1)}.dashboard{z-index:20;pointer-events:none;color:#0f8;text-shadow:0 0 10px #00ff8880;flex-direction:column;font-family:Courier New,monospace;display:flex;position:absolute;inset:0}.dashboard.hidden{display:none}.dash-top{justify-content:space-between;align-items:center;padding:4px 16px;display:flex}.dash-top-center{flex:1;justify-content:center;display:flex}.blinker{opacity:.2;font-size:36px;transition:opacity .15s}.blinker.active{opacity:1;color:#0f8;animation:.5s infinite blink}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.compass{background:#0009;border:2px solid #0f86;border-radius:6px;flex-direction:column;align-items:center;padding:4px 16px;display:flex}.compass-heading{font-size:24px;font-weight:700}.compass-degrees{opacity:.7;font-size:12px}.dash-main{flex:1;justify-content:space-between;align-items:flex-start;margin-top:-16px;padding:0 16px;display:flex}.dash-left{flex-direction:column;gap:4px;display:flex}.gear-indicator{background:#0009;border:2px solid #0f86;border-radius:6px;flex-direction:column;width:60px;padding:4px;display:flex}.gear{text-align:center;opacity:.3;padding:1px 10px;font-size:16px}.gear.active{opacity:1;background:#0f83;border-radius:4px}.gear.reverse{color:#f44;text-shadow:0 0 10px #ff444480}.cruise-box{text-align:center;background:#0009;border:2px solid #0f86;border-radius:6px;min-width:60px;padding:4px 10px}.cruise-label{opacity:.7;margin-bottom:1px;font-size:9px}.cruise-speed{font-size:22px;font-weight:700}.cruise-status{opacity:.7;font-size:9px}.cruise-status.set{color:#0f8;opacity:1}.dash-center{flex-direction:column;align-items:center;display:flex}.engage-status{text-transform:uppercase;letter-spacing:1px;border-radius:16px;margin-bottom:4px;padding:4px 16px;font-size:12px}.engage-status.disengaged{color:#999;text-shadow:none;background:#64646499;border:2px solid #96969699}.engage-status.engaged{color:#0f8;background:#0f83;border:2px solid #0f8c;animation:2s infinite pulse-glow}.engage-status.override{color:#ffc800;text-shadow:0 0 10px #ffc80080;background:#ffc80033;border:2px solid #ffc800cc}@keyframes pulse-glow{0%,to{box-shadow:0 0 10px #00ff884d}50%{box-shadow:0 0 20px #0f89}}.speed-display{text-align:center;background:#0009;border:3px solid #64646499;border-radius:12px;padding:8px 24px;transition:border-color .3s,box-shadow .3s}.speed-display.engaged{border-color:#0f8c;box-shadow:0 0 20px #0f86}.speed-display.override{border-color:#ffc800cc;box-shadow:0 0 20px #ffc80066}.current-speed{text-shadow:0 0 30px #00ff8880;font-size:72px;font-weight:700;line-height:1}.speed-unit{opacity:.7;font-size:16px}.dash-right{flex-direction:column;align-items:flex-end;gap:4px;display:flex}.lead-car-box{text-align:center;background:#0009;border:2px solid #0f86;border-radius:6px;width:70px;padding:4px 10px}.lead-label{opacity:.7;margin-bottom:1px;font-size:9px}.lead-distance{font-size:18px;font-weight:700}.lead-speed-rel{opacity:.7;font-size:12px}.lead-speed-rel.closing{color:#f44;text-shadow:0 0 10px #ff444480}.lead-speed-rel.opening{color:#4f4}.gforce-container{text-align:center;background:#0009;border:2px solid #0f86;border-radius:6px;width:70px;padding:4px}.gforce-label{opacity:.7;margin-bottom:2px;font-size:9px}.gforce-display{width:56px;height:56px;margin:0 auto;position:relative}.gforce-grid{background:radial-gradient(circle,#0006 0%,#000c 100%);border:1px solid #00ff884d;border-radius:50%;width:100%;height:100%;position:relative}.gforce-crosshair-h,.gforce-crosshair-v{background:#0f83;position:absolute}.gforce-crosshair-h{width:100%;height:1px;top:50%;left:0}.gforce-crosshair-v{width:1px;height:100%;top:0;left:50%}.gforce-dot{background:#0f8;border-radius:50%;width:10px;height:10px;transition:top .1s,left .1s;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);box-shadow:0 0 8px #0f8c}.gforce-values{opacity:.7;justify-content:space-between;margin-top:4px;font-size:10px;display:flex}.dash-bottom{justify-content:space-between;align-items:center;padding:8px 16px 12px;display:flex}.stats-row{gap:12px;display:flex}.stat-item{background:#0009;border:1px solid #00ff884d;border-radius:4px;flex-direction:column;align-items:center;padding:4px 10px;display:flex}.stat-label{opacity:.6;font-size:9px}.stat-value{font-size:14px;font-weight:700}.warnings-row{gap:8px;display:flex}.warning-icon{background:#f443;border:2px solid #f449;border-radius:6px;padding:4px;font-size:18px;animation:1s infinite warning-pulse}.warning-icon.hidden{display:none}@keyframes warning-pulse{0%,to{opacity:1}50%{opacity:.5}}.animate-fade-in{animation:.2s ease-out forwards fade-in}.animate-fade-out{animation:.2s ease-out forwards fade-out}.animate-slide-up{animation:.3s ease-out forwards slide-up}.animate-slide-down{animation:.2s ease-out forwards slide-down}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes fade-out{0%{opacity:1}to{opacity:0}}@keyframes slide-up{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes slide-down{0%{transform:translateY(0)}to{transform:translateY(100%)}}@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-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@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-font-weight{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-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false} \ No newline at end of file +@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-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking: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-duration:initial;--tw-ease: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;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--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);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--radius-lg:.5rem;--radius-2xl:1rem;--ease-out:cubic-bezier(0,0,.2,1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--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{background:var(--page-scroll-bg,var(--root-bg));--page-scroll-bg-on:linear-gradient(var(--root-bg,#0000),var(--root-bg,#0000))var(--root-bg,#0000)}@supports (color:color-mix(in lab, red, red)){:root{--page-scroll-bg-on:linear-gradient(var(--root-bg,#0000),var(--root-bg,#0000))color-mix(in srgb,var(--root-bg,#0000),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)}@keyframes set-page-has-scroll{0%,to{--page-has-scroll:1}}:root,[data-theme]{background:var(--page-scroll-bg,var(--root-bg));color:var(--color-base-content)}:where(:root,[data-theme]){--root-bg:var(--color-base-100)}}@layer components;@layer utilities{@layer daisyui.l1.l2.l3{.diff{webkit-user-select:none;-webkit-user-select:none;user-select:none;direction:ltr;grid-template-rows:1fr 1.8rem 1fr;grid-template-columns:auto 1fr;width:100%;display:grid;position:relative;overflow:hidden;container-type:inline-size}.diff:focus-visible,.diff:has(.diff-item-1:focus-visible),.diff:focus-visible{outline-style:var(--tw-outline-style);outline-offset:1px;outline-width:2px;outline-color:var(--color-base-content)}.diff:focus-visible .diff-resizer{min-width:95cqi;max-width:95cqi}.diff:has(.diff-item-1:focus-visible){outline-style:var(--tw-outline-style);outline-offset:1px;outline-width:2px}.diff:has(.diff-item-1:focus-visible) .diff-resizer{min-width:5cqi;max-width:5cqi}@supports (-webkit-overflow-scrolling:touch) and (overflow:-webkit-paged-x){.diff:focus .diff-resizer{min-width:5cqi;max-width:5cqi}.diff:has(.diff-item-1:focus) .diff-resizer{min-width:95cqi;max-width:95cqi}}.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(69% - var(--border) + .25px),var(--tab-border-color)69%,var(--tab-bg)calc(69% + .25px);order:var(--tab-order);height:var(--tab-height);padding-inline:var(--tab-p);border-color:#0000;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}: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,min-height .2s ease-out allow-discrete,padding .1s ease-out 20ms,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);width:100%;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}@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)*-100cqw) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100cqw*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)*-100cqw) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100cqw*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)*-100cqw) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100cqw*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)*-100cqw) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100cqw*var(--range-fill))}}.range:disabled{cursor:not-allowed;opacity:.3}.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}.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}.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%}.hero{background-position:50%;background-size:cover;place-items:center;width:100%;display:grid}.hero>*{grid-row-start:1;grid-column-start:1}.modal-box{background-color:var(--color-base-100);border-top-left-radius:var(--modal-tl,var(--radius-box));border-top-right-radius:var(--modal-tr,var(--radius-box));border-bottom-left-radius:var(--modal-bl,var(--radius-box));border-bottom-right-radius:var(--modal-br,var(--radius-box));opacity:0;overscroll-behavior:contain;grid-row-start:1;grid-column-start:1;width:91.6667%;max-width:32rem;max-height:100vh;padding:1.5rem;transition:translate .3s ease-out,scale .3s ease-out,opacity .2s ease-out 50ms,box-shadow .3s ease-out;overflow-y:auto;scale:95%;box-shadow:0 25px 50px -12px oklch(0% 0 0/.25)}.divider{white-space:nowrap;height:1rem;margin:var(--divider-m,1rem 0);--divider-color:var(--color-base-content);flex-direction:row;align-self:stretch;align-items:center;display:flex}@supports (color:color-mix(in lab, red, red)){.divider{--divider-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.divider:before,.divider:after{content:"";background-color:var(--divider-color);flex-grow:1;width:100%;height:.125rem}@media print{.divider:before,.divider:after{border:.5px solid}}.divider:not(:empty){gap:1rem}.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)}}.modal-action{justify-content:flex-end;gap:.5rem;margin-top:1.5rem;display:flex}.breadcrumbs{max-width:100%;padding-block:.5rem;overflow-x:auto}.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol{white-space:nowrap;align-items:center;min-height:min-content;display:flex}:is(.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol)>li{align-items:center;display:flex}:is(.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol)>li>*{cursor:pointer;align-items:center;gap:.5rem;display:flex}@media (hover:hover){:is(.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol)>li>:hover{text-decoration-line:underline}}:is(.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol)>li>:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){:is(.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol)>li>:focus{outline-offset:2px;outline:2px solid #0000}}:is(.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol)>li>:focus-visible{outline-offset:2px;outline:2px solid}:is(.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol)>li+:before{content:"";opacity:.4;background-color:#0000;border-top:1px solid;border-right:1px solid;width:.375rem;height:.375rem;margin-left:.5rem;margin-right:.75rem;display:block;rotate:45deg}[dir=rtl] :is(:is(.breadcrumbs>menu,.breadcrumbs>ul,.breadcrumbs>ol)>li)+:before{rotate:-135deg}.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));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);width:fit-content;height:var(--size);padding-inline:calc(var(--size)/2 - var(--border));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}.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)}.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-error{--btn-color:var(--color-error);--btn-fg:var(--color-error-content)}.btn-primary{--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content)}.btn-success{--btn-color:var(--color-success);--btn-fg:var(--color-success-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)}}.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>*{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{cursor:not-allowed;opacity:.2}: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}.btn-circle{width:var(--size);height:var(--size);border-radius:3.40282e38px;padding-inline:0}.btn-square{width:var(--size);height:var(--size);padding-inline:0}.loading-lg{width:calc(var(--size-selector,.25rem)*7)}.loading-md{width:calc(var(--size-selector,.25rem)*6)}.loading-sm{width:calc(var(--size-selector,.25rem)*5)}.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")}.link-hover{text-decoration-line:none}@media (hover:hover){.link-hover:hover{text-decoration-line:underline}}.btn-md{--fontsize:.875rem;--btn-p:1rem;--size:calc(var(--size-field,.25rem)*10)}.btn-sm{--fontsize:.75rem;--btn-p:.75rem;--size:calc(var(--size-field,.25rem)*8)}.btn-xs{--fontsize:.6875rem;--btn-p:.5rem;--size:calc(var(--size-field,.25rem)*6)}.toggle-lg[type=checkbox],.toggle-lg:has([type=checkbox]){--size:calc(var(--size-selector,.25rem)*7)}.toggle-primary:checked,.toggle-primary[aria-checked=true]{--input-color:var(--color-primary)}.toggle-xs[type=checkbox],.toggle-xs:has([type=checkbox]){--size:calc(var(--size-selector,.25rem)*4)}}.prose :where(a.btn:not(.btn-link)):not(:where([class~=not-prose],[class~=not-prose] *)){text-decoration-line:none}.pointer-events-none{pointer-events:none}.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}.inset-0{inset:calc(var(--spacing)*0)}.right-0{right:calc(var(--spacing)*0)}.bottom-0{bottom:calc(var(--spacing)*0)}.left-0{left:calc(var(--spacing)*0)}.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)}.join>:where(:focus,:has(:focus)){z-index:1}@media (hover:hover){.join>:where(.btn:hover,:has(.btn:hover)){isolation:isolate}}.isolate{isolation:isolate}.z-10{z-index:10}.z-\[300\]{z-index:300}.z-\[400\]{z-index:400}.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-3{margin-inline:calc(var(--spacing)*3)}.mx-auto{margin-inline:auto}.my-1{margin-block:calc(var(--spacing)*1)}.join-item:where(:not(:first-child,:disabled,[disabled],.btn-disabled)){margin-block-start:0;margin-inline-start:calc(var(--border,1px)*-1)}.join-item:where(:is(:disabled,[disabled],.btn-disabled)){border-width:var(--border,1px)0 var(--border,1px)var(--border,1px)}.-mt-2{margin-top:calc(var(--spacing)*-2)}.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-4{margin-top:calc(var(--spacing)*4)}.mr-2{margin-right:calc(var(--spacing)*2)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-4{margin-left:calc(var(--spacing)*4)}.alert{border-width:var(--border);border-color:var(--alert-border-color,var(--color-base-200))}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.table{display:table}.h-1{height:calc(var(--spacing)*1)}.h-24{height:calc(var(--spacing)*24)}.h-auto{height:auto}.h-full{height:100%}.max-h-40{max-height:calc(var(--spacing)*40)}.max-h-\[80vh\]{max-height:80vh}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-12{min-height:calc(var(--spacing)*12)}.w-10{width:calc(var(--spacing)*10)}.w-12{width:calc(var(--spacing)*12)}.w-14{width:calc(var(--spacing)*14)}.w-24{width:calc(var(--spacing)*24)}.w-48{width:calc(var(--spacing)*48)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-24{max-width:calc(var(--spacing)*24)}.max-w-full{max-width:100%}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-12{min-width:calc(var(--spacing)*12)}.min-w-20{min-width:calc(var(--spacing)*20)}.min-w-24{min-width:calc(var(--spacing)*24)}.min-w-28{min-width:calc(var(--spacing)*28)}.min-w-32{min-width:calc(var(--spacing)*32)}.min-w-36{min-width:calc(var(--spacing)*36)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.resize{resize:both}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}: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-content\/10>:not(:last-child)){border-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:where(.divide-base-content\/10>:not(:last-child)){border-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-t-2xl{border-top-left-radius:var(--radius-2xl);border-top-right-radius:var(--radius-2xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-none{--tw-border-style:none;border-style:none}.border-base-content\/10{border-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.border-base-content\/10{border-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.border-base-content\/15{border-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.border-base-content\/15{border-color:color-mix(in oklab,var(--color-base-content)15%,transparent)}}.bg-base-100\/50{background-color:var(--color-base-100)}@supports (color:color-mix(in lab, red, red)){.bg-base-100\/50{background-color:color-mix(in oklab,var(--color-base-100)50%,transparent)}}.bg-base-200,.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-300,.bg-base-300\/30{background-color:var(--color-base-300)}@supports (color:color-mix(in lab, red, red)){.bg-base-300\/30{background-color:color-mix(in oklab,var(--color-base-300)30%,transparent)}}.bg-base-300\/50{background-color:var(--color-base-300)}@supports (color:color-mix(in lab, red, red)){.bg-base-300\/50{background-color:color-mix(in oklab,var(--color-base-300)50%,transparent)}}.bg-base-300\/90{background-color:var(--color-base-300)}@supports (color:color-mix(in lab, red, red)){.bg-base-300\/90{background-color:color-mix(in oklab,var(--color-base-300)90%,transparent)}}.bg-base-content\/20{background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.bg-base-content\/20{background-color:color-mix(in oklab,var(--color-base-content)20%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab, red, red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black)60%,transparent)}}.bg-black\/95{background-color:#000000f2}@supports (color:color-mix(in lab, red, red)){.bg-black\/95{background-color:color-mix(in oklab,var(--color-black)95%,transparent)}}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-8{padding:calc(var(--spacing)*8)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-8{padding-block:calc(var(--spacing)*8)}.pt-3{padding-top:calc(var(--spacing)*3)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.pb-3{padding-bottom:calc(var(--spacing)*3)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--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-\[10px\]{font-size:10px}.leading-none{--tw-leading:1;line-height:1}.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-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-all{word-break:break-all}.whitespace-pre-wrap{white-space:pre-wrap}.text-base-content,.text-base-content\/50{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/50{color:color-mix(in oklab,var(--color-base-content)50%,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\/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-base-content\/90{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/90{color:color-mix(in oklab,var(--color-base-content)90%,transparent)}}.text-error{color:var(--color-error)}.text-primary,.text-primary\/70{color:var(--color-primary)}@supports (color:color-mix(in lab, red, red)){.text-primary\/70{color:color-mix(in oklab,var(--color-primary)70%,transparent)}}.text-success{color:var(--color-success)}.text-warning{color:var(--color-warning)}.text-white\/70{color:#ffffffb3}@supports (color:color-mix(in lab, red, red)){.text-white\/70{color:color-mix(in oklab,var(--color-white)70%,transparent)}}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.shadow{--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)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px 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)}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);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}}.btn-outline:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn),:disabled,[disabled],.btn-disabled){--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color);--btn-border:var(--btn-color);--btn-noise:none}@media (hover:none){.btn-outline:not(.btn-active,:active,:focus-visible,input:checked:not(.filter .btn)):hover{--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color);--btn-border:var(--btn-color);--btn-noise:none}}}.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,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.placeholder\:text-base-content\/50::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.placeholder\:text-base-content\/50::placeholder{color:color-mix(in oklab,var(--color-base-content)50%,transparent)}}.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\:border-primary\/30:hover{border-color:var(--color-primary)}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/30:hover{border-color:color-mix(in oklab,var(--color-primary)30%,transparent)}}.hover\:bg-base-100:hover{background-color:var(--color-base-100)}.hover\:bg-base-200:hover{background-color:var(--color-base-200)}.hover\:bg-base-300\/50:hover{background-color:var(--color-base-300)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-base-300\/50:hover{background-color:color-mix(in oklab,var(--color-base-300)50%,transparent)}}.hover\:bg-error\/10:hover{background-color:var(--color-error)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-error\/10:hover{background-color:color-mix(in oklab,var(--color-error)10%,transparent)}}}.focus\:ring-0:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:bg-base-100\/50:active{background-color:var(--color-base-100)}@supports (color:color-mix(in lab, red, red)){.active\:bg-base-100\/50:active{background-color:color-mix(in oklab,var(--color-base-100)50%,transparent)}}.active\:bg-base-300:active{background-color:var(--color-base-300)}@media (min-width:40rem){.sm\:px-10{padding-inline:calc(var(--spacing)*10)}}@media (min-width:48rem){@layer daisyui.l1.l2{.md\:btn-lg{--fontsize:1.125rem;--btn-p:1.25rem;--size:calc(var(--size-field,.25rem)*12)}}}@media (min-width:64rem){.lg\:px-20{padding-inline:calc(var(--spacing)*20)}}}@font-face{font-family:Geist Mono;src:url(/fonts/GeistMono-Variable.woff2)format("woff2-variations");font-weight:100 900;font-display:block}[data-theme=dark]{--color-primary:oklch(55% .15 230);--color-primary-content:oklch(100% 0 0)}:root{--hud-tesla-blue:#5fbef0;--hud-surface-deep:#0a0e15}.hud-mono{letter-spacing:.08em;font-family:Geist Mono,monospace}.hud-mono-tight{letter-spacing:.02em;font-family:Geist Mono,monospace}.hud-sidebar-active{color:var(--hud-tesla-blue);background:#5fbef014;position:relative;box-shadow:inset 0 0 24px #5fbef00f}.hud-sidebar-active:before{content:"";background:var(--hud-tesla-blue);width:2px;position:absolute;top:0;bottom:0;left:0;box-shadow:0 0 8px #5fbef099}.hud-panel-surface{background:var(--hud-surface-deep,#0a0e15)}.hud-tab-active{box-shadow:inset 0 -2px 0 var(--hud-tesla-blue);position:relative;color:var(--hud-tesla-blue)!important;background:0 0!important;font-weight:600!important}.hud-tab-active:after{content:"";background:var(--hud-tesla-blue);height:2px;position:absolute;bottom:-2px;left:0;right:0;box-shadow:0 0 8px #5fbef099}.hud-section-title{letter-spacing:.18em;text-transform:uppercase;color:var(--hud-tesla-blue);opacity:.85;font-family:Geist Mono,monospace;font-size:.75rem;font-weight:600}.hud-panel-surface ::-webkit-scrollbar{width:6px;height:6px}.hud-panel-surface ::-webkit-scrollbar-track{background:0 0}.hud-panel-surface ::-webkit-scrollbar-thumb{background:#5fbef040;border-radius:3px}.hud-panel-surface ::-webkit-scrollbar-thumb:hover{background:#5fbef073}html,body{height:100%;margin:0;padding:0;overflow:hidden}@media (pointer:coarse){html{font-size:18px}}@media (pointer:coarse) and (max-height:500px){html{font-size:20px}}.hud-page{background:var(--hud-surface-deep,#0a0e15);position:fixed;inset:0}.tesla-boot-overlay{z-index:50;pointer-events:none;background:var(--hud-surface-deep,#0a0e15);flex-direction:column;justify-content:center;align-items:center;animation:.9s cubic-bezier(.22,.61,.36,1) forwards tesla-boot-fade;display:flex;position:absolute;inset:0}.tesla-boot-logo{mix-blend-mode:screen;filter:drop-shadow(0 0 18px #5fbef059);width:clamp(180px,28vw,360px);height:auto}.tesla-boot-subtitle{color:#ffffff73;letter-spacing:.5em;text-transform:uppercase;margin-top:1.2em;padding-left:.5em;font-family:Geist Mono,monospace;font-size:clamp(.7rem,1.4vw,1rem);font-weight:300}@keyframes tesla-boot-fade{0%{opacity:0;transform:scale(.94)}28%{opacity:1;transform:scale(1)}66%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.06)}}#hud-page-content.hud-mode{transform:scaleY(-1)}.dashboard{z-index:20;pointer-events:none;color:#0f8;text-shadow:0 0 10px #00ff8880;flex-direction:column;font-family:Courier New,monospace;display:flex;position:absolute;inset:0}.dashboard.hidden{display:none}.dash-top{justify-content:space-between;align-items:center;padding:4px 16px;display:flex}.dash-top-center{flex:1;justify-content:center;display:flex}.blinker{opacity:.2;font-size:36px;transition:opacity .15s}.blinker.active{opacity:1;color:#0f8;animation:.5s infinite blink}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.compass{background:#0009;border:2px solid #0f86;border-radius:6px;flex-direction:column;align-items:center;padding:4px 16px;display:flex}.compass-heading{font-size:24px;font-weight:700}.compass-degrees{opacity:.7;font-size:12px}.dash-main{flex:1;justify-content:space-between;align-items:flex-start;margin-top:-16px;padding:0 16px;display:flex}.dash-left{flex-direction:column;gap:4px;display:flex}.gear-indicator{background:#0009;border:2px solid #0f86;border-radius:6px;flex-direction:column;width:60px;padding:4px;display:flex}.gear{text-align:center;opacity:.3;padding:1px 10px;font-size:16px}.gear.active{opacity:1;background:#0f83;border-radius:4px}.gear.reverse{color:#f44;text-shadow:0 0 10px #ff444480}.cruise-box{text-align:center;background:#0009;border:2px solid #0f86;border-radius:6px;min-width:60px;padding:4px 10px}.cruise-label{opacity:.7;margin-bottom:1px;font-size:9px}.cruise-speed{font-size:22px;font-weight:700}.cruise-status{opacity:.7;font-size:9px}.cruise-status.set{color:#0f8;opacity:1}.dash-center{flex-direction:column;align-items:center;display:flex}.engage-status{text-transform:uppercase;letter-spacing:1px;border-radius:16px;margin-bottom:4px;padding:4px 16px;font-size:12px}.engage-status.disengaged{color:#999;text-shadow:none;background:#64646499;border:2px solid #96969699}.engage-status.engaged{color:#0f8;background:#0f83;border:2px solid #0f8c;animation:2s infinite pulse-glow}.engage-status.override{color:#ffc800;text-shadow:0 0 10px #ffc80080;background:#ffc80033;border:2px solid #ffc800cc}@keyframes pulse-glow{0%,to{box-shadow:0 0 10px #00ff884d}50%{box-shadow:0 0 20px #0f89}}.speed-display{text-align:center;background:#0009;border:3px solid #64646499;border-radius:12px;padding:8px 24px;transition:border-color .3s,box-shadow .3s}.speed-display.engaged{border-color:#0f8c;box-shadow:0 0 20px #0f86}.speed-display.override{border-color:#ffc800cc;box-shadow:0 0 20px #ffc80066}.current-speed{text-shadow:0 0 30px #00ff8880;font-size:72px;font-weight:700;line-height:1}.speed-unit{opacity:.7;font-size:16px}.dash-right{flex-direction:column;align-items:flex-end;gap:4px;display:flex}.lead-car-box{text-align:center;background:#0009;border:2px solid #0f86;border-radius:6px;width:70px;padding:4px 10px}.lead-label{opacity:.7;margin-bottom:1px;font-size:9px}.lead-distance{font-size:18px;font-weight:700}.lead-speed-rel{opacity:.7;font-size:12px}.lead-speed-rel.closing{color:#f44;text-shadow:0 0 10px #ff444480}.lead-speed-rel.opening{color:#4f4}.gforce-container{text-align:center;background:#0009;border:2px solid #0f86;border-radius:6px;width:70px;padding:4px}.gforce-label{opacity:.7;margin-bottom:2px;font-size:9px}.gforce-display{width:56px;height:56px;margin:0 auto;position:relative}.gforce-grid{background:radial-gradient(circle,#0006 0%,#000c 100%);border:1px solid #00ff884d;border-radius:50%;width:100%;height:100%;position:relative}.gforce-crosshair-h,.gforce-crosshair-v{background:#0f83;position:absolute}.gforce-crosshair-h{width:100%;height:1px;top:50%;left:0}.gforce-crosshair-v{width:1px;height:100%;top:0;left:50%}.gforce-dot{background:#0f8;border-radius:50%;width:10px;height:10px;transition:top .1s,left .1s;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);box-shadow:0 0 8px #0f8c}.gforce-values{opacity:.7;justify-content:space-between;margin-top:4px;font-size:10px;display:flex}.dash-bottom{justify-content:space-between;align-items:center;padding:8px 16px 12px;display:flex}.stats-row{gap:12px;display:flex}.stat-item{background:#0009;border:1px solid #00ff884d;border-radius:4px;flex-direction:column;align-items:center;padding:4px 10px;display:flex}.stat-label{opacity:.6;font-size:9px}.stat-value{font-size:14px;font-weight:700}.warnings-row{gap:8px;display:flex}.warning-icon{background:#f443;border:2px solid #f449;border-radius:6px;padding:4px;font-size:18px;animation:1s infinite warning-pulse}.warning-icon.hidden{display:none}@keyframes warning-pulse{0%,to{opacity:1}50%{opacity:.5}}.animate-fade-in{animation:.2s ease-out forwards fade-in}.animate-fade-out{animation:.2s ease-out forwards fade-out}.animate-slide-up{animation:.3s ease-out forwards slide-up}.animate-slide-down{animation:.2s ease-out forwards slide-down}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes fade-out{0%{opacity:1}to{opacity:0}}@keyframes slide-up{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes slide-down{0%{transform:translateY(0)}to{transform:translateY(100%)}}@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-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@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-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-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false} \ No newline at end of file diff --git a/dragonpilot/dashy/web/dist/js/app.js b/dragonpilot/dashy/web/dist/js/app.js index 2d005e794..5114cefd5 100644 --- a/dragonpilot/dashy/web/dist/js/app.js +++ b/dragonpilot/dashy/web/dist/js/app.js @@ -20,7 +20,7 @@ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -(function(){"use strict";const Re=new URLSearchParams(window.location.search),ge=Re.get("debug")==="true",De=localStorage.getItem("debug")==="true";if(window.debug=ge||De||!1,ge!==De&&(ge?localStorage.setItem("debug","true"):Re.has("debug")&&localStorage.removeItem("debug")),window._originalConsoleLog=console.log,console.log=function(...e){window.debug&&window._originalConsoleLog(...e)},window.debug){window._originalConsoleLog("%c Debug Mode Enabled","color: #00ff00; font-weight: bold; font-size: 16px"),window._originalConsoleLog("To disable: add ?debug=false to URL or run dashy.debug(false)");const e=document.createElement("div");e.id="debug-indicator",e.textContent="DEBUG",e.style.cssText=` +(function(){"use strict";window.__I18N__=window.__I18N__||{};function S(e){return window.__I18N__&&window.__I18N__[e]||e}function pn(e){return e}function rt(e){e&&typeof e=="object"&&(window.__I18N__=e)}window.tr=S,window.tr_noop=pn,window.applyI18n=rt;const $e={_sentinel:null,_want:!1,async _acquire(){if(!(this._sentinel||!("wakeLock"in navigator)))try{this._sentinel=await navigator.wakeLock.request("screen"),this._sentinel.addEventListener("release",()=>{this._sentinel=null})}catch{this._sentinel=null}},_release(){this._sentinel&&(this._sentinel.release().catch(()=>{}),this._sentinel=null)},setWanted(e){this._want=!!e,this._apply()},_apply(){this._want&&document.visibilityState==="visible"?this._acquire():this._release()},init(){document.addEventListener("visibilitychange",()=>this._apply()),this._want=!0,this._apply()}};window.WakeLock=$e;const kt=new URLSearchParams(window.location.search),lt=kt.get("debug")==="true",At=localStorage.getItem("debug")==="true";if(window.debug=lt||At||!1,lt!==At&&(lt?localStorage.setItem("debug","true"):kt.has("debug")&&localStorage.removeItem("debug")),window._originalConsoleLog=console.log,console.log=function(...e){window.debug&&window._originalConsoleLog(...e)},window.debug){window._originalConsoleLog("%c Debug Mode Enabled","color: #00ff00; font-weight: bold; font-size: 16px"),window._originalConsoleLog("To disable: add ?debug=false to URL or run dashy.debug(false)");const e=document.createElement("div");e.id="debug-indicator",e.textContent="DEBUG",e.style.cssText=` position: fixed; top: 10px; right: 10px; @@ -32,13 +32,13 @@ font-size: 12px; z-index: 10000; pointer-events: none; - `,document.addEventListener("DOMContentLoaded",()=>{document.body.appendChild(e)})}window.dashy={debug:function(e=!0){if(window.debug=e,e)localStorage.setItem("debug","true"),window._originalConsoleLog("%c Debug Mode Enabled","color: #00ff00; font-weight: bold"),window._originalConsoleLog("Refresh page to see all debug logs");else{localStorage.removeItem("debug"),window._originalConsoleLog("%c Debug Mode Disabled","color: #ff0000; font-weight: bold");const t=document.getElementById("debug-indicator");t&&t.remove()}return e?"Debug enabled":"Debug disabled"},perf:function(){const e={fps:window._fps||0,frameTime:window._frameTime||0,drawCalls:window._drawCalls||0,canvasOps:window._canvasOps||0};return window._originalConsoleLog("%cPerformance Metrics","color: #00aaff; font-weight: bold"),window._originalConsoleLog(e),e},help:function(){window._originalConsoleLog("%cDashy Debug Commands:","color: #ffaa00; font-weight: bold; font-size: 14px"),window._originalConsoleLog("dashy.debug(true/false) - Enable/disable debug mode"),window._originalConsoleLog("dashy.perf() - Show performance metrics"),window._originalConsoleLog("dashy.help() - Show this help"),window._originalConsoleLog(` -URL Parameters:`),window._originalConsoleLog("?debug=true - Enable debug mode"),window._originalConsoleLog("?debug=false - Disable debug mode")}},(function(){let t=0,n=null,a=0;function i(s,o,r){if(!I.get("ui.hudEnabled")||window.PanelOverlay&&T._isOpen)return;const d=window.innerWidth,c=window.innerHeight;if(s>d-100&&o>c-100){const l=Date.now();if(l-a<100)return;if(a=l,t++,n&&clearTimeout(n),n=setTimeout(()=>{t=0},1e3),t===5){t=0;const p=!window.debug;window.dashy.debug(p);const h=document.createElement("div");h.textContent=p?"Debug Enabled":"Debug Disabled",h.style.cssText=` + `,document.addEventListener("DOMContentLoaded",()=>{document.body.appendChild(e)})}window.dashy={debug:function(e=!0){if(window.debug=e,e)localStorage.setItem("debug","true"),window._originalConsoleLog("%c Debug Mode Enabled","color: #00ff00; font-weight: bold"),window._originalConsoleLog("Refresh page to see all debug logs");else{localStorage.removeItem("debug"),window._originalConsoleLog("%c Debug Mode Disabled","color: #ff0000; font-weight: bold");const i=document.getElementById("debug-indicator");i&&i.remove()}return e?"Debug enabled":"Debug disabled"},perf:function(){const e={fps:window._fps||0,frameTime:window._frameTime||0,drawCalls:window._drawCalls||0,canvasOps:window._canvasOps||0};return window._originalConsoleLog("%cPerformance Metrics","color: #00aaff; font-weight: bold"),window._originalConsoleLog(e),e},help:function(){window._originalConsoleLog("%cDashy Debug Commands:","color: #ffaa00; font-weight: bold; font-size: 14px"),window._originalConsoleLog("dashy.debug(true/false) - Enable/disable debug mode"),window._originalConsoleLog("dashy.perf() - Show performance metrics"),window._originalConsoleLog("dashy.help() - Show this help"),window._originalConsoleLog(` +URL Parameters:`),window._originalConsoleLog("?debug=true - Enable debug mode"),window._originalConsoleLog("?debug=false - Disable debug mode")}},(function(){let i=0,n=null,t=0;function a(s,r,l){if(!ue.get("ui.hudEnabled")||window.PanelOverlay&&Q._isOpen)return;const u=window.innerWidth,h=window.innerHeight;if(s>u-100&&r>h-100){const f=Date.now();if(f-t<100)return;if(t=f,i++,n&&clearTimeout(n),n=setTimeout(()=>{i=0},1e3),i===5){i=0;const m=!window.debug;window.dashy.debug(m);const g=document.createElement("div");g.textContent=m?"Debug Enabled":"Debug Disabled",g.style.cssText=` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); - background: ${p?"rgba(0, 255, 0, 0.9)":"rgba(255, 0, 0, 0.9)"}; + background: ${m?"rgba(0, 255, 0, 0.9)":"rgba(255, 0, 0, 0.9)"}; color: white; padding: 20px 40px; border-radius: 10px; @@ -46,16 +46,19 @@ URL Parameters:`),window._originalConsoleLog("?debug=true - Enable debug mode"), font-weight: bold; z-index: 10001; pointer-events: none; - `,document.body.appendChild(h),navigator.vibrate&&navigator.vibrate(p?[100,50,100]:[200]),setTimeout(()=>{h.remove(),location.reload()},1500)}}}document.addEventListener("touchend",s=>{const o=s.changedTouches[0];o.clientX>window.innerWidth-100&&o.clientY>window.innerHeight-100&&i(o.clientX,o.clientY,"Touch")}),document.addEventListener("click",s=>{s.clientX>window.innerWidth-100&&s.clientY>window.innerHeight-100&&i(s.clientX,s.clientY,"Click")})})();const ye={events:{},on(e,t){return this.events[e]||(this.events[e]=[]),this.events[e].push(t),()=>this.off(e,t)},off(e,t){this.events[e]&&(this.events[e]=this.events[e].filter(n=>n!==t))},emit(e,t){this.events[e]&&this.events[e].forEach(n=>n(t))}},I={_data:{settings:{},localSettings:{},ui:{hudEnabled:!0,hudStyle:"tesla",hudModeEnabled:!1,rainbowEnabled:!1,leadStatsEnabled:!0},files:{currentPath:"/",items:[]}},get(e){return e.split(".").reduce((n,a)=>n?.[a],this._data)},set(e,t){const n=e.split("."),a=n.pop(),i=n.reduce((o,r)=>(o[r]||(o[r]={}),o[r]),this._data),s=i[a];s!==t&&(i[a]=t,ye.emit("state:change",{path:e,value:t,oldValue:s}),ye.emit(`state:${e}`,t))},subscribe(e,t){return ye.on(`state:${e}`,t)}};window.state=I;function u(e,t,n={}){const a=document.createElement(e);return t&&(a.className=t),Object.entries(n).forEach(([i,s])=>{i==="dataset"?Object.entries(s).forEach(([o,r])=>{a.dataset[o]=r}):a[i]=s}),a}const j=new Set,ee=new Set;function R(e,t){const n=setTimeout(()=>{e(),j.delete(n)},t);return j.add(n),n}function He(e,t){const n=setInterval(e,t);return ee.add(n),n}function Ae(e){clearTimeout(e),j.delete(e)}function te(e){clearInterval(e),ee.delete(e)}function on(){j.forEach(e=>clearTimeout(e)),j.clear(),ee.forEach(e=>clearInterval(e)),ee.clear()}function Me(e){const{title:t="Confirm",message:n="",confirmText:a="Confirm",cancelText:i="Cancel",confirmStyle:s="danger",onConfirm:o=null,onCancel:r=null}=e,d=u("dialog","modal modal-open");d.style.zIndex="500";const c=u("div","modal-box"),l=u("h3","font-bold text-lg",{textContent:t}),p=u("p","py-4 text-base-content/80",{textContent:n}),h=u("div","modal-action"),f=u("button","btn",{textContent:i}),m=u("button",`btn ${s==="danger"?"btn-error":"btn-primary"}`,{textContent:a}),C=()=>{d.classList.remove("modal-open"),setTimeout(()=>d.remove(),200)};return f.addEventListener("click",()=>{C(),r&&r()}),m.addEventListener("click",async()=>{if(o){m.classList.add("loading"),m.disabled=!0;try{await o()}finally{C()}}else C()}),d.addEventListener("click",E=>{E.target===d&&(C(),r&&r())}),h.appendChild(f),h.appendChild(m),c.appendChild(l),c.appendChild(p),c.appendChild(h),d.appendChild(c),document.body.appendChild(d),{close:C}}const q={BREAKPOINTS:{xs:0,sm:480,md:768,lg:1024,xl:1280},REF_WIDTH:1920,REF_HEIGHT:1080,_width:0,_height:0,_scale:1,_breakpoint:"md",_orientation:"landscape",_pixelRatio:1,_subscribers:[],_resizeTimeout:null,_initialized:!1,_resizeHandler:null,_orientationHandler:null,_fullscreenHandler:null,init(){this._initialized||(this._initialized=!0,this._update(),this._resizeHandler=()=>this._onResize(),this._orientationHandler=()=>setTimeout(()=>this._onResize(),100),this._fullscreenHandler=()=>this._onResize(),window.addEventListener("resize",this._resizeHandler),window.addEventListener("orientationchange",this._orientationHandler),document.addEventListener("fullscreenchange",this._fullscreenHandler),console.log("[ScreenObserver] Initialized:",this.getState()))},subscribe(e){return typeof e=="function"&&(this._subscribers.push(e),e(this.getState())),()=>this.unsubscribe(e)},unsubscribe(e){this._subscribers=this._subscribers.filter(t=>t!==e)},getState(){return{width:this._width,height:this._height,scale:this._scale,breakpoint:this._breakpoint,orientation:this._orientation,pixelRatio:this._pixelRatio,isPortrait:this._orientation==="portrait",isLandscape:this._orientation==="landscape",isMobile:this._breakpoint==="xs"||this._breakpoint==="sm",isTablet:this._breakpoint==="md",isDesktop:this._breakpoint==="lg"||this._breakpoint==="xl"}},_onResize(){this._resizeTimeout&&clearTimeout(this._resizeTimeout),this._resizeTimeout=setTimeout(()=>{this._update(),this._notify()},50)},_update(){this._width=window.innerWidth,this._height=window.innerHeight,this._pixelRatio=window.devicePixelRatio||1,this._scale=this._height/this.REF_HEIGHT,this._orientation=this._width>this._height?"landscape":"portrait",this._breakpoint="xs";for(const[e,t]of Object.entries(this.BREAKPOINTS))this._width>=t&&(this._breakpoint=e)},_notify(){const e=this.getState();this._subscribers.forEach(t=>{try{t(e)}catch(n){console.error("[ScreenObserver] Subscriber error:",n)}})},destroy(){this._subscribers=[],this._resizeTimeout&&(clearTimeout(this._resizeTimeout),this._resizeTimeout=null),this._resizeHandler&&(window.removeEventListener("resize",this._resizeHandler),this._resizeHandler=null),this._orientationHandler&&(window.removeEventListener("orientationchange",this._orientationHandler),this._orientationHandler=null),this._fullscreenHandler&&(document.removeEventListener("fullscreenchange",this._fullscreenHandler),this._fullscreenHandler=null),this._initialized=!1}};q.init(),window.ScreenObserver=q;const y={_serverUp:!1,_streamUp:!1,_streamEverConnected:!1,_dashyOn:!0,_isOffroad:!1,_loadingScreen:null,_loadingStatus:null,_retryTimeout:null,_retryFn:null,init(e){this._loadingScreen=document.getElementById("loading-screen"),this._loadingStatus=document.getElementById("loading-status"),this._retryFn=e},setServerUp(e){this._serverUp=e,this._update()},setStreamUp(e){this._streamUp=e,this._update()},setDashyOn(e){this._dashyOn=e,this._update()},setOffroad(e){this._isOffroad=e,this._update()},isServerUp(){return this._serverUp},isStreamUp(){return this._streamUp},isDashyOn(){return this._dashyOn},isOffroad(){return this._isOffroad},showLoading(e="Connecting to device..."){this._loadingScreen&&(this._loadingScreen.style.display="flex"),this._loadingStatus&&(this._loadingStatus.textContent=e)},hideLoading(){this._loadingScreen&&(this._loadingScreen.style.display="none")},startRetry(e=2e3){clearTimeout(this._retryTimeout),this._retryFn&&(this._retryTimeout=setTimeout(()=>{this._retryFn()},e))},stopRetry(){clearTimeout(this._retryTimeout)},_update(){if(console.log("[ConnectionState] _update: server="+this._serverUp+", stream="+this._streamUp+", dashy="+this._dashyOn+", offroad="+this._isOffroad),!this._serverUp){this.showLoading("Server unavailable. Retrying...");return}if(this.hideLoading(),!this._dashyOn){window.PanelOverlay&&(T.setHudEnabled(!1),T._isOpen||T.show("settings"));return}if(this._isOffroad){window.PanelOverlay&&(T.setConnected(!1),T._isOpen||T.show("settings"));return}window.PanelOverlay&&(T.setConnected(this._streamUp),T.setHudEnabled(!0),!this._streamUp&&this._streamEverConnected&&!T._isOpen&&T.show("settings"))},onServerFailed(){console.log("[ConnectionState] onServerFailed"),this._streamUp=!1,this.setServerUp(!1),this.startRetry()},onServerConnected(){console.log("[ConnectionState] onServerConnected"),this._streamUp=!!(window.dataStream&&O.isConnected());const e=!!(window.PanelOverlay&&T._isOpen);this.setServerUp(!0),this.stopRetry(),e&&T.reload()},onStreamConnected(){console.log("[ConnectionState] onStreamConnected, serverUp="+this._serverUp),this._serverUp&&(this._streamEverConnected=!0,this.setStreamUp(!0))},onStreamDisconnected(){console.log("[ConnectionState] onStreamDisconnected"),this.setStreamUp(!1)}};window.ConnectionState=y;let g=null,ne=null,G=null,M=!1,Be=null,W=0;const ve=1e3,Fe=5e3,$e=2;let B=ve;const ut=/\bNaN\b/g,ht=/-?\bInfinity\b/g;let F={onStatus:null,onData:null,isEnabled:null};const O={init(e){F={...F,...e}},setHost(e){Be=e},connect(){if(M){console.log("[WS] Connection already in progress, skipping");return}const e=Be||window.location.hostname;this.cleanup(),B=ve,M=!0,pt(e).finally(()=>{M=!1})},stop(){console.log("[WS] Stopping connection"),clearTimeout(G),g&&(g.close(),g=null)},cleanup(){clearTimeout(G),te(ne),ne=null,M=!1,g&&(g.onopen=null,g.onmessage=null,g.onerror=null,g.onclose=null,g.close(),g=null)},isConnecting(){return M},getLastDataTime(){return W},isConnected(){return!!(g&&g.readyState===WebSocket.OPEN)}};async function pt(e){P("Connecting...","yellow"),clearTimeout(G),W=Date.now();try{const n=`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/api/ws`;console.log("[WS] Connecting to:",n),g=new WebSocket(n),g.onopen=()=>{console.log("[WS] Connected"),P("Connected","green"),window.ConnectionState&&y.onStreamConnected(),B=ve,W=Date.now(),mt()},g.onmessage=a=>{ft(a.data)},g.onerror=a=>{console.error("[WS] Error:",a)},g.onclose=()=>{console.log("[WS] Closed"),window.ConnectionState&&y.onStreamDisconnected(),P("Disconnected. Reconnecting...","red"),ze()&&(G=R(()=>{M=!1,O.connect(),B=Math.min(B*$e,Fe)},B))}}catch(t){console.error("[WS] Connection error:",t),P("Error. Reconnecting...","red"),ze()&&(G=R(()=>{M=!1,O.connect(),B=Math.min(B*$e,Fe)},B))}}function ft(e){W=Date.now(),window.ConnectionState&&!y.isStreamUp()&&(P("Connected","green"),y.onStreamConnected());try{let t=e;t.includes("NaN")&&(t=t.replace(ut,"null")),t.includes("Infinity")&&(t=t.replace(ht,"null"));const n=JSON.parse(t);if(n.ts!==void 0&&F.onData)for(const a of Object.keys(n))a==="ts"||a==="display"||F.onData(a,n[a])}catch(t){console.warn("[WS] message parse failed:",t.message)}}function mt(){te(ne),W=Date.now(),R(()=>{ne=He(()=>{Date.now()-W>3e3&&(!g||g.readyState!==WebSocket.OPEN?(console.warn("[WS] Data stalled and socket not open, reconnecting..."),P("Data stalled. Reconnecting...","yellow"),M=!1,O.connect()):window.ConnectionState&&y.isStreamUp()&&(console.warn("[WS] Socket open but no data \u2014 marking stream down"),P("No data from dashyd","yellow"),y.onStreamDisconnected()))},1e3)},1e3)}function P(e,t){F.onStatus&&F.onStatus(e,t)}function ze(){return F.isEnabled?F.isEnabled():!0}window.dataStream=O;const v=(function(){let i=null;function s(){return i||(i=window.location.hostname),i}function o(){return`${window.location.protocol}//${s()}:5088`}async function r(_,k={}){const{method:S="GET",body:D=null,timeout:ke=5e3,retries:be=2}=k,Oe=`${o()}${_}`;for(let Z=0;Z<=be;Z++)try{const H={method:S,signal:AbortSignal.timeout(ke),headers:{}};D&&(H.headers["Content-Type"]="application/json",H.body=JSON.stringify(D));const A=await fetch(Oe,H);if(!A.ok){const Ie=await A.text().catch(()=>A.statusText);throw new Error(`${A.status}: ${Ie}`)}return A.headers.get("content-type")?.includes("application/json")?await A.json():await A.text()}catch(H){const A=Z===be,dt=H.name==="TimeoutError"||H.name==="TypeError"||H.message.includes("500");if(A||!dt)throw console.error(`[API] ${S} ${_} failed:`,H.message),H;console.warn(`[API] ${S} ${_} failed (attempt ${Z+1}), retrying...`),await new Promise(Ie=>setTimeout(Ie,1e3*(Z+1)))}}function d(_,k={}){return r(_,{...k,method:"GET"})}function c(_,k,S={}){return r(_,{...S,method:"POST",body:k})}async function l(){return d("/api/init")}async function p(){return d("/api/settings")}async function h(_){return d(`/api/settings/params/${_}`)}async function f(_,k){return c(`/api/settings/params/${_}`,{value:k})}async function m(){return d("/api/models")}async function C(_){return c("/api/models/select",{selected_model:_})}async function E(_="/"){return d(`/api/files?path=${encodeURIComponent(_)}`)}async function x(_,k={}){return c(`/api/action/${encodeURIComponent(_)}`,k)}return{getHost:s,getBaseUrl:o,request:r,get:d,post:c,init:l,getSettings:p,getParam:h,saveParam:f,getModels:m,selectModel:C,getFiles:E,callAction:x}})();window.Api=v;const _t={sections:[{title:"Dashy Visual",requiresHud:!0,settings:[{id:"HudModeEnabled",type:"toggle",label:"Heads-up Display (HUD) Mode",description:"Mirror the display for windshield projection.",defaultValue:!1,storage:"local"},{id:"RainbowEnabled",type:"toggle",label:"Rainbow Road",description:"Animated rainbow gradient on the driving path. Scrolls with vehicle speed.",defaultValue:!1,storage:"local"},{id:"LeadStatsEnabled",type:"toggle",label:"Lead Stats Panel",description:"Show distance, speed, and TTC for the lead vehicle at the bottom of the HUD.",defaultValue:!0,storage:"local"}]}]};function xe(e,t){try{const n=JSON.parse(localStorage.getItem("dashySettings")||"{}");if(n[e]!==void 0)return n[e]}catch{}return t}function bt(e){const t=e.id||e.key;let n=e.defaultValue??e.current_value??!1,a=n===!0||n==="true"||n===1||n==="1";e.storage==="local"&&(a=xe(t,a)===!0);const i=u("input","toggle toggle-primary toggle-lg",{type:"checkbox",dataset:{param:t}});return e.storage&&(i.dataset.storage=e.storage),i.checked=a,i}function gt(e){const t=e.id||e.key,n=e.type==="text_spin_button_item",a=!!e.key,i=u("div","join bg-base-300/50 rounded-lg",{dataset:{param:t}});if(n)i.dataset.min=0,i.dataset.max=e.options.length-1,i.dataset.textOptions=JSON.stringify(e.options);else{const h=e.min??e.min_val??e.minVal,f=e.max??e.max_val??e.maxVal;h!==void 0&&(i.dataset.min=h),f!==void 0&&(i.dataset.max=f)}const s=e.step||1,o=u("button","btn btn-ghost btn-md join-item text-lg min-w-12 min-h-12",{dataset:{step:n?-1:-s},textContent:n?"\u2039":"\u2212"});let r,d;const c=e.defaultValue??e.current_value??0;if(n){const h=c||0;r=e.options[h]||e.options[0],d=h}else{const h=String(s).includes(".")||e.type==="double_spin_button_item";d=c;const f=e.min??e.min_val;e.special_value_text&&c===f?r=e.special_value_text:e.suffix?r=(h?c.toFixed(1):c)+e.suffix:r=h?c.toFixed(1):c}const l=u("span","btn btn-md join-item min-w-24 min-h-12 pointer-events-none stepper-value bg-base-100/50 border-none font-medium text-base",{textContent:r});l.dataset.actualValue=d,n?l.dataset.isTextSpin="true":a&&(l.dataset.suffix=e.suffix||"",l.dataset.specialValueText=e.special_value_text||"",l.dataset.minVal=e.min_val!==void 0?e.min_val:"");const p=u("button","btn btn-ghost btn-md join-item text-lg min-w-12 min-h-12",{dataset:{step:n?1:s},textContent:n?"\u203A":"+"});return i.appendChild(o),i.appendChild(l),i.appendChild(p),i}function yt(e){const t=e.id||e.key;let n=e.defaultValue??e.current_value;e.storage==="local"&&(n=xe(t,n));const a=u("div","join bg-base-300/50 rounded-lg",{dataset:{param:t}});return e.storage&&(a.dataset.storage=e.storage),e.options.forEach(i=>{const s=i.value??i,o=i.label??i.text??i,r=u("button","btn btn-md join-item min-w-24",{dataset:{value:s},textContent:o});s===n?r.classList.add("btn-primary"):r.classList.add("btn-ghost"),a.appendChild(r)}),a}function vt(e){const t=(e.options||[]).map((a,i)=>({value:i,label:a})),n={...e,options:t,defaultValue:e.current_value??e.defaultValue??0};return Pe(n)}function Pe(e){const t=e.id||e.key;let n=e.defaultValue??e.current_value;e.storage==="local"&&(n=xe(t,n));const a=e.options||[],i=a.find(c=>(c.value??c)===n),s=i?i.label??i.text??i:n,o=u("button","btn btn-ghost bg-base-300/50 justify-between gap-2 min-w-36 border border-base-content/10 hover:border-primary/30",{dataset:{param:t,value:n}});e.storage&&(o.dataset.storage=e.storage);const r=u("span","select-label",{textContent:s}),d=u("span","text-base-content/50",{textContent:"\u25BC"});return o.appendChild(r),o.appendChild(d),o.addEventListener("click",c=>{c.stopPropagation(),Ve({title:e.label,options:a,currentValue:o.dataset.value,onSelect:(l,p)=>{o.dataset.value=l,r.textContent=p,o.dispatchEvent(new CustomEvent("select-change",{bubbles:!0,detail:{key:t,value:l}}))}})}),o}let X=null;function Ve({title:e,options:t,currentValue:n,onSelect:a,searchable:i=!1,grouped:s=!1}){X&&ae();const o=u("div","fixed inset-0 bg-black/60 animate-fade-in");o.style.zIndex="400";const r=u("div","fixed bottom-0 left-0 right-0 bg-base-200 rounded-t-2xl max-h-[80vh] flex flex-col animate-slide-up shadow-2xl");r.style.zIndex="401";const d=u("div","flex justify-center pt-3 pb-2");if(d.appendChild(u("div","w-10 h-1 bg-base-content/20 rounded-full")),r.appendChild(d),e){const f=u("div","px-4 pb-3 text-lg font-semibold text-center",{textContent:e});r.appendChild(f)}let c=null;if(i){const f=u("div","px-4 pb-3");c=u("input","input input-bordered w-full bg-base-300 placeholder:text-base-content/50",{type:"text",placeholder:"Search..."}),f.appendChild(c),r.appendChild(f)}r.appendChild(u("div","border-b border-base-content/10"));const l=u("div","overflow-y-auto flex-1 py-2");function p(f=""){l.innerHTML="";const m=f.toLowerCase().trim().split(/\s+/).filter(E=>E),C=E=>{if(m.length===0)return!0;const x=E.toLowerCase();return m.every(_=>x.includes(_))};if(s?Object.entries(t).forEach(([E,x])=>{const _=x.filter(S=>{const D=S.label??S.text??S;return C(D)});if(_.length===0)return;const k=u("div","px-6 py-2 text-sm font-semibold text-primary/70 bg-base-300/30",{textContent:E});l.appendChild(k),_.forEach(S=>{const D=S.value??S,ke=S.label??S.text??S,be=String(D)===String(n),Oe=Ne(D,ke,be,a);l.appendChild(Oe)})}):t.filter(x=>{const _=x.label??x.text??x;return C(String(_))}).forEach(x=>{const _=x.value??x,k=x.label??x.text??x,S=String(_)===String(n),D=Ne(_,k,S,a);l.appendChild(D)}),l.children.length===0){const E=u("div","px-6 py-8 text-center text-base-content/50",{textContent:"No results found"});l.appendChild(E)}}p(),c&&(c.addEventListener("input",f=>{p(f.target.value)}),setTimeout(()=>c.focus(),300)),r.appendChild(l);const h=u("button","w-full py-4 text-center font-medium text-base-content/70 border-t border-base-content/10 hover:bg-base-300/50 shrink-0",{textContent:"Cancel"});h.addEventListener("click",ae),r.appendChild(h),o.addEventListener("click",ae),document.body.appendChild(o),document.body.appendChild(r),X={backdrop:o,sheet:r},document.body.style.overflow="hidden",setTimeout(()=>{const f=l.querySelector('[data-selected="true"]');f&&f.scrollIntoView({block:"center",behavior:"instant"})},50)}function Ne(e,t,n,a){const i=u("button","w-full px-6 py-4 flex items-center justify-between hover:bg-base-300/50 active:bg-base-300 transition-colors "+(n?"text-primary":"text-base-content"));n&&(i.dataset.selected="true");const s=u("span","text-base text-left",{textContent:t});if(i.appendChild(s),n){const o=u("span","text-primary text-lg",{textContent:"\u2713"});i.appendChild(o)}return i.addEventListener("click",()=>{a(e,t),ae()}),i}function ae(){if(!X)return;const{backdrop:e,sheet:t}=X;e.classList.add("animate-fade-out"),t.classList.add("animate-slide-down"),setTimeout(()=>{e.remove(),t.remove(),document.body.style.overflow="",X=null},200)}function xt(e){const t=e.id||e.key;return u("input","input input-bordered w-full max-w-xs",{type:"text",placeholder:e.placeholder||"",dataset:{param:t}})}function ie(e,t,n,a=1200){e._origText||(e._origText=e.textContent),e.textContent=t,e.classList.add(n),R(()=>{e.textContent=e._origText,e.classList.remove("btn-disabled","btn-error","btn-success")},a)}async function Ue(){const e=[];document.querySelectorAll('[data-item-type="text_display_item"][data-param]').forEach(t=>{e.push((async()=>{try{const a=(await v.getParam(t.dataset.param))?.value;t.textContent=a===""||a==null?"(empty)":String(a)}catch(n){console.warn("[settings] refresh display failed:",t.dataset.param,n)}})())}),document.querySelectorAll('input[data-item-type="text_input_item"][data-param]').forEach(t=>{e.push((async()=>{if(document.activeElement!==t)try{const n=await v.getParam(t.dataset.param);t.value=n?.value??""}catch{}})())}),await Promise.all(e)}function wt(e){const t=e.id||e.key,n=e.action,a=e.current_value??"",i=u("div","flex items-center gap-2 mt-3 w-full"),s=u("input","input input-bordered flex-1 min-w-0",{type:"text",placeholder:e.placeholder||"",dataset:{param:t,itemType:"text_input_item"}});s.value=a,n&&(s.dataset.action=n);const o=u("button","btn btn-primary btn-sm shrink-0",{textContent:"Save"});async function r(){const d=s.value.trim();if(d&&!o.classList.contains("btn-disabled")){o.classList.add("btn-disabled"),o.textContent="Saving\u2026";try{n?await v.callAction(n,{value:d}):await v.saveParam(t,d),await Ue(),ie(o,"Saved","btn-success")}catch(c){console.error("[settings] text_input save failed:",c),ie(o,"Error","btn-error",2e3)}}}return o.addEventListener("click",r),s.addEventListener("keydown",d=>{d.key==="Enter"&&(d.preventDefault(),r())}),i.appendChild(s),i.appendChild(o),i}function Ct(e){const t=e.id||e.key,n=e.current_value??"",a=n===""||n==null?"(empty)":String(n);return u("pre","mt-2 p-3 bg-base-300/50 rounded text-xs whitespace-pre-wrap break-all max-h-40 overflow-auto font-mono",{textContent:a,dataset:{param:t,itemType:"text_display_item"}})}const St=new Set(["ssh_key_clear"]);function Tt(e){const t=e.id||e.key,n=e.action||t,i=St.has(n)?"btn btn-error btn-sm":"btn btn-outline btn-sm",s=u("button",i,{textContent:e.button_label||e.title||e.label||"Run",dataset:{param:t,action:n,itemType:"action_item"}});return s.addEventListener("click",async()=>{if(s.classList.contains("btn-disabled"))return;s.classList.add("btn-disabled");const o=s.textContent;s._origText=o,s.textContent="Running\u2026";try{await v.callAction(n),await Ue(),ie(s,"Done","btn-success")}catch(r){console.error("[settings] action_item run failed:",r),ie(s,"Error","btn-error",2e3)}}),s}function Et(e){const t=e.id||e.key,n=!!e.key,a=e.dependsOn||e.initially_enabled_by,i=e.type,s=i==="toggle"||i==="toggle_item",o=u("div","p-4 hover:bg-base-100 transition-colors duration-200");a&&(o.classList.add("ml-4","mr-2","my-1","rounded-lg","bg-base-300/50"),o.dataset.dependsOn=e.dependsOn||e.initially_enabled_by?.param);const r=u("div","flex justify-between items-center gap-4"),d=u("p","font-medium flex-1 text-base",{[n?"innerHTML":"textContent"]:e.label||e.title});r.appendChild(d);const c=e.description;let l=null;c&&(l=u("p","text-sm text-base-content/50 mt-0.5",{[n?"innerHTML":"textContent"]:c}));let p;switch(i){case"toggle":case"toggle_item":p=bt(e),r.appendChild(p),o.appendChild(r),l&&o.appendChild(l),o.classList.add("cursor-pointer","active:bg-base-100/50"),o.addEventListener("click",h=>{h.target.matches('input[type="checkbox"]')||(p.checked=!p.checked,p.dispatchEvent(new Event("change",{bubbles:!0})))});break;case"stepper":case"spin_button_item":case"double_spin_button_item":return p=gt(e),r.appendChild(p),o.appendChild(r),l&&o.appendChild(l),o;case"text_spin_button_item":return p=vt(e),r.appendChild(p),o.appendChild(r),l&&o.appendChild(l),o;case"btn-group":case"button-group":return p=yt(e),r.appendChild(p),o.appendChild(r),l&&o.appendChild(l),o;case"text-input":p=xt(e),r.appendChild(p),o.appendChild(r),l&&o.appendChild(l);break;case"select":return p=Pe(e),r.appendChild(p),o.appendChild(r),l&&o.appendChild(l),o;case"text_input_item":return o.appendChild(r),l&&o.appendChild(l),o.appendChild(wt(e)),o;case"text_display_item":return o.appendChild(r),l&&o.appendChild(l),o.appendChild(Ct(e)),o;case"action_item":{p=Tt(e);const h=u("div","flex justify-end");return h.appendChild(p),o.appendChild(h),l&&o.appendChild(l),o}default:o.appendChild(r),l&&o.appendChild(l)}return o}function se(e,t){document.querySelectorAll(`[data-depends-on="${e}"]`).forEach(a=>{a.style.display=t?"":"none"})}function we(e,t,n,a){const i=e.querySelectorAll("button"),s=i[0],o=i[i.length-1];s&&(s.disabled=t<=n,s.classList.toggle("btn-disabled",t<=n)),o&&(o.disabled=t>=a,o.classList.toggle("btn-disabled",t>=a))}function Lt(e,t){const n=e.id||e.key;return t.some(a=>a.dependsOn===n||a.initially_enabled_by?.param===n)}function kt(e,t){const n=e.settings;let a=null,i=null;n.forEach(s=>{const o=Et(s),r=s.id||s.key,d=s.dependsOn||s.initially_enabled_by,c=s.dependsOn||s.initially_enabled_by?.param,l=o.querySelector(".join[data-param]");if(l&&l.querySelector(".stepper-value")){const p=l.querySelector(".stepper-value"),h=parseFloat(p.dataset.actualValue),f=l.dataset.min!==void 0?parseFloat(l.dataset.min):-1/0,m=l.dataset.max!==void 0?parseFloat(l.dataset.max):1/0;we(l,h,f,m)}if(d&&a&&c===i)o.style.display="none",a.appendChild(o);else if(Lt(s,n))a=u("div",""),a.dataset.parentKey=r,i=r,a.appendChild(o),t.appendChild(a);else if(d){const p=t.querySelector(`[data-parent-key="${c}"]`);p?(o.style.display="none",p.appendChild(o)):(o.style.display="none",t.appendChild(o)),a=null,i=null}else t.appendChild(o),a=null,i=null})}function Ce(e=!0){const t={},n=e?'[data-param][data-storage="local"]':'[data-param]:not([data-storage="local"])';return document.querySelectorAll(n).forEach(a=>{const i=a.dataset.param;if(a.matches('input[type="checkbox"]'))t[i]=a.checked;else if(a.matches(".join")&&a.querySelector(".stepper-value"))t[i]=parseFloat(a.querySelector(".stepper-value").textContent);else if(a.matches(".join")&&!a.querySelector(".stepper-value")){const s=a.querySelector("button.btn-primary");s&&(t[i]=s.dataset.value)}else a.matches(".text-input")&&(t[i]=a.value)}),t}function Y(e,t){try{let n=JSON.parse(localStorage.getItem("dashySettings"))||{};n[e]=t,localStorage.setItem("dashySettings",JSON.stringify(n)),We(e,t)}catch(n){console.error("Error saving local setting:",n)}}function We(e,t){I.set(`ui.${e.charAt(0).toLowerCase()+e.slice(1)}`,t),se(e,t)}function Ot(){try{const e=JSON.parse(localStorage.getItem("dashySettings"))||{};for(const[t,n]of Object.entries(e)){const a=document.querySelector(`[data-param="${t}"][data-storage="local"]`);if(a)if(a.matches('input[type="checkbox"]'))a.checked=n;else if(a.matches(".join")&&a.querySelector(".stepper-value"))a.querySelector(".stepper-value").textContent=n;else if(a.matches(".join")&&!a.querySelector(".stepper-value")){a.querySelectorAll("button").forEach(s=>s.classList.remove("btn-primary"));const i=a.querySelector(`[data-value="${n}"]`);i&&i.classList.add("btn-primary")}else a.matches(".text-input")&&(a.value=n);We(t,n)}}catch(e){console.error("Error loading local settings:",e)}}function It(){I.subscribe("ui.hudModeEnabled",e=>{const t=document.getElementById("hud-page-content");t&&(e?t.classList.add("hud-mode"):t.classList.remove("hud-mode")),se("HudModeEnabled",e)})}const je={},qe={};function Ge(e,t){const n=e.trim().match(/^value\s*(===?|!==?|>=?|<=?)\s*(-?\d+(?:\.\d+)?|true|false)$/);if(!n)return console.warn("Unsafe condition pattern rejected:",e),!1;const a=n[1];let i=n[2];switch(i==="true"?i=!0:i==="false"?i=!1:i=parseFloat(i),a){case"==":return t==i;case"===":return t===i;case"!=":return t!=i;case"!==":return t!==i;case">":return t>i;case">=":return t>=i;case"<":return t{const o=s.target,r=s.action,d=s.condition;if(r==="set_enabled"&&d){const c=Ge(d,n);se(e,c)}});const i=t===!0||n===1||n>0;se(e,i)}const re={};async function le(e,t,n=!1){n?(re[e]&&clearTimeout(re[e]),re[e]=R(()=>{Xe(e,t),delete re[e]},500)):Xe(e,t)}async function Xe(e,t){try{await v.saveParam(e,t)}catch(n){console.error(`Failed to save setting ${e}:`,n)}}async function Rt(e,t=!1){try{const n=await v.getModels();return!n.model_list||Object.keys(n.model_list).length===0?(console.warn("No models available"),!1):(Dt(e,n.model_list,n.selected_model,t),!0)}catch(n){return console.warn("Error fetching model list:",n.message),!1}}function Dt(e,t,n,a=!1){const i=u("div","text-xs text-base-content/60 font-semibold mb-2 flex items-center gap-1.5 px-1");i.innerHTML=`${V.render("car",14,"inline-block")} VEHICLE`;const s=u("div","card bg-base-200 divide-y divide-base-content/10 mb-4 model-selector-card"),o=u("div","p-3 hover:bg-base-100 transition-colors"),r=u("div","flex justify-between items-center gap-4"),d=u("div","flex-1");d.appendChild(u("p","font-medium text-sm",{textContent:"Vehicle Model"})),d.appendChild(u("p","text-xs text-base-content/50 mt-0.5",{textContent:"Select your vehicle or use AUTO for detection."})),r.appendChild(d);const c=u("button","btn btn-ghost bg-base-200/50 justify-between gap-2 min-w-32 border border-base-content/10 hover:border-primary/30");c.dataset.value=n||"";const l=u("span","select-label truncate max-w-24",{textContent:n||"[AUTO]"}),p=u("span","text-base-content/50",{textContent:"\u25BC"});c.appendChild(l),c.appendChild(p);const h={"":[{value:"",label:"[AUTO]"}]};Object.keys(t).sort().forEach(f=>{h[f]=t[f].sort().map(m=>({value:m,label:m}))}),c.addEventListener("click",()=>{Ve({title:"Select Vehicle Model",options:h,currentValue:c.dataset.value,searchable:!0,grouped:!0,onSelect:async(f,m)=>{c.dataset.value=f,l.textContent=m,await Ht(f)}})}),r.appendChild(c),o.appendChild(r),s.appendChild(o),a&&e.firstChild?(e.insertBefore(s,e.firstChild),e.insertBefore(i,s)):(e.appendChild(i),e.appendChild(s))}async function Ht(e){try{await v.selectModel(e)}catch(t){console.error("Error saving model selection:",t)}}function Ye(e,t){t===void 0&&(t=0);const n=e.querySelector(".stepper-value");if(n)return parseFloat(n.dataset.actualValue)||0;const a=e.matches('input[type="checkbox"]')?e:e.querySelector('input[type="checkbox"]');if(a)return a.checked?1:0;if(e.dataset.value!==void 0){const i=e.dataset.value;return i==="true"?1:i==="false"?0:parseFloat(i)||0}return t}function At(e){const t=e||document;for(const[n,a]of Object.entries(qe)){const i=t.querySelector(`[data-param="${a.param}"]`),s=t.querySelector(`[data-param="${n}"]`);if(i&&s){const o=Ye(i,a.default||0),r=Ge(a.condition,o),d=s.closest("[data-depends-on]");d&&(d.style.display=r?"":"none")}}t.querySelectorAll("[data-depends-on]").forEach(n=>{const a=n.dataset.dependsOn;if(!a)return;const i=t.querySelector(`[data-param="${a}"]`);if(!i)return;const s=Ye(i);n.style.display=s>0?"":"none"})}const T={_overlay:null,_contentContainer:null,_closeBtn:null,_currentTab:"controls",_isOpen:!1,_isConnected:!1,_hudEnabled:!0,async show(e="controls"){console.log("[PanelOverlay] show() called, _isOpen="+this._isOpen),!this._isOpen&&(this._isOpen=!0,this._currentTab=e,this._createOverlay(),document.body.appendChild(this._overlay),console.log("[PanelOverlay] show() calling _updateCloseButton"),this._updateCloseButton(),await this._loadTabContent(this._currentTab))},hide(){this._isOpen&&(this._hudEnabled&&!this._isConnected||(this._isOpen=!1,this._overlay&&this._overlay.parentNode&&(this._overlay.classList.add("animate-fade-out"),setTimeout(()=>{this._overlay&&this._overlay.parentNode&&this._overlay.parentNode.removeChild(this._overlay),this._overlay=null,this._contentContainer=null,this._contentWrapper=null,this._closeBtn=null},200))))},setConnected(e){this._isConnected=e,this._updateCloseButton()},setHudEnabled(e){this._hudEnabled=e,this._updateCloseButton()},_updateCloseButton(){console.log("[PanelOverlay] _updateCloseButton: _closeBtn="+!!this._closeBtn+", _isConnected="+this._isConnected+", _hudEnabled="+this._hudEnabled),this._closeBtn&&(this._hudEnabled?this._isConnected?(this._closeBtn.style.display="",this._closeBtn.disabled=!1,this._closeBtn.innerHTML=V.render("x",20),console.log("[PanelOverlay] showing X")):(this._closeBtn.style.display="",this._closeBtn.disabled=!0,this._closeBtn.innerHTML='',console.log("[PanelOverlay] showing spinner")):this._closeBtn.style.display="none")},reload(){this._isOpen&&this._loadTabContent(this._currentTab)},_createOverlay(){this._overlay=document.createElement("div"),this._overlay.className="fixed inset-0 z-[300] flex flex-col hud-panel-surface",this._overlay.style.animation="fadeIn 0.2s ease-out";const e=document.createElement("div");e.className="flex items-center gap-3 p-3 border-b border-base-content/10 hud-mono",e.innerHTML=` - `;e.innerHTML=` + -
- - - +
+ ${i("settings","settings",S("Settings"))} + ${i("files","folder",S("Files"))} + ${this._accelEqAvailable?i("accel","gauge",S("Accel EQ")):""}
- `,this._closeBtn=e.querySelector(".panel-close-btn"),this._closeBtn.addEventListener("click",()=>this.hide()),e.querySelectorAll(".panel-tab-btn").forEach(n=>{n.addEventListener("click",()=>{const a=n.dataset.tab;this._switchTab(a)})}),this._contentWrapper=document.createElement("div"),this._contentWrapper.className="flex-1 overflow-y-auto p-3",this._contentContainer=document.createElement("div"),this._contentContainer.className="max-w-2xl mx-auto",this._contentContainer.innerHTML='
',this._contentWrapper.appendChild(this._contentContainer),this._overlay.appendChild(e),this._overlay.appendChild(this._contentWrapper);const t=n=>{n.key==="Escape"&&(this.hide(),document.removeEventListener("keydown",t))};if(document.addEventListener("keydown",t),!document.getElementById("panel-overlay-styles")){const n=document.createElement("style");n.id="panel-overlay-styles",n.textContent=` + `,this._closeBtn=e.querySelector(".panel-close-btn"),this._closeBtn.addEventListener("click",()=>this.hide()),e.querySelectorAll(".panel-tab-btn").forEach(t=>{t.addEventListener("click",()=>{const a=t.dataset.tab;this._switchTab(a)})}),this._contentWrapper=document.createElement("div"),this._contentWrapper.className="flex-1 overflow-y-auto p-3",this._contentContainer=document.createElement("div"),this._contentContainer.className="max-w-2xl mx-auto",this._contentContainer.innerHTML='
',this._contentWrapper.appendChild(this._contentContainer),this._overlay.appendChild(e),this._overlay.appendChild(this._contentWrapper);const n=t=>{t.key==="Escape"&&(this.hide(),document.removeEventListener("keydown",n))};if(document.addEventListener("keydown",n),!document.getElementById("panel-overlay-styles")){const t=document.createElement("style");t.id="panel-overlay-styles",t.textContent=` @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } @@ -67,32 +70,32 @@ URL Parameters:`),window._originalConsoleLog("?debug=true - Enable debug mode"), from { opacity: 1; } to { opacity: 0; } } - `,document.head.appendChild(n)}},async _switchTab(e){e!==this._currentTab&&(this._currentTab=e,this._overlay.querySelectorAll(".panel-tab-btn").forEach(t=>{t.dataset.tab===e?(t.classList.remove("btn-ghost"),t.classList.add("hud-tab-active")):(t.classList.remove("hud-tab-active"),t.classList.add("btn-ghost"))}),await this._loadTabContent(e))},async _loadTabContent(e){if(this._contentContainer)switch(this._contentWrapper.className="flex-1 overflow-y-auto p-3",this._contentContainer.className="max-w-2xl mx-auto",this._contentContainer.innerHTML='
',e){case"controls":await this._loadControlsTab();break;case"settings":await this._loadSettingsTab();break;case"files":await this._loadFilesTab();break}},async _loadControlsTab(){if(!this._contentContainer)return;this._contentContainer.className="w-full",this._contentContainer.innerHTML="";const e=u("div","hud-section-title mb-3 flex items-center gap-1.5 px-1");e.innerHTML=`${V.render("sliders",14,"inline-block")} DEVICE CONTROLS`,this._contentContainer.appendChild(e);const t=u("div","card bg-base-200 divide-y divide-base-content/10"),n=u("div","p-4 hover:bg-base-100 transition-colors"),a=u("div","flex justify-between items-center gap-4"),i=u("div","flex-1");i.appendChild(u("p","font-medium text-base",{textContent:"Force Offroad"})),i.appendChild(u("p","text-sm text-base-content/50 mt-0.5",{textContent:"Temporarily go offroad to update system."}));const s=u("div","join"),o=u("button","btn btn-lg join-item",{textContent:"Off"}),r=u("button","btn btn-lg join-item",{textContent:"On"}),d=c=>{c?(r.classList.add("btn-error"),o.classList.remove("btn-neutral")):(o.classList.add("btn-neutral"),r.classList.remove("btn-error"))};o.addEventListener("click",async()=>{await v.saveParam("dp_dev_go_off_road",!1),d(!1)}),r.addEventListener("click",()=>{Me({title:"Force Offroad?",message:"This will temporarily disable driving mode. You can update the system while offroad.",confirmText:"Confirm",cancelText:"Cancel",confirmStyle:"warning",onConfirm:async()=>{await v.saveParam("dp_dev_go_off_road",!0),d(!0)}})}),s.appendChild(o),s.appendChild(r),a.appendChild(i),a.appendChild(s),n.appendChild(a),t.appendChild(n),this._contentContainer.appendChild(t);try{const c=await v.getParam("dp_dev_go_off_road"),l=c.value===!0||c.value==="true"||c.value==="1";d(l)}catch(c){console.error("Error fetching offroad param:",c),window.ConnectionState&&y.onServerFailed()}},async _loadSettingsTab(){if(!this._contentContainer)return;this._contentWrapper.className="flex-1 flex flex-col min-h-0 overflow-hidden",this._contentContainer.className="flex flex-1 min-h-0 w-full",this._contentContainer.innerHTML="";const e=u("div","w-48 shrink-0 border-r border-base-content/10 overflow-y-auto py-2"),t=u("div","flex-1 overflow-y-auto p-4");let n=[];try{const h=await v.getSettings();h.settings&&Array.isArray(h.settings)&&(n=h.settings,n.forEach(f=>{f.settings.forEach(m=>{m.on_change&&(je[m.key]=m.on_change),m.initially_enabled_by&&(qe[m.key]=m.initially_enabled_by)})}))}catch(h){console.error("[PanelOverlay] Error loading settings:",h),window.ConnectionState&&y.onServerFailed();return}const a=[...n,..._t.sections],i=[];a.forEach((h,f)=>{const m=u("div","");m.style.display=f===0?"":"none";const C=u("div","card bg-base-200 divide-y divide-base-content/10");kt(h,C),m.appendChild(C),i.push(m),t.appendChild(m)});const s=u("div","space-y-4");if(s.style.display="none",!await Rt(s,!1)){const h=u("div","text-sm text-base-content/60 p-4 text-center",{textContent:"No vehicle models available. Set dp_dev_model_list on the device to populate this list."});s.appendChild(h)}i.push(s),t.appendChild(s),At(t);let r=0;const d=[];a.forEach((h,f)=>{const m=u("button","w-full text-left px-4 py-3 text-sm transition-colors "+(f===0?"hud-sidebar-active hud-mono uppercase font-semibold":"text-base-content/70 hover:bg-base-100 hud-mono uppercase"));m.textContent=h.title,m.addEventListener("click",()=>p(f)),e.appendChild(m),d.push(m)});const c=u("button","w-full text-left px-4 py-3 text-sm text-base-content/70 hover:bg-base-100 transition-colors");c.textContent="Vehicle",c.addEventListener("click",()=>p(a.length)),e.appendChild(c),d.push(c),e.appendChild(u("div","divider my-1 mx-3"));const l=u("button","w-full text-left px-4 py-3 text-sm text-error hover:bg-error/10 transition-colors");l.textContent="Reboot Device",l.addEventListener("click",()=>{Me({title:"Reboot Device?",message:"This will reboot the device to apply changes.",confirmText:"Reboot",cancelText:"Cancel",confirmStyle:"danger",onConfirm:async()=>{await v.saveParam("DoReboot",!0)}})}),e.appendChild(l);function p(h){i[r].style.display="none",i[h].style.display="",d[r].className="w-full text-left px-4 py-3 text-sm text-base-content/70 hover:bg-base-100 transition-colors",d[h].className="w-full text-left px-4 py-3 text-sm hud-sidebar-active hud-mono uppercase font-semibold transition-colors",r=h,t.scrollTop=0}this._contentContainer.appendChild(e),this._contentContainer.appendChild(t)},async _loadFilesTab(){if(!this._contentContainer)return;this._contentContainer.className="w-full",this._contentContainer.innerHTML="";const e=u("div","hud-section-title mb-3 flex items-center gap-1.5 px-1");e.innerHTML=`${V.render("briefcase",14,"inline-block")} FILE BROWSER`,this._contentContainer.appendChild(e);const t=u("div","breadcrumbs text-sm mb-4");t.appendChild(u("ul")),this._contentContainer.appendChild(t);const n=u("div","overflow-x-auto card bg-base-200"),a=u("table","table table-zebra w-full");a.innerHTML=` + `,document.head.appendChild(t)}},async _switchTab(e){e!==this._currentTab&&(this._currentTab=e,this._overlay.querySelectorAll(".panel-tab-btn").forEach(i=>{i.dataset.tab===e?(i.classList.remove("btn-ghost"),i.classList.add("hud-tab-active")):(i.classList.remove("hud-tab-active"),i.classList.add("btn-ghost"))}),await this._loadTabContent(e))},async _loadTabContent(e){if(this._contentContainer)switch(this._accelPage&&e!=="accel"&&(this._accelPage.destroy(),this._accelPage=null),this._contentWrapper.className="flex-1 overflow-y-auto p-3",this._contentContainer.className="max-w-2xl mx-auto",this._contentContainer.innerHTML='
',e){case"settings":await this._loadSettingsTab();break;case"files":await this._loadFilesTab();break;case"accel":await this._loadAccelEqTab();break}},async _loadSettingsTab(){if(!this._contentContainer)return;this._contentWrapper.className="flex-1 flex flex-col min-h-0 overflow-hidden",this._contentContainer.className="flex flex-1 min-h-0 w-full",this._contentContainer.innerHTML="";const e=d("div","shrink-0 border-r border-base-content/10 overflow-y-auto py-2"),i=d("div","flex-1 overflow-y-auto p-4");let n=[],t=null;try{const v=await N.getSettings();t=v.languages||null,v.settings&&Array.isArray(v.settings)&&(n=v.settings,n.forEach(o=>{o.settings.forEach(L=>{L.on_change&&($t[L.key]=L.on_change),L.initially_enabled_by&&(jt[L.key]=L.initially_enabled_by)})}))}catch(v){console.error("[PanelOverlay] Error loading settings:",v),window.ConnectionState&&K.onServerFailed();return}const a=!!(t&&t.available&&t.list&&Object.keys(t.list).length),s=n.find(v=>v&&v.title==="Device"&&Array.isArray(v.settings));if(s&&Array.isArray(ut.dashyVisual)){const v=s.settings.findIndex(o=>(o.key||o.id)==="dp_dev_dashy");v>=0&&s.settings.splice(v+1,0,...ut.dashyVisual)}const r=[...n,...ut.sections],l=[];let u=null;r.forEach((v,o)=>{const L=d("div","");L.style.display=o===0?"":"none";const B=d("div","card bg-base-200 divide-y divide-base-content/10");Pn(v,B),a&&v.title==="Device"&&B.appendChild(this._buildLanguageRow(t)),L.appendChild(B),v.title==="Device"&&(u=L),l.push(L),i.appendChild(L)});const h=u||i,f=d("div","mt-4");await On(f,!1)||f.appendChild(d("div","text-sm text-base-content/60 p-4 text-center",{textContent:S("No vehicle models available. Set dp_dev_model_list on the device to populate this list.")})),h.appendChild(f),Bn(i);const g={Device:"smartphone",Lateral:"move-horizontal",Longitudinal:"move-vertical",UI:"palette",Openpilot:"brand-comma",Developer:"code","Toyota / Lexus":"brand-toyota",Honda:"brand-honda",HKG:"brand-hyundai",VAG:"brand-vw",Mazda:"brand-mazda"},w=v=>g[v]||"sliders",k=(v,o,L,B=!1)=>{const x=d("button","nav-item w-full px-3 py-3 text-sm transition-colors hud-mono uppercase flex items-center gap-3 "+(B?"text-error hover:bg-error/10":"text-base-content/70 hover:bg-base-100")),A=d("span","shrink-0 flex items-center");return A.innerHTML=ie.render(v,22,"inline-block"),x.appendChild(A),x.appendChild(d("span","nav-label truncate",{textContent:o})),x.addEventListener("click",L),x},j="dashy.settingsNavCollapsed";let P;try{const v=localStorage.getItem(j);P=v==="1"?!0:v==="0"?!1:window.innerWidth<640}catch{P=window.innerWidth<640}function E(){e.classList.toggle("w-48",!P),e.classList.toggle("w-14",P),e.querySelectorAll(".nav-label").forEach(v=>v.classList.toggle("hidden",P)),e.querySelectorAll(".nav-item").forEach(v=>v.classList.toggle("justify-center",P)),_.innerHTML=ie.render(P?"chevrons-right":"chevrons-left",22,"inline-block")}const _=d("button","w-full px-3 py-2 mb-1 text-base-content/50 hover:bg-base-100 flex items-center justify-center");_.addEventListener("click",()=>{P=!P;try{localStorage.setItem(j,P?"1":"0")}catch{}E()}),e.appendChild(_);let R=0;const H=[];function U(v){l[R].style.display="none",l[v].style.display="",H[R].classList.remove("hud-sidebar-active","font-semibold"),H[R].classList.add("text-base-content/70"),H[v].classList.add("hud-sidebar-active","font-semibold"),H[v].classList.remove("text-base-content/70"),R=v,i.scrollTop=0}r.forEach((v,o)=>{const L=k(w(v.title),v.title,()=>U(o));o===0&&(L.classList.add("hud-sidebar-active","font-semibold"),L.classList.remove("text-base-content/70")),e.appendChild(L),H.push(L)}),e.appendChild(d("div","divider my-1 mx-3"));let z=!1;const X=k("square-parking",S("Force Offroad"),()=>{z?N.saveParam("dp_dev_go_off_road",!1).then(()=>{z=!1,ae()}).catch(v=>console.error("[PanelOverlay] offroad off failed:",v)):Pt({title:S("Force Offroad?"),message:S("This temporarily disables driving mode so you can update the system. Tap again to resume."),confirmText:S("Confirm"),cancelText:S("Cancel"),confirmStyle:"warning",onConfirm:async()=>{await N.saveParam("dp_dev_go_off_road",!0),z=!0,ae()}})});function ae(){const v=X.querySelector(".nav-label");v&&(v.textContent=S(z?"Resume Onroad":"Force Offroad")),X.classList.toggle("text-warning",z),X.classList.toggle("text-base-content/70",!z),X.title=S(z?"Currently offroad \u2014 tap to resume":"Force offroad to update")}N.getParam("dp_dev_go_off_road").then(v=>{z=!!(v&&(v.value===!0||v.value==="true"||v.value==="1")),ae()}).catch(()=>{}),e.appendChild(X);const G=k("power",S("Reboot Device"),()=>{Pt({title:S("Reboot Device?"),message:S("This will reboot the device to apply changes."),confirmText:S("Reboot"),cancelText:"Cancel",confirmStyle:"danger",onConfirm:async()=>{await N.saveParam("DoReboot",!0)}})},!0);e.appendChild(G),E(),this._contentContainer.appendChild(e),this._contentContainer.appendChild(i)},_buildLanguageRow(e){const i=e.list,n={};Object.entries(i).forEach(([l,u])=>{n[u]=l});const t=e.current,a=d("div","p-4"),s=d("div","flex justify-between items-center gap-4");s.appendChild(d("p","font-medium flex-1 text-base",{textContent:e.title||S("Change Language")}));const r=d("button","btn btn-ghost bg-base-300/50 justify-between gap-2 min-w-36 border border-base-content/10 shrink-0");return r.append(d("span","",{textContent:n[t]||t||S("Default")}),d("span","text-base-content/50",{textContent:"\u25BE"})),r.addEventListener("click",()=>{Pe({title:e.dialog||S("Select a language"),searchable:!0,options:Object.entries(i).map(([l,u])=>({value:u,label:l})),currentValue:t,onSelect:async l=>{try{await N.saveParam("LanguageSetting",l)}catch(u){console.error("[PanelOverlay] set language failed:",u);return}this._loadSettingsTab()}})}),s.appendChild(r),a.appendChild(s),a},async _loadFilesTab(){if(!this._contentContainer)return;this._contentContainer.className="w-full",this._contentContainer.innerHTML="";const e=d("div","hud-section-title mb-3 flex items-center gap-1.5 px-1");e.innerHTML=`${ie.render("briefcase",14,"inline-block")} ${S("FILE BROWSER")}`,this._contentContainer.appendChild(e);const i=d("div","breadcrumbs text-sm mb-4");i.appendChild(d("ul")),this._contentContainer.appendChild(i);const n=d("div","overflow-x-auto card bg-base-200"),t=d("table","table table-zebra w-full");t.innerHTML=` - Name - Last Modified - Size + ${S("Name")} + ${S("Last Modified")} + ${S("Size")} - `,n.appendChild(a),this._contentContainer.appendChild(n),Bt(t,a)}};window.PanelOverlay=T;function Mt(e){if(e===0)return"0 B";const t=1024,n=["B","KB","MB","GB","TB"],a=Math.floor(Math.log(e)/Math.log(t));return parseFloat((e/Math.pow(t,a)).toFixed(1))+" "+n[a]}const Se={fileRow:(e,t)=>{const n=e.is_dir?"\u{1F4C1}":e.name.endsWith(".ts")?"\u{1F3AC}":"\u{1F4C4}",a=`${t}/${e.name}`.replace("//","/"),i=e.name.endsWith(".ts")?`\u25B6`:"",s=e.is_dir?`${e.name}`:`${e.name}`;return` + `,n.appendChild(t),this._contentContainer.appendChild(n),Fn(i,t)},async _loadAccelEqTab(){this._contentContainer&&(this._contentContainer.className="w-full",this._contentContainer.innerHTML="",this._accelPage&&(this._accelPage.destroy(),this._accelPage=null),this._accelPage=yt.render(this._contentContainer))}};window.PanelOverlay=Q;function Nn(e){if(e===0)return"0 B";const i=1024,n=["B","KB","MB","GB","TB"],t=Math.floor(Math.log(e)/Math.log(i));return parseFloat((e/Math.pow(i,t)).toFixed(1))+" "+n[t]}const gt={fileRow:(e,i)=>{const n=e.is_dir?"\u{1F4C1}":e.name.endsWith(".ts")?"\u{1F3AC}":"\u{1F4C4}",t=`${i}/${e.name}`.replace("//","/"),a=e.name.endsWith(".ts")?`\u25B6`:"",s=e.is_dir?`${e.name}`:`${e.name}`;return` - ${n} ${i} + ${n} ${a} ${s} ${e.mtime} - ${e.is_dir?"-":Mt(e.size)} + ${e.is_dir?"-":Nn(e.size)} - `},breadcrumb:e=>{const t=e.split("/").filter(Boolean);let n="",a='
  • root
  • ';return t.forEach((i,s)=>{n+="/"+i,s===t.length-1?a+=`
  • ${i}
  • `:a+=`
  • ${i}
  • `}),a}};let Ke=null,Je=null;async function ce(e="/"){const t=Ke||document.querySelector("#files-table tbody"),n=Je||document.getElementById("files-breadcrumbs");if(!(!t||!n))try{t.innerHTML='';const a=await v.getFiles(e);Ft(a.path,n),$t(a.path,a.files,t)}catch(a){t.innerHTML=`Failed to load files: ${a.message}`,console.error("Fetch files error:",a)}}function Bt(e,t){Je=e,Ke=t.querySelector("tbody"),e.addEventListener("click",n=>{const a=n.target.closest("a[data-path]");a&&(n.preventDefault(),ce(a.dataset.path))}),t.addEventListener("click",n=>{const a=n.target.closest("a[data-path]");a&&(n.preventDefault(),ce(a.dataset.path))}),ce("/")}function Ft(e,t){const n=t.querySelector("ul");n?n.innerHTML=Se.breadcrumb(e):t.innerHTML="
      "+Se.breadcrumb(e)+"
    ",I.set("files.currentPath",e)}function $t(e,t,n){I.set("files.items",t);let a="";if(e){const i=e.substring(0,e.lastIndexOf("/"))||"/";a+=`\u{1F4C2}.. (Parent Directory)`}t.forEach(i=>{a+=Se.fileRow(i,e)}),n.innerHTML=a}const V={settings:'',x:'',car:'',sliders:'',briefcase:'',render(e,t=24,n=""){const a=this[e];if(!a)return console.warn(`Icon "${e}" not found`),"";const i=`width="${t}" height="${t}"`,s=n?` class="${n}"`:"";return a.replace("{this._text="",this._dirty=!0},5e3)},setLastDataTime(e){this._lastDataTime=e},checkHealth(){const e=Date.now();return e-this._lastHealthCheckTime0&&e-this._lastDataTime>3e3&&this._cachedHealthWarnings.push(this._warningDataStale),this._cachedHealthWarnings)},draw(e,t,n){const a=this._text,i=this._color;(a!==this._lastText||i!==this._lastColor)&&(this._dirty=!0,this._lastText=a,this._lastColor=i);const s=this.checkHealth();if(s.length>0&&!a){const r=s.find(d=>d.severity==="critical")||s[0];this.updateStatus(r.text,r.severity==="critical"?"red":"yellow");return}if(a){if((!this._canvasSize||this._canvasSize.width!==t||this._canvasSize.height!==n)&&(this._dirty=!0,this._canvasSize={width:t,height:n}),this._dirty){this._dirty=!1;const o=1.5*8,r=.5*8,d=1.5*8;e.font=`${d}px Arial`;const l=e.measureText(a).width+o*2+6+4,p=d+r*2,h=400,f=40;this._offscreenCanvas||(this._offscreenCanvas=document.createElement("canvas"),this._offscreenCanvas.width=h,this._offscreenCanvas.height=f,this._offscreenCtx=this._offscreenCanvas.getContext("2d")),this._currentWidth=Math.min(l,h),this._currentHeight=p,this._offscreenCtx.clearRect(0,0,this._currentWidth,this._currentHeight);const m=5;this._offscreenCtx.fillStyle="rgba(0, 0, 0, 0.7)",this._offscreenCtx.beginPath(),this._offscreenCtx.roundRect(0,0,this._currentWidth,this._currentHeight,m),this._offscreenCtx.fill();const C=3,E=o+C,x=this._currentHeight/2;this._offscreenCtx.fillStyle=i,this._offscreenCtx.beginPath(),this._offscreenCtx.arc(E,x,C,0,Math.PI*2),this._offscreenCtx.fill(),this._offscreenCtx.fillStyle="white",this._offscreenCtx.font=`${d}px Arial`,this._offscreenCtx.textAlign="left",this._offscreenCtx.textBaseline="middle";const _=E+C+4;this._offscreenCtx.fillText(a,_,x)}if(this._offscreenCanvas&&this._currentWidth>0){const o=t/2,r=15,d=o-this._currentWidth/2,c=n-this._currentHeight-r;e.drawImage(this._offscreenCanvas,0,0,this._currentWidth,this._currentHeight,d,c,this._currentWidth,this._currentHeight)}}},cleanup(){Ae(this._timeout),this._timeout=null,this._offscreenCanvas=null,this._offscreenCtx=null,this._text="",this._lastText="",this._lastColor="",this._dirty=!0,this._canvasSize=null,this._currentWidth=0,this._currentHeight=0,this._lastHealthCheckTime=0,this._cachedHealthWarnings=[],this._lastDataTime=0}};window.StatusBar=K;const Qe={_button:null,create(e){this.destroy(),this._button=document.createElement("button"),this._button.id="hud-settings-btn",this._button.className="btn btn-square btn-md md:btn-lg bg-base-300/90 hover:bg-base-200 border-base-content/10 focus:outline-none focus:ring-0 shadow-lg",this._button.style.cssText=` + `},breadcrumb:e=>{const i=e.split("/").filter(Boolean);let n="",t='
  • root
  • ';return i.forEach((a,s)=>{n+="/"+a,s===i.length-1?t+=`
  • ${a}
  • `:t+=`
  • ${a}
  • `}),t}};let qt=null,Ut=null;async function Ke(e="/"){const i=qt||document.querySelector("#files-table tbody"),n=Ut||document.getElementById("files-breadcrumbs");if(!(!i||!n))try{i.innerHTML='';const t=await N.getFiles(e);$n(t.path,n),jn(t.path,t.files,i)}catch(t){i.innerHTML=`Failed to load files: ${t.message}`,console.error("Fetch files error:",t)}}function Fn(e,i){Ut=e,qt=i.querySelector("tbody"),e.addEventListener("click",n=>{const t=n.target.closest("a[data-path]");t&&(n.preventDefault(),Ke(t.dataset.path))}),i.addEventListener("click",n=>{const t=n.target.closest("a[data-path]");t&&(n.preventDefault(),Ke(t.dataset.path))}),Ke("/")}function $n(e,i){const n=i.querySelector("ul");n?n.innerHTML=gt.breadcrumb(e):i.innerHTML="
      "+gt.breadcrumb(e)+"
    ",ue.set("files.currentPath",e)}function jn(e,i,n){ue.set("files.items",i);let t="";if(e){const a=e.substring(0,e.lastIndexOf("/"))||"/";t+=`\u{1F4C2}.. (Parent Directory)`}i.forEach(a=>{t+=gt.fileRow(a,e)}),n.innerHTML=t}const ie={settings:'',x:'',lock:'',folder:'',activity:'',gauge:'',undo:'',redo:'',car:'',sliders:'',briefcase:'',eye:'',"eye-off":'',smartphone:'',palette:'',"move-vertical":'',"move-horizontal":'',code:'',power:'',"square-parking":'',"chevrons-left":'',"chevrons-right":'',"brand-toyota":'',"brand-honda":'',"brand-hyundai":'',"brand-vw":'',"brand-comma":'',"brand-mazda":'',render(e,i=24,n=""){const t=this[e];if(!t)return console.warn(`Icon "${e}" not found`),"";const a=`width="${i}" height="${i}"`,s=n?` class="${n}"`:"";return t.replace("{this._text="",this._dirty=!0},5e3)},setLastDataTime(e){this._lastDataTime=e},checkHealth(){const e=Date.now();return e-this._lastHealthCheckTime0&&e-this._lastDataTime>3e3&&this._cachedHealthWarnings.push(this._warningDataStale),this._cachedHealthWarnings)},draw(e,i,n){const t=this._text,a=this._color;(t!==this._lastText||a!==this._lastColor)&&(this._dirty=!0,this._lastText=t,this._lastColor=a);const s=this.checkHealth();if(s.length>0&&!t){const l=s.find(u=>u.severity==="critical")||s[0];this.updateStatus(l.text,l.severity==="critical"?"red":"yellow");return}if(t){if((!this._canvasSize||this._canvasSize.width!==i||this._canvasSize.height!==n)&&(this._dirty=!0,this._canvasSize={width:i,height:n}),this._dirty){this._dirty=!1;const r=1.5*8,l=.5*8,u=1.5*8;e.font=`${u}px Arial`;const f=e.measureText(t).width+r*2+6+4,m=u+l*2,g=400,w=40;this._offscreenCanvas||(this._offscreenCanvas=document.createElement("canvas"),this._offscreenCanvas.width=g,this._offscreenCanvas.height=w,this._offscreenCtx=this._offscreenCanvas.getContext("2d")),this._currentWidth=Math.min(f,g),this._currentHeight=m,this._offscreenCtx.clearRect(0,0,this._currentWidth,this._currentHeight);const k=5;this._offscreenCtx.fillStyle="rgba(0, 0, 0, 0.7)",this._offscreenCtx.beginPath(),this._offscreenCtx.roundRect(0,0,this._currentWidth,this._currentHeight,k),this._offscreenCtx.fill();const j=3,P=r+j,E=this._currentHeight/2;this._offscreenCtx.fillStyle=a,this._offscreenCtx.beginPath(),this._offscreenCtx.arc(P,E,j,0,Math.PI*2),this._offscreenCtx.fill(),this._offscreenCtx.fillStyle="white",this._offscreenCtx.font=`${u}px Arial`,this._offscreenCtx.textAlign="left",this._offscreenCtx.textBaseline="middle";const _=P+j+4;this._offscreenCtx.fillText(t,_,E)}if(this._offscreenCanvas&&this._currentWidth>0){const r=i/2,l=15,u=r-this._currentWidth/2,h=n-this._currentHeight-l;e.drawImage(this._offscreenCanvas,0,0,this._currentWidth,this._currentHeight,u,h,this._currentWidth,this._currentHeight)}}},cleanup(){Mt(this._timeout),this._timeout=null,this._offscreenCanvas=null,this._offscreenCtx=null,this._text="",this._lastText="",this._lastColor="",this._dirty=!0,this._canvasSize=null,this._currentWidth=0,this._currentHeight=0,this._lastHealthCheckTime=0,this._cachedHealthWarnings=[],this._lastDataTime=0}};window.StatusBar=De;const Xt={_button:null,create(e){this.destroy(),this._button=document.createElement("button"),this._button.id="hud-settings-btn",this._button.className="btn btn-square btn-md md:btn-lg bg-base-300/90 hover:bg-base-200 border-base-content/10 focus:outline-none focus:ring-0 shadow-lg",this._button.style.cssText=` position: absolute; right: 12px; bottom: 16px; z-index: 200; pointer-events: auto; - `,this._button.innerHTML=V.render("settings",24),this._button.onclick=t=>{t.stopPropagation(),typeof T<"u"&&T.show("controls")},e.appendChild(this._button)},destroy(){this._button&&(this._button.remove(),this._button=null)}};window.SettingsButton=Qe;const zt=100,Pt=120,Vt=72,Nt=64,Ut=44;function N(e){const t=window.ScreenObserver?q.getState().scale:1;return Math.round(e*t)}const Ze=5,Wt=10,J={none:"none",small:"small",mid:"mid",full:"full"},$={normal:"normal",userPrompt:"userPrompt",critical:"critical"},et={[$.normal]:"rgba(0, 0, 0, 0.92)",[$.userPrompt]:"rgba(254, 140, 52, 0.92)",[$.critical]:"rgba(201, 34, 49, 0.92)"},jt={text1:"openpilot Unavailable",text2:"Waiting to start",size:J.mid,status:$.normal},qt={text1:"TAKE CONTROL IMMEDIATELY",text2:"System Unresponsive",size:J.full,status:$.critical},Gt={text1:"System Unresponsive",text2:"Reboot Device",size:J.full,status:$.critical};class Xt{constructor(){this.started_time=0,this.last_selfdrive_time=0,this._overlay=null,this._text1=null,this._text2=null,this._cachedAlert=null}render(t,n){this._overlay||this._createOverlay();const a=this._getAlert(n);if(!a)return this._overlay.style.display="none",this._cachedAlert=null,!1;const i=`${t.x},${t.width}`;if(this._cachedAlert&&this._cachedAlert.text1===a.text1&&this._cachedAlert.text2===a.text2&&this._cachedAlert.size===a.size&&this._cachedAlert.status===a.status&&this._cachedAlert.rectKey===i)return!0;this._cachedAlert={...a,rectKey:i};const s=et[a.status]||et[$.normal],o=t.x||0,r=t.width||"100%",d=typeof r=="number"?`${r}px`:r;if(a.size===J.full){this._overlay.style.cssText=` + `,this._button.innerHTML=ie.render("settings",24),this._button.onclick=i=>{i.stopPropagation(),typeof Q<"u"&&Q.show("settings")},e.appendChild(this._button)},destroy(){this._button&&(this._button.remove(),this._button=null)}};window.SettingsButton=Xt;const zn=100,Vn=120,Wn=72,qn=64,Un=44;function we(e){const i=window.ScreenObserver?Le.getState().scale:1;return Math.round(e*i)}const Gt=5,Xn=10,Oe={none:"none",small:"small",mid:"mid",full:"full"},ge={normal:"normal",userPrompt:"userPrompt",critical:"critical"},Kt={[ge.normal]:"rgba(0, 0, 0, 0.92)",[ge.userPrompt]:"rgba(254, 140, 52, 0.92)",[ge.critical]:"rgba(201, 34, 49, 0.92)"},Gn={text1:"openpilot Unavailable",text2:"Waiting to start",size:Oe.mid,status:ge.normal},Kn={text1:"TAKE CONTROL IMMEDIATELY",text2:"System Unresponsive",size:Oe.full,status:ge.critical},Yn={text1:"System Unresponsive",text2:"Reboot Device",size:Oe.full,status:ge.critical};class Jn{constructor(){this.started_time=0,this.last_selfdrive_time=0,this._overlay=null,this._text1=null,this._text2=null,this._cachedAlert=null}render(i,n){this._overlay||this._createOverlay();const t=this._getAlert(n);if(!t)return this._overlay.style.display="none",this._cachedAlert=null,!1;const a=`${i.x},${i.width}`;if(this._cachedAlert&&this._cachedAlert.text1===t.text1&&this._cachedAlert.text2===t.text2&&this._cachedAlert.size===t.size&&this._cachedAlert.status===t.status&&this._cachedAlert.rectKey===a)return!0;this._cachedAlert={...t,rectKey:a};const s=Kt[t.status]||Kt[ge.normal],r=i.x||0,l=i.width||"100%",u=typeof l=="number"?`${l}px`:l;if(t.size===Oe.full){this._overlay.style.cssText=` position: absolute; - top: 0; left: ${o}px; width: ${d}; bottom: 0; + top: 0; left: ${r}px; width: ${u}; bottom: 0; background: ${s}; display: flex; flex-direction: column; @@ -100,10 +103,10 @@ URL Parameters:`),window._originalConsoleLog("?debug=true - Enable debug mode"), justify-content: center; z-index: 300; pointer-events: none; - `;const c=a.text1.length>15;this._text1.style.fontSize=N(c?zt:Pt)+"px",this._text2.style.fontSize=N(Vt)+"px"}else{const c=N(32),l=N(16);this._overlay.style.cssText=` + `;const h=t.text1.length>15;this._text1.style.fontSize=we(h?zn:Vn)+"px",this._text2.style.fontSize=we(Wn)+"px"}else{const h=we(32),f=we(16);this._overlay.style.cssText=` position: absolute; - left: ${o}px; width: ${d}; bottom: 0; - padding: ${c}px ${l}px; + left: ${r}px; width: ${u}; bottom: 0; + padding: ${h}px ${f}px; background: ${s}; display: flex; flex-direction: column; @@ -112,4 +115,4 @@ URL Parameters:`),window._originalConsoleLog("?debug=true - Enable debug mode"), z-index: 300; pointer-events: none; box-sizing: border-box; - `,this._text1.style.fontSize=N(Nt)+"px",this._text2.style.fontSize=N(Ut)+"px"}return this._text1.textContent=a.text1,this._text2.textContent=a.text2||"",this._text2.style.display=a.text2?"":"none",this._text2.style.marginTop=N(8)+"px",this._overlay.style.display="",!0}_createOverlay(){const t=document.getElementById("hud-page-content");t&&(this._overlay=document.createElement("div"),this._overlay.id="alert-overlay",this._overlay.style.display="none",this._text1=document.createElement("div"),this._text1.style.cssText="color: white; font-family: Arial, sans-serif; font-weight: bold; text-align: center; padding: 0 20px;",this._text2=document.createElement("div"),this._text2.style.cssText="color: white; font-family: Arial, sans-serif; font-weight: bold; text-align: center; padding: 0 20px; margin-top: 8px;",this._overlay.appendChild(this._text1),this._overlay.appendChild(this._text2),t.appendChild(this._overlay))}_getAlert(t){const n=Date.now();if(this.started_time===0&&t.selfdriveState&&(this.started_time=n),!t.selfdriveState)return this.started_time>0&&n-this.started_time>5e3?jt:null;t.updated&&t.updated.selfdriveState&&(this.last_selfdrive_time=n);const a=t.deviceState;if(a&&["tici","tizi","mici"].includes(a.deviceType)&&this.last_selfdrive_time>0){const o=(n-this.last_selfdrive_time)/1e3;if(o>Ze){const r=t.selfdriveState;return r&&r.enabled&&o-Zew;let Yt=null,Ee=null,L=null,z=null,de=0,ue=0,he=null,st=[],pe=[];async function Kt(e){if(Yt=e.hudPage,Ee=e.hudContent,L=e.uiCanvas,z=L.getContext("2d"),O.init({onStatus:Qt,onData:Jt,isEnabled:()=>I.get("ui.hudEnabled")&&!tt}),await new Promise((a,i)=>{const s=document.createElement("script");s.src="/js/themes/tesla.js",s.onload=()=>a(),s.onerror=()=>i(new Error("Failed to load Tesla theme")),document.head.appendChild(s)}),w=new TeslaTheme,L&&(L.style.pointerEvents="none"),w.init&&await w.init(L,z),document.fonts?.load)try{await document.fonts.load('64px "Geist Mono"')}catch{}window.animationFrameId=requestAnimationFrame(Le),he=q.subscribe(a=>{ot(a)});const t=a=>{if(w&&w.handleClick){const i=L.getBoundingClientRect(),s=a.clientX-i.left,o=a.clientY-i.top;w.handleClick(s,o)&&a.stopPropagation()}};L.addEventListener("click",t),pe.push(()=>L.removeEventListener("click",t));const n=()=>{if(document.hidden)Te=Date.now();else if(I.get("ui.hudEnabled")){const a=Te>0?Date.now()-Te:0;window.animationFrameId&&(cancelAnimationFrame(window.animationFrameId),window.animationFrameId=null),window.animationFrameId=requestAnimationFrame(Le),a>3e3&&(console.log(`Tab was hidden for ${a}ms, forcing data-stream reconnect`),O.connect())}};document.addEventListener("visibilitychange",n),pe.push(()=>document.removeEventListener("visibilitychange",n)),ot()}function ot(e){const t=e?.width||Ee.getBoundingClientRect().width,n=e?.height||Ee.getBoundingClientRect().height,a=window.devicePixelRatio||1;if(t<=0)return;const i=Math.round(t*a),s=Math.round(n*a);L.width===i&&L.height===s||(L.width=i,L.height=s,L.style.width=t+"px",L.style.height=n+"px",z.setTransform(a,0,0,a,0,0),de=t,ue=n,w&&w.onResize&&w.onResize(t,n,e||q.getState()))}function Jt(e,t){const n=Date.now();b[e]=t,b.recv_frame[e]=b.frame,b.recv_time[e]=n/1e3,b.updated[e]=!0,b.valid[e]=t!==null,b.seen[e]=!0,e==="selfdriveState"&&(it=n,nt===0&&(nt=n,at=b.frame)),b.frame++}function Qt(e,t){K.updateStatus(e,t)}function rt(){O.cleanup(),window.animationFrameId&&(cancelAnimationFrame(window.animationFrameId),window.animationFrameId=null),st.forEach(e=>{typeof e=="function"&&e()}),st=[],pe.forEach(e=>{typeof e=="function"&&e()}),pe=[],he&&(he(),he=null),Qe.destroy(),K.cleanup(),w&&w.destroy&&(w.destroy(),w=null)}let Q=null,fe=null,me=null,_e=null;function Zt(){te(fe),fe=He(async()=>{try{const e=await v.init();_e!==null&&e.dp_dev_dashy!==_e&&y.setDashyOn(e.dp_dev_dashy!==!1),_e=e.dp_dev_dashy!==!1,me!==null&&!!e.isOffroad!==me&&(y.setOffroad(!!e.isOffroad),e.isOffroad||O.connect()),me=!!e.isOffroad}catch{te(fe),fe=null,y.onServerFailed()}},5e3)}async function lt(){const e=v.getHost();O.setHost(e),console.log("[Init] Connecting to device at:",e);try{Q=await v.init(),console.log("[Init] Device connected, fetching settings..."),y.onServerConnected(),Q.dp_dev_dashy===!1&&y.setDashyOn(!1),_e=Q.dp_dev_dashy!==!1,me=!!Q.isOffroad,Q.isOffroad?y.setOffroad(!0):(y.setOffroad(!1),O.connect()),Zt()}catch(t){console.error("[Init] Failed to connect:",t),y.onServerFailed()}}function rn(){tt=!0,I.set("ui.hudEnabled",!1);const e=document.getElementById("hud-page");e&&(e.style.display="none")}let ct=-1;function Le(){if(!I.get("ui.hudEnabled")||document.hidden){window.animationFrameId=null;return}if(window.animationFrameId=requestAnimationFrame(Le),b.frame!==ct){ct=b.frame,z.save(),z.clearRect(0,0,de,ue),U.frame=b.frame,U.started_frame=at,b.selfdriveState&&(U.engaged=b.selfdriveState.enabled||!1,b.selfdriveState.enabled?U.status="ENGAGED":b.selfdriveState.activeOverride?U.status="OVERRIDE":U.status="DISENGAGED"),w&&(w.update(b),w.render(z,de,ue)),K.setLastDataTime(it),K.draw(z,de,ue),z.restore();for(const e in b.updated)b.updated[e]=!1}}const en=document.getElementById("hud-page"),tn=document.getElementById("hud-page-content"),nn=document.getElementById("uiCanvas");function an(){document.addEventListener("click",e=>{if(e.target.matches(".join button")&&e.target.parentElement.querySelector(".stepper-value")){if(e.target.disabled){e.preventDefault();return}const t=e.target.parentElement,n=t.querySelector(".stepper-value"),a=parseFloat(e.target.dataset.step),i=t.dataset.min!==void 0?parseFloat(t.dataset.min):-1/0,s=t.dataset.max!==void 0?parseFloat(t.dataset.max):1/0;if(n.dataset.isTextSpin==="true"){const d=JSON.parse(t.dataset.textOptions||"[]");let c=parseInt(n.dataset.actualValue||0);if(a<0&&c<=0||a>0&&c>=d.length-1)return;let l=c+a;if(l=Math.max(0,Math.min(d.length-1,l)),l===c)return;n.dataset.actualValue=l,n.textContent=d[l],we(t,l,0,d.length-1),oe(t.dataset.param,l);const p=t.dataset.param;p&&!t.dataset.storage&&le(p,l,!0)}else{const d=n.dataset.actualValue;if(d===void 0||d===""){console.error("actualValue not set for numeric spinner");return}let c=parseFloat(d);if(isNaN(c)){console.error("Invalid numeric value:",d);return}if(a<0&&c<=i||a>0&&c>=s)return;let l=c+a;if(String(a).includes(".")&&(l=parseFloat(l.toFixed(2))),l=Math.max(i,Math.min(s,l)),l===c)return;n.dataset.actualValue=l;const p=n.dataset.suffix||"",h=n.dataset.specialValueText||"",f=n.dataset.minVal!==""?parseFloat(n.dataset.minVal):i;h&&l===f?n.textContent=h:p?String(a).includes(".")?n.textContent=l.toFixed(1)+p:n.textContent=l+p:String(a).includes(".")?n.textContent=l.toFixed(1):n.textContent=l,we(t,l,i,s),oe(t.dataset.param,l);const m=t.dataset.param;m&&!t.dataset.storage&&le(m,l,!0)}const r=e.target.closest("[data-param]");r&&r.dataset.storage==="local"&&R(()=>{const d=r.dataset.param,c=Ce(!0)[d];c!==void 0&&Y(d,c)},50);return}if(e.target.matches(".join button")&&!e.target.parentElement.querySelector(".stepper-value")){e.target.parentElement.querySelectorAll("button").forEach(a=>a.classList.remove("btn-primary")),e.target.classList.add("btn-primary");const n=e.target.closest("[data-param]");n&&n.dataset.storage==="local"&&R(()=>{const a=n.dataset.param,i=Ce(!0)[a];i!==void 0&&Y(a,i)},50);return}if(e.target.matches("#files-table a, #files-breadcrumbs a, #files-breadcrumbs ul a")){if(e.target.href&&e.target.href.includes("/api/play"))return;const t=e.target.dataset.path;t!==void 0&&(e.preventDefault(),ce(t));return}}),document.addEventListener("change",e=>{const t=e.target.closest("[data-param]");if(t){if(t.dataset.storage==="local")R(()=>{const n=t.dataset.param,a=Ce(!0)[n];a!==void 0&&Y(n,a)},50);else if(e.target.matches('input[type="checkbox"]')){const n=t.dataset.param,a=e.target.checked;le(n,a,!1),oe(n,a?1:0)}}}),document.addEventListener("input",e=>{const t=e.target.closest('[data-param][data-storage="local"]');t&&e.target.matches(".text-input")&&Y(t.dataset.param,e.target.value)}),document.addEventListener("select-change",e=>{const{key:t,value:n}=e.detail,a=e.target.closest("[data-param]");a&&(a.dataset.storage==="local"?Y(t,n):(le(t,n,!1),oe(t,n)))})}async function sn(){It(),Ot(),Kt({hudPage:en,hudContent:tn,uiCanvas:nn}),an(),y.init(lt),await lt()}window.addEventListener("beforeunload",rt),window.addEventListener("unload",rt),sn()})(); + `,this._text1.style.fontSize=we(qn)+"px",this._text2.style.fontSize=we(Un)+"px"}return this._text1.textContent=S(t.text1),this._text2.textContent=t.text2?S(t.text2):"",this._text2.style.display=t.text2?"":"none",this._text2.style.marginTop=we(8)+"px",this._overlay.style.display="",!0}_createOverlay(){const i=document.getElementById("hud-page-content");i&&(this._overlay=document.createElement("div"),this._overlay.id="alert-overlay",this._overlay.style.display="none",this._text1=document.createElement("div"),this._text1.style.cssText="color: white; font-family: Arial, sans-serif; font-weight: bold; text-align: center; padding: 0 20px;",this._text2=document.createElement("div"),this._text2.style.cssText="color: white; font-family: Arial, sans-serif; font-weight: bold; text-align: center; padding: 0 20px; margin-top: 8px;",this._overlay.appendChild(this._text1),this._overlay.appendChild(this._text2),i.appendChild(this._overlay))}_getAlert(i){const n=Date.now();if(this.started_time===0&&i.selfdriveState&&(this.started_time=n),!i.selfdriveState)return this.started_time>0&&n-this.started_time>5e3?Gn:null;i.updated&&i.updated.selfdriveState&&(this.last_selfdrive_time=n);const t=i.deviceState;if(t&&["tici","tizi","mici"].includes(t.deviceType)&&this.last_selfdrive_time>0){const r=(n-this.last_selfdrive_time)/1e3;if(r>Gt){const l=i.selfdriveState;return l&&l.enabled&&r-GtY;let Qn=null,_t=null,te=null,_e=null,Ye=0,Je=0,Qe=null,en=[],Ze=[];async function Zn(e){if(Qn=e.hudPage,_t=e.hudContent,te=e.uiCanvas,_e=te.getContext("2d"),ce.init({onStatus:ti,onData:ei,isEnabled:()=>ue.get("ui.hudEnabled")&&!Yt}),await new Promise((t,a)=>{const s=document.createElement("script");s.src="/js/themes/tesla.js",s.onload=()=>t(),s.onerror=()=>a(new Error("Failed to load Tesla theme")),document.head.appendChild(s)}),Y=new TeslaTheme,te&&(te.style.pointerEvents="none"),Y.init&&await Y.init(te,_e),document.fonts?.load)try{await document.fonts.load('64px "Geist Mono"')}catch{}window.animationFrameId=requestAnimationFrame(vt),Qe=Le.subscribe(t=>{tn(t)});const i=t=>{if(Y&&Y.handleClick){const a=te.getBoundingClientRect(),s=t.clientX-a.left,r=t.clientY-a.top;Y.handleClick(s,r)&&t.stopPropagation()}};te.addEventListener("click",i),Ze.push(()=>te.removeEventListener("click",i));const n=()=>{if(document.hidden)bt=Date.now();else if(ue.get("ui.hudEnabled")){const t=bt>0?Date.now()-bt:0;window.animationFrameId&&(cancelAnimationFrame(window.animationFrameId),window.animationFrameId=null),window.animationFrameId=requestAnimationFrame(vt),t>3e3&&(console.log(`Tab was hidden for ${t}ms, forcing data-stream reconnect`),ce.connect())}};document.addEventListener("visibilitychange",n),Ze.push(()=>document.removeEventListener("visibilitychange",n)),tn()}function tn(e){const i=e?.width||_t.getBoundingClientRect().width,n=e?.height||_t.getBoundingClientRect().height,t=window.devicePixelRatio||1;if(i<=0)return;const a=Math.round(i*t),s=Math.round(n*t);te.width===a&&te.height===s||(te.width=a,te.height=s,te.style.width=i+"px",te.style.height=n+"px",_e.setTransform(t,0,0,t,0,0),Ye=i,Je=n,Y&&Y.onResize&&Y.onResize(i,n,e||Le.getState()))}function ei(e,i){const n=Date.now();V[e]=i,V.recv_frame[e]=V.frame,V.recv_time[e]=n/1e3,V.updated[e]=!0,V.valid[e]=i!==null,V.seen[e]=!0,e==="selfdriveState"&&(Zt=n,Jt===0&&(Jt=n,Qt=V.frame)),V.frame++}function ti(e,i){De.updateStatus(e,i)}function nn(){ce.cleanup(),window.animationFrameId&&(cancelAnimationFrame(window.animationFrameId),window.animationFrameId=null),en.forEach(e=>{typeof e=="function"&&e()}),en=[],Ze.forEach(e=>{typeof e=="function"&&e()}),Ze=[],Qe&&(Qe(),Qe=null),Xt.destroy(),De.cleanup(),Y&&Y.destroy&&(Y.destroy(),Y=null)}let Re=null,et=null,tt=null,nt=null;function ni(){ze(et),et=Lt(async()=>{try{const e=await N.init();nt!==null&&e.dp_dev_dashy!==nt&&K.setDashyOn(e.dp_dev_dashy!==!1),nt=e.dp_dev_dashy!==!1,tt!==null&&!!e.isOffroad!==tt&&(K.setOffroad(!!e.isOffroad),e.isOffroad||ce.connect()),tt=!!e.isOffroad}catch{ze(et),et=null,K.onServerFailed()}},5e3)}async function an(){const e=N.getHost();ce.setHost(e),console.log("[Init] Connecting to device at:",e);try{Re=await N.init(),console.log("[Init] Device connected, fetching settings..."),K.onServerConnected(),Re.dp_dev_dashy===!1&&K.setDashyOn(!1),nt=Re.dp_dev_dashy!==!1,tt=!!Re.isOffroad,Re.isOffroad?K.setOffroad(!0):(K.setOffroad(!1),ce.connect()),ni()}catch(i){console.error("[Init] Failed to connect:",i),K.onServerFailed()}}function wi(){Yt=!0,ue.set("ui.hudEnabled",!1);const e=document.getElementById("hud-page");e&&(e.style.display="none")}let on=-1;function vt(){if(!ue.get("ui.hudEnabled")||document.hidden){window.animationFrameId=null;return}if(window.animationFrameId=requestAnimationFrame(vt),V.frame!==on){on=V.frame,_e.save(),_e.clearRect(0,0,Ye,Je),be.frame=V.frame,be.started_frame=Qt,V.selfdriveState&&(be.engaged=V.selfdriveState.enabled||!1,_n(be.engaged),V.selfdriveState.enabled?be.status="ENGAGED":V.selfdriveState.activeOverride?be.status="OVERRIDE":be.status="DISENGAGED"),Y&&(Y.update(V),Y.render(_e,Ye,Je)),De.setLastDataTime(Zt),De.draw(_e,Ye,Je),_e.restore();for(const e in V.updated)V.updated[e]=!1}}const ii={PROFILES_KEY:"dp_lon_accel_profiles",SPEED_CEIL:60,MIN_GAP:.5,MIN_PTS:2,MAX_PTS:12,MAX_ACCEL_CEIL:2,MS_TO_MPH:2.237,MS_TO_KMH:3.6,STOCK_NAME:"Stock",STOCK_MAX_BP:[0,10,25,40],STOCK_MAX_V:[1.6,1.2,.8,.6],_isFiniteNum(e){return typeof e=="number"&&Number.isFinite(e)},applyConfig(e){if(!e||typeof e!="object")return;const i=(t,a)=>{this._isFiniteNum(a)&&(this[t]=a)};i("MAX_PTS",e.max_pts),i("MIN_PTS",e.min_pts),i("MIN_GAP",e.min_gap),i("SPEED_CEIL",e.speed_ceil),i("MAX_ACCEL_CEIL",e.max_accel_ceil);const n=t=>t&&Array.isArray(t.bp)&&Array.isArray(t.v)&&t.bp.length===t.v.length&&t.bp.length>=2&&t.bp.every(this._isFiniteNum)&&t.v.every(this._isFiniteNum);e.stock&&n(e.stock.max)&&(this.STOCK_MAX_BP=e.stock.max.bp.slice(),this.STOCK_MAX_V=e.stock.max.v.slice())},_clamp(e,i,n){return Math.min(Math.max(e,i),n)},validateCurve(e,i){if(!e||typeof e!="object"||Array.isArray(e))return null;const n=e.bp,t=e.v;if(!Array.isArray(n)||!Array.isArray(t)||n.length!==t.length||n.lengththis.MAX_PTS)return null;const a=[];for(let l=0;ll[0]-u[0]);const s=[],r=[];for(const[l,u]of a){const h=this._clamp(l,0,this.SPEED_CEIL);if(s.length&&h-s[s.length-1]=n.speed)return n.value;for(let t=0;t=a.speed&&i<=s.speed){const r=(i-a.speed)/(s.speed-a.speed);return a.value+r*(s.value-a.value)}}return n.value},clampDragged(e,i,n,t,a){const s=i>0?e[i-1].speed+this.MIN_GAP:0,r=i=this.MAX_PTS?!1:this._largestGap(e).size>=2*this.MIN_GAP},_largestGap(e){let i={index:0,size:-1};for(let n=0;ni.size&&(i={index:n,size:t})}return i},addPointAtLargestGap(e){if(e.length>=this.MAX_PTS)return null;const i=this._largestGap(e);if(i.size<2*this.MIN_GAP)return null;const n=e[i.index],t=e[i.index+1],a=(n.speed+t.speed)/2,s=(n.value+t.value)/2,r=e.slice();return r.splice(i.index+1,0,{speed:a,value:s}),{points:r,index:i.index+1}},addPointAtSpeed(e,i,n){if(e.length>=this.MAX_PTS)return null;const t=this._clamp(i,0,this.SPEED_CEIL);let a=e.findIndex(h=>h.speed>t);a===-1&&(a=e.length);const s=a===0||t-e[a-1].speed>=this.MIN_GAP,r=a===e.length||e[a].speed-t>=this.MIN_GAP;if(!s||!r)return null;const l=this._clamp(this.interpValue(e,t),0,n),u=e.slice();return u.splice(a,0,{speed:t,value:l}),{points:u,index:a}},removePoint(e,i){if(e.length<=this.MIN_PTS)return null;const n=e.slice();return n.splice(i,1),n},curveToPoints(e){return!e||!Array.isArray(e.bp)?[]:e.bp.map((i,n)=>({speed:i,value:e.v[n]}))},pointsToCurve(e){return{bp:e.map(i=>i.speed),v:e.map(i=>i.value)}},stockCurve(){return{bp:this.STOCK_MAX_BP.slice(),v:this.STOCK_MAX_V.slice()}},seedDoc(){return{active:this.STOCK_NAME,use_personality:!1,personality_map:{},profiles:[{name:this.STOCK_NAME}]}},parseDoc(e){let i=e;if(typeof e=="string")try{i=JSON.parse(e)}catch{return this.seedDoc()}if(!i||typeof i!="object"||!Array.isArray(i.profiles))return this.seedDoc();const n=new Set,t=[];for(const r of i.profiles){if(!r||typeof r!="object"||typeof r.name!="string"||!r.name||n.has(r.name))continue;n.add(r.name);const l={...r};delete l.turn,l.name===this.STOCK_NAME&&delete l.max,t.push(l)}if(!t.length)return this.seedDoc();const a=new Set(t.map(r=>r.name));return{active:typeof i.active=="string"&&a.has(i.active)?i.active:a.has(this.STOCK_NAME)?this.STOCK_NAME:t[0].name,use_personality:i.use_personality===!0,personality_map:i.personality_map&&typeof i.personality_map=="object"?i.personality_map:{},profiles:t}},serializeDoc(e){const i=(e.profiles||[]).map(a=>{const s={name:a.name};if(a.name===this.STOCK_NAME)return s;if(typeof a.source=="string"&&a.source&&(s.source=a.source),a.max!==void 0){const r=this.validateCurve(a.max,this.MAX_ACCEL_CEIL);r&&(s.max=r)}return s}),n=new Set(i.map(a=>a.name)),t=typeof e.active=="string"&&n.has(e.active)?e.active:n.has(this.STOCK_NAME)?this.STOCK_NAME:i[0]&&i[0].name;return JSON.stringify({active:t,use_personality:e.use_personality===!0,personality_map:e.personality_map||{},profiles:i})},uniqueName(e,i){const n=new Set((e.profiles||[]).map(s=>s.name));let t=1,a=`${i} ${t}`;for(;n.has(a);)t+=1,a=`${i} ${t}`;return a},ensurePersonalityDefaults(e,i){const n={...e.personality_map||{}};return["0","1","2"].forEach(t=>{n[t]||(n[t]=i)}),{...e,personality_map:n}},resolveEffectiveName(e,i,n){if(e&&e.use_personality===!0&&e.personality_map&&typeof e.personality_map=="object"){const t=e.personality_map[String(n)];if(typeof t=="string"&&t)return t}return i},addProfile(e,{seedFrom:i}={}){const n=this.uniqueName(e,i||"Profile"),t=i?e.profiles.find(r=>r.name===i):null,a=t?{name:n,source:i,max:JSON.parse(JSON.stringify(t.max||this.stockCurve()))}:{name:n,max:this.stockCurve()};return{doc:{...e,profiles:e.profiles.concat([a])},name:n}},renameProfile(e,i,n){if(i===this.STOCK_NAME||n===this.STOCK_NAME||typeof n!="string"||!n||e.profiles.some(l=>l.name===n&&l.name!==i))return null;const t=e.profiles.map(l=>{let u=l.name===i?{...l,name:n}:l;return u.source===i&&(u={...u,source:n}),u}),a=e.personality_map||{},s=Object.fromEntries(Object.entries(a).map(([l,u])=>[l,u===i?n:u])),r=e.active===i?n:e.active;return{...e,profiles:t,personality_map:s,active:r}},deleteProfile(e,i){if(i===this.STOCK_NAME)return{doc:e,activeFallback:i};let n=e.profiles.filter(l=>l.name!==i);if(!n.length){const l=this.seedDoc(),u=e.personality_map||{},h=new Set(l.profiles.map(m=>m.name)),f=Object.fromEntries(Object.entries(u).filter(([,m])=>h.has(m)));return{doc:{...e,profiles:l.profiles,personality_map:f,active:"Stock"},activeFallback:"Stock"}}n=n.map(l=>l.source===i?{...l,source:void 0}:l);const t=e.personality_map||{},a=new Set(n.map(l=>l.name)),s=Object.fromEntries(Object.entries(t).filter(([,l])=>a.has(l))),r=a.has(e.active)?e.active:n[0].name;return{doc:{...e,profiles:n,personality_map:s,active:r},activeFallback:r}}};window.AccelEqModel=ii;const b={PALETTE:{bezel:"#0E1518",field:"#131A1D",gridMinor:"rgba(160,190,200,0.06)",gridMajor:"rgba(160,190,200,0.17)",axis:"rgba(160,190,200,0.22)",labelMajor:"rgba(221,230,232,0.62)",labelMinor:"rgba(221,230,232,0.34)",signalHot:"#FF6A3D",signalWarm:"#FFB23E",fillTop:"rgba(255,106,61,0.32)",fillBot:"rgba(255,178,62,0.03)",glow:"rgba(255,138,70,0.55)",habit:"rgba(96,178,205,0.18)",habitInk:"rgba(120,195,215,0.9)",stock:"rgba(150,180,190,0.22)",sel:"#FFFFFF",kill:"#FF5A5F",text:"#DDE6E8"},PAD:{left:44,right:16,top:16,bottom:34},GRAB_RADIUS_PX:30,LINE_GRAB_PX:20,ADD_DRAG_THRESHOLD_PX:6,KILL_MARGIN_PX:12,AXIS_MAX_MS:40,create(e){const i=window.AccelEqModel,n=b.PALETTE,t={ceil:e.ceil,stock:e.stock?i.curveToPoints(e.stock):null,refLabel:e.refLabel||"Stock",points:(e.points||[]).map(c=>({...c})),onChange:e.onChange||(()=>{}),onUnitChange:e.onUnitChange||(()=>{}),unit:e.unit||"kmh",readOnly:e.readOnly===!0,dragIndex:-1,selIndex:-1,pendingAdd:null,killArmed:!1,tapCandidate:null,habit:null,habitBands:null},a=d("div","accel-eq-editor"),s=d("canvas","w-full rounded-lg touch-none",{});s.style.background=n.bezel;const r=d("div","px-4 sm:px-10 lg:px-20");r.appendChild(s),a.appendChild(r);const l=d("div","flex items-center justify-between gap-2 mt-2"),u=d("div","text-xs text-base-content/50 hud-mono",{textContent:""});let h=null,f=null;t.readOnly||(h=d("button","btn btn-xs btn-ghost",{title:"Undo"}),h.innerHTML=ie.render("undo",14,"inline-block"),h.addEventListener("click",()=>bi()),f=d("button","btn btn-xs btn-ghost",{title:"Redo"}),f.innerHTML=ie.render("redo",14,"inline-block"),f.addEventListener("click",()=>_i()));const m=d("div","flex items-center gap-1");h&&m.append(h,f),m.append(u);const g=d("div","flex items-center gap-2");if(e.showHabitToggle&&e.onHabitToggle){const c=d("label","flex items-center gap-1.5 cursor-pointer");c.append(d("span","text-xs text-base-content/50 hud-mono",{textContent:"log"}));const p=d("input","toggle toggle-xs toggle-primary",{type:"checkbox"});p.checked=e.habitOn===!0,p.addEventListener("change",()=>e.onHabitToggle(p.checked)),c.append(p),g.append(c)}l.innerHTML="",l.append(m,g),a.appendChild(l);const w=[1,5],k=[.05,.1,.5];let j=0,P=0,E=[],_=-1;const R=d("div","flex flex-col gap-3 mt-2 hidden");function H(c,p){const C=d("div","flex items-center justify-between gap-3"),y=d("div","flex-1");y.appendChild(d("p","font-medium text-base",{textContent:c})),p&&y.appendChild(d("p","text-sm text-base-content/50 mt-0.5",{textContent:p}));const T=d("div","join bg-base-300/50 rounded-lg shrink-0"),M=d("button","btn btn-ghost btn-md join-item text-lg min-w-12 min-h-12",{textContent:"\u2212"}),O=d("button","btn btn-md join-item min-w-20 min-h-12 bg-base-100/50 border-none font-medium hud-mono",{}),D=d("button","btn btn-ghost btn-md join-item text-lg min-w-12 min-h-12",{textContent:"+"});return T.append(M,O,D),C.append(y,T),{row:C,minus:M,chip:O,plus:D}}const U=d("div","text-base hud-mono font-medium text-base-content/90",{textContent:""}),z=H("Speed","Where this point sits on the speed axis."),X=H(e.valueLabel||"Limit",e.valueDesc||""),ae=d("div","flex gap-2"),G=d("button","btn btn-sm btn-error flex-1",{textContent:"Remove"}),v=d("button","btn btn-sm btn-ghost flex-1",{textContent:`Reset to ${e.refLabel||"Stock"}`});ae.append(G,v),R.append(U,z.row,X.row,ae),a.appendChild(R);const o=s.getContext("2d");let L=300,B=190;function x(c){return Math.round(c*20)/20}const A=()=>L-b.PAD.left-b.PAD.right,F=()=>B-b.PAD.top-b.PAD.bottom;function I(c){return b.PAD.left+c/b.AXIS_MAX_MS*A()}function $(c){return b.PAD.top+(1-c/t.ceil)*F()}function q(c){return(c-b.PAD.left)/A()*b.AXIS_MAX_MS}function J(c){return(1-(c-b.PAD.top)/F())*t.ceil}function se(){return t.unit==="mph"?{major:10,minor:5}:t.unit==="kmh"?{major:10,minor:5}:{major:5,minor:5}}function re(){const c=t.habit,p=t.habitBands,C=p&&p.upper&&p.upper.length>=2;if(!((!c||!c.length)&&!C)){if(o.save(),c&&c.length){o.fillStyle=n.habit;for(let y=0;yb.AXIS_MAX_MS||M<0||M>t.ceil||o.fillRect(I(T)-1,$(M)-1,2,2)}}if(C){const y=D=>D.map(([ne,ke])=>({speed:ne,value:ke}));if(p.lower&&p.lower.length){o.beginPath(),p.upper.forEach((D,ne)=>{const ke=I(D[0]),le=$(D[1]);ne?o.lineTo(ke,le):o.moveTo(ke,le)});for(let D=p.lower.length-1;D>=0;D--)o.lineTo(I(p.lower[D][0]),$(p.lower[D][1]));o.closePath(),o.fillStyle="rgba(96,178,205,0.07)",o.fill()}o.lineJoin="round";const T=(D,ne)=>{!D||D.length<2||(o.strokeStyle=`rgba(120,195,215,${ne})`,o.lineWidth=2,Te(y(D)))};T(p.lower,.4),T(p.mid,.7),T(p.upper,.95);let M=b.PAD.top+8+(t.stock?16:0);const O=b.PAD.left+8;o.font='10px "Geist Mono", monospace',o.textAlign="left",o.textBaseline="middle",[["hardest",.95],["brisk",.7],["usual",.4]].forEach(([D,ne])=>{o.strokeStyle=`rgba(120,195,215,${ne})`,o.lineWidth=2,o.beginPath(),o.moveTo(O,M),o.lineTo(O+18,M),o.stroke(),o.fillStyle=`rgba(120,195,215,${Math.min(1,ne+.15)})`,o.fillText(D,O+24,M),M+=14})}o.restore()}}function Z(){o.clearRect(0,0,L,B),o.fillStyle=n.bezel,o.fillRect(0,0,L,B),o.fillStyle=n.field,o.fillRect(b.PAD.left,b.PAD.top,A(),F()),Be(),ve(),Ce(),Ee(t.points),t.stock&&(o.strokeStyle=n.stock,o.setLineDash([4,4]),o.lineWidth=1.5,Te(t.stock),o.setLineDash([]),He()),re(),o.save(),o.shadowColor=n.glow,o.shadowBlur=8,o.strokeStyle=oe(),o.lineWidth=2.5,o.lineJoin="round",o.lineCap="round",Te(t.points),o.restore();for(let c=0;c=0){const c=t.points[t.dragIndex];o.beginPath(),o.arc(I(c.speed),$(c.value),6,0,Math.PI*2),o.strokeStyle=n.signalWarm,o.globalAlpha=.7,o.lineWidth=1.5,o.stroke(),o.globalAlpha=1}it()}function oe(){const c=o.createLinearGradient(0,b.PAD.top,0,B-b.PAD.bottom);return c.addColorStop(0,n.signalHot),c.addColorStop(1,n.signalWarm),c}function Ee(c){if(!c.length)return;const p=B-b.PAD.bottom,C=o.createLinearGradient(0,b.PAD.top,0,p);C.addColorStop(0,n.fillTop),C.addColorStop(1,n.fillBot),o.beginPath(),o.moveTo(I(c[0].speed),p);for(let y=0;y=0?t.dragIndex:t.selIndex;if(c<0){U.textContent="";return}const p=t.points[c],C=i.speedToDisplay(p.speed,t.unit);U.textContent=`${C.toFixed(1)} ${i.speedUnitLabel(t.unit)} \u2192 ${p.value.toFixed(2)} m/s\xB2`}function di(c){let p=-1,C=b.GRAB_RADIUS_PX*b.GRAB_RADIUS_PX;for(let y=0;y=b.PAD.left&&c.x<=L-b.PAD.right&&c.y>=b.PAD.top&&c.y<=B-b.PAD.bottom}function pi(c){const p=t.points;if(!p.length)return 0;if(c<=p[0].speed)return p[0].value;const C=p[p.length-1];if(c>=C.speed)return C.value;for(let y=0;y=T.speed&&c<=M.speed){const O=(c-T.speed)/(M.speed-T.speed||1);return T.value+O*(M.value-T.value)}}return C.value}function hi(c){const p=$(pi(q(c.x)));return Math.abs(c.y-p)<=b.LINE_GRAB_PX}function fi(c){let p=-1,C=1/0;for(let y=0;y=0){t.dragIndex=C,t.selIndex=C,t.pendingAdd=null,t.tapCandidate=null,de(),Z(),c.preventDefault();return}if(ui(p)){if(hi(p)){t.pendingAdd={x0:p.x,y0:p.y,added:!1},c.preventDefault();return}t.tapCandidate={x:p.x,y:p.y},c.preventDefault()}}function xt(c){const p=sn(c);if(t.tapCandidate){const M=p.x-t.tapCandidate.x,O=p.y-t.tapCandidate.y;M*M+O*O>=b.ADD_DRAG_THRESHOLD_PX*b.ADD_DRAG_THRESHOLD_PX&&(t.tapCandidate=null)}if(t.pendingAdd&&!t.pendingAdd.added){const M=p.x-t.pendingAdd.x0,O=p.y-t.pendingAdd.y0;if(M*M+O*O>=b.ADD_DRAG_THRESHOLD_PX*b.ADD_DRAG_THRESHOLD_PX){const D=i.addPointAtSpeed(t.points,q(t.pendingAdd.x0),t.ceil);if(D)t.points=D.points,t.dragIndex=D.index,t.selIndex=D.index,t.points[D.index].value=x(t.points[D.index].value),t.pendingAdd.added=!0;else{vi(t.points.length>=i.MAX_PTS?`Max ${i.MAX_PTS} points`:"No room here"),t.pendingAdd=null;return}}else return}if(t.dragIndex<0)return;const C=p.yB-b.PAD.bottom+b.KILL_MARGIN_PX;t.killArmed=C&&t.points.length>i.MIN_PTS;const y=i.clampDragged(t.points,t.dragIndex,q(p.x),J(p.y),t.ceil),T=Math.min(y.speed,b.AXIS_MAX_MS);t.points[t.dragIndex]={speed:T,value:x(y.value)},de(),Z(),c.preventDefault()}function wt(){const c=t.dragIndex>=0,p=t.dragIndex,C=t.killArmed,y=t.tapCandidate;if(t.dragIndex=-1,t.pendingAdd=null,t.killArmed=!1,t.tapCandidate=null,c){if(C&&p>=0){const T=i.removePoint(t.points,p);T&&(t.points=T,t.selIndex=-1,de())}ot()}else if(y){const T=fi(y);T>=0&&(t.selIndex=T,de())}de(),Z()}function ln(){return i.displayToSpeed(w[j],t.unit)}function Ct(){z.chip.textContent=`${w[j]} ${i.speedUnitLabel(t.unit)}`,X.chip.textContent=`${k[P]} m/s\xB2`}function de(){if(ci(),Ct(),t.selIndex<0&&t.dragIndex<0){R.classList.add("hidden");return}R.classList.remove("hidden"),G.classList.toggle("btn-disabled",t.points.length<=i.MIN_PTS)}function at(c,p){if(t.selIndex<0)return;const C=t.points[t.selIndex],y=i.clampDragged(t.points,t.selIndex,C.speed+c,C.value+p,t.ceil);t.points[t.selIndex]={speed:Math.min(y.speed,b.AXIS_MAX_MS),value:x(y.value)},de(),Z(),ot()}function cn(){return t.points.map(c=>({...c}))}function mi(c,p){return c.length===p.length&&c.every((C,y)=>C.speed===p[y].speed&&C.value===p[y].value)}function St(){h&&(h.classList.toggle("btn-disabled",_<=0),f.classList.toggle("btn-disabled",_>=E.length-1))}function gi(){const c=cn();_>=0&&mi(c,E[_])||(E=E.slice(0,_+1),E.push(c),E.length>50&&(E=E.slice(E.length-50)),_=E.length-1,St())}function ot(){gi(),t.onChange(Tt())}function dn(){t.points=E[_].map(c=>({...c})),t.selIndex=-1,t.dragIndex=-1,de(),Z(),St(),t.onChange(Tt())}function bi(){_>0&&(_--,dn())}function _i(){_at(-ln(),0)),z.plus.addEventListener("click",()=>at(ln(),0)),z.chip.addEventListener("click",()=>{j=(j+1)%w.length,Ct()}),X.minus.addEventListener("click",()=>at(0,-k[P])),X.plus.addEventListener("click",()=>at(0,k[P])),X.chip.addEventListener("click",()=>{P=(P+1)%k.length,Ct()}),G.addEventListener("click",()=>{if(t.selIndex<0)return;const c=i.removePoint(t.points,t.selIndex);c&&(t.points=c,t.selIndex=-1,de(),Z(),ot())}),v.addEventListener("click",()=>{t.stock&&(t.points=t.stock.map(c=>({...c})),t.selIndex=-1,de(),Z(),ot())});let Ne=null;function vi(c){u.textContent=c,Ne&&clearTimeout(Ne),Ne=ee(()=>{u.textContent=""},1500)}s.addEventListener("mousedown",rn),window.addEventListener("mousemove",xt),window.addEventListener("mouseup",wt),s.addEventListener("touchstart",rn,{passive:!1}),s.addEventListener("touchmove",xt,{passive:!1}),s.addEventListener("touchend",wt);function Et(){const c=s.getBoundingClientRect();L=Math.max(280,Math.round(c.width)),B=Math.round(Math.min(Math.max(L*.42,200),420)),s.width=L,s.height=B,s.style.height=`${B}px`,Z()}function Tt(){return t.points.map(c=>({...c}))}const yi={element:a,setPoints(c){t.points=c.map(p=>({...p})),t.selIndex=-1,de(),Z()},getPoints:Tt,setCeil(c){t.ceil=c,Z()},setStockGhost(c){t.stock=c?i.curveToPoints(c):null,Z()},setHabit(c){t.habit=c&&Array.isArray(c.points)?c.points:Array.isArray(c)?c:null;const p=c&&c.envelope;t.habitBands=p&&typeof p=="object"&&!Array.isArray(p)?p:null,Z()},setUnit(c){t.unit=c,de(),Z()},destroy(){window.removeEventListener("mousemove",xt),window.removeEventListener("mouseup",wt),window.removeEventListener("resize",Et),Ne&&clearTimeout(Ne)}};return E=[cn()],_=0,St(),ee(Et,0),window.addEventListener("resize",Et),yi}};window.AccelEqEditor=b;const yt={SAVE_DEBOUNCE_MS:400,render(e){const i=window.AccelEqModel,n={doc:i.seedDoc(),active:"Stock",editingName:"Stock",editor:null,saveTimer:null,personality:1,persisted:!1,unit:"kmh",habit:void 0,habitCount:0,habitBands:0,showHabit:(()=>{try{return localStorage.getItem("dashy.accelHabit")==="1"}catch{return!1}})()},t=15;e.innerHTML="";const a=d("div","w-full");e.appendChild(a);const s=d("div","hud-section-title mb-3 flex items-center gap-1.5 px-1");s.innerHTML=`${ie.render("sliders",14,"inline-block")} ACCELERATION EQ`,a.appendChild(s);const r=d("div","card bg-base-200 p-3 mb-3");a.appendChild(r);const l=d("div","card bg-base-200 p-3 mb-3");a.appendChild(l);const u=d("div","card bg-base-200 p-3 mb-3");a.appendChild(u);function h(){return Number.isFinite(n.personality)?n.personality:1}async function f(){if(!n.personalityUnavailable)try{const x=await N.getParam("LongitudinalPersonality"),A=x&&x.value,F=typeof A=="string"?parseInt(A,10):A;Number.isFinite(F)&&F!==n.personality&&(n.personality=F,n.doc.use_personality===!0&&z())}catch{n.personalityUnavailable=!0,console.warn("[AccelEq] LongitudinalPersonality unavailable; personality polling disabled")}}async function m(){try{const x=await N.getParam("IsMetric"),A=x&&x.value,F=A===!0||A===1||A==="1"||A==="true";n.unit=F?"kmh":"mph"}catch{n.unit="mph"}}function g(){return i.resolveEffectiveName(n.doc,n.active,h())}function w(x){return n.doc.profiles.find(A=>A.name===x)||n.doc.profiles[0]}function k(){return n.doc.profiles.some(x=>x.name===i.STOCK_NAME)?i.STOCK_NAME:n.doc.profiles[0].name}function j(x){const A=i.addProfile(n.doc,{seedFrom:x});n.doc=A.doc,n.editingName=A.name,n.doc.use_personality!==!0&&(n.active=A.name,_(A.name)),E(),G()}function P(){Pe({title:"Duplicate from\u2026",options:n.doc.profiles.map(x=>({value:x.name,label:x.name})),currentValue:n.editingName,onSelect:x=>j(x)})}function E(){n.saveTimer&&clearTimeout(n.saveTimer),n.saveTimer=ee(async()=>{n.saveTimer=null;try{await N.saveParam(i.PROFILES_KEY,i.serializeDoc(n.doc)),n.persisted=!0,U("Saved","text-success")}catch(x){console.error("[AccelEq] profiles save failed:",x),U("Save failed \u2014 retry")}},yt.SAVE_DEBOUNCE_MS)}function _(x){n.doc={...n.doc,active:x},E()}let R=null,H=null;function U(x,A){R||(R=d("div","text-xs mt-1"),a.appendChild(R)),H&&clearTimeout(H),R.className=`text-xs mt-1 ${A||"text-error"}`,R.textContent=x,H=ee(()=>{R&&(R.textContent="")},A?1200:3e3)}function z(){if(l.innerHTML="",n.doc.use_personality===!0){l.classList.add("hidden");return}l.classList.remove("hidden");const x=g(),A=d("div","flex items-center gap-3");A.appendChild(d("p","font-medium text-base flex-1",{textContent:"Accel Profile"}));const F=d("div","flex items-center gap-2 overflow-x-auto min-w-0");F.style.scrollbarWidth="none";let I=null;n.doc.profiles.forEach(q=>{const J=q.name===x,se=q.name===n.editingName,re=d("button","btn btn-sm hud-mono shrink-0 "+(J?"btn-primary":se?"btn-outline":"btn-ghost"),{textContent:q.name});re.addEventListener("click",()=>{n.editingName=q.name,n.active=q.name,_(q.name),G()}),se&&(I=re),F.appendChild(re)});const $=d("button","btn btn-sm btn-ghost border border-base-content/15 shrink-0 gap-1",{title:"Duplicate a profile"});$.innerHTML='+ Duplicate',$.addEventListener("click",P),A.append(F,$),l.appendChild(A),l.appendChild(d("p","text-sm text-base-content/50 mt-0.5",{textContent:"Tap a profile to use it \u2014 then tune its curve below. + Duplicate copies one."})),I&&ee(()=>{F.scrollLeft=I.offsetLeft-F.clientWidth/2+I.clientWidth/2},0)}function X(){if(n.editor&&(n.editor.destroy(),n.editor=null),u.innerHTML="",n.doc.use_personality===!0){u.classList.add("hidden");return}u.classList.remove("hidden");const x=n.editingName===i.STOCK_NAME,A=i.MAX_ACCEL_CEIL,F=w(n.editingName).source,I=F&&F!==n.editingName?n.doc.profiles.find(oe=>oe.name===F):null,$=I&&i.validateCurve(I.max||i.stockCurve(),A)||i.stockCurve(),q=I?F:"Stock",J=d("div","flex items-center justify-between gap-2 mt-3");if(x){const oe=d("div","text-xs text-base-content/50 flex items-center gap-1.5");oe.innerHTML=`${ie.render("lock",12,"inline-block")} Editing: Stock \u2014 the baseline reference. Duplicate it to make changes.`,J.appendChild(oe)}else{const oe=`Editing: ${n.editingName}${I?` \u2014 copied from ${F}`:""}`;J.appendChild(d("div","text-xs text-base-content/50",{textContent:oe}));const Ee=d("div","flex gap-1 shrink-0"),Te=d("button","btn btn-xs btn-ghost",{textContent:"Rename"});Te.addEventListener("click",()=>{const ve=n.editingName;Cn({title:"Rename profile",value:ve,placeholder:"Profile name",confirmLabel:"Rename",onConfirm:Be=>{const Ce=(Be||"").trim();if(!Ce)return"Name can\u2019t be empty";const it=i.renameProfile(n.doc,ve,Ce);if(!it)return"Name must be unique (and not \u201CStock\u201D)";n.active===ve&&(n.active=Ce,_(Ce)),n.editingName=Ce,n.doc=it,E(),G()}})});const He=d("button","btn btn-xs btn-ghost text-error",{textContent:"Delete"});He.addEventListener("click",()=>{const ve=n.editingName,Be=i.deleteProfile(n.doc,ve);n.doc=Be.doc,n.editingName=k(),n.active===ve&&(n.active=n.editingName,_(n.editingName)),E(),G()}),He.classList.toggle("btn-disabled",n.doc.profiles.length<=1),Ee.append(Te,He),J.appendChild(Ee)}const re=w(n.editingName).max||i.stockCurve(),Z=i.curveToPoints(i.validateCurve(re,A)||i.stockCurve());n.editor=b.create({ceil:A,stock:$,refLabel:q,points:Z,unit:n.unit,valueLabel:"Max acceleration",valueDesc:"Acceleration limit allowed at this speed (m/s\xB2).",readOnly:x,onChange:oe=>{const Ee=w(n.editingName);Ee.max=i.pointsToCurve(oe),E()},showHabitToggle:n.habitBands>=t,habitOn:n.showHabit,onHabitToggle:oe=>{n.showHabit=oe;try{localStorage.setItem("dashy.accelHabit",oe?"1":"0")}catch{}L()}}),u.appendChild(n.editor.element),u.appendChild(J),n.habitBands>=t&&L()}function ae(){r.innerHTML="";const x=d("div","divide-y divide-base-content/10");function A(I,$,q){const J=d("div","py-3 first:pt-0 last:pb-0"),se=d("div","flex justify-between items-center gap-4");return se.appendChild(d("p","font-medium flex-1 text-base",{textContent:I})),q&&(q.classList.add("shrink-0"),se.appendChild(q)),J.appendChild(se),$&&J.appendChild(d("p","text-sm text-base-content/50 mt-0.5",{textContent:$})),x.appendChild(J),J}const F=d("input","toggle toggle-primary toggle-lg",{type:"checkbox"});if(F.checked=n.doc.use_personality===!0,F.addEventListener("change",()=>{let I={...n.doc,use_personality:F.checked};F.checked&&(I=i.ensurePersonalityDefaults(I,k())),n.doc=I,E(),G()}),A("Personality Accel Profile","Let the openpilot personality button pick the active profile. It also controls follow distance.",F),n.doc.use_personality===!0){const I=n.doc.personality_map||{};[["0","Aggressive"],["1","Standard"],["2","Relaxed"]].forEach(([$,q])=>{const J=d("button","btn btn-sm btn-ghost border border-base-content/10 min-w-28 justify-between"),se=d("span","hud-mono",{textContent:I[$]||"\u2014"});J.append(se,d("span","text-base-content/50",{textContent:"\u25BE"})),J.addEventListener("click",()=>{Pe({title:`${q} \u2192 profile`,options:n.doc.profiles.map(re=>({value:re.name,label:re.name})),currentValue:I[$]||"",onSelect:re=>{n.doc.personality_map={...n.doc.personality_map||{},[$]:re},E(),ae(),z()}})}),A(q,null,J)})}r.appendChild(x)}function G(){z(),X(),ae()}async function v(){try{const x=await N.get("/api/accel_eq/config");x&&!x.error&&i.applyConfig(x)}catch{}}async function o(){try{const x=await N.get("/api/accel_eq/habit");n.habit=x&&Array.isArray(x.points)?x:null}catch{n.habit=null}}async function L(){if(n.editor){if(!n.showHabit){n.editor.setHabit(null);return}n.habit===void 0&&await o(),n.editor&&n.editor.setHabit(n.habit||null)}}async function B(){try{const x=await N.get("/api/accel_eq/habit?meta=1");n.habitCount=x&&typeof x.count=="number"?x.count:0,n.habitBands=x&&typeof x.bands=="number"?x.bands:0}catch{n.habitCount=0}}return(async()=>{await v(),n.doc=i.seedDoc();try{const x=await N.getParam(i.PROFILES_KEY),A=x&&x.value;A&&A!=="false"&&(n.doc=i.parseDoc(A),n.persisted=!0),n.active=n.doc.active,n.editingName=n.active}catch(x){console.warn("[AccelEq] load failed, using seed:",x)}await Promise.all([f(),m(),B()]),G()})(),{destroy(){n.saveTimer&&(clearTimeout(n.saveTimer),n.saveTimer=null,N.saveParam(i.PROFILES_KEY,i.serializeDoc(n.doc)).catch(()=>{})),n.editor&&n.editor.destroy()}}}};window.AccelEqPage=yt;const ai=document.getElementById("hud-page"),oi=document.getElementById("hud-page-content"),si=document.getElementById("uiCanvas");function ri(){document.addEventListener("click",e=>{if(e.target.matches(".join button")&&e.target.parentElement.querySelector(".stepper-value")){if(e.target.disabled){e.preventDefault();return}const i=e.target.parentElement,n=i.querySelector(".stepper-value"),t=parseFloat(e.target.dataset.step),a=i.dataset.min!==void 0?parseFloat(i.dataset.min):-1/0,s=i.dataset.max!==void 0?parseFloat(i.dataset.max):1/0;if(n.dataset.isTextSpin==="true"){const u=JSON.parse(i.dataset.textOptions||"[]");let h=parseInt(n.dataset.actualValue||0);if(t<0&&h<=0||t>0&&h>=u.length-1)return;let f=h+t;if(f=Math.max(0,Math.min(u.length-1,f)),f===h)return;n.dataset.actualValue=f,n.textContent=u[f],ft(i,f,0,u.length-1),Ue(i.dataset.param,f);const m=i.dataset.param;m&&!i.dataset.storage&&Ge(m,f,!0)}else{const u=n.dataset.actualValue;if(u===void 0||u===""){console.error("actualValue not set for numeric spinner");return}let h=parseFloat(u);if(isNaN(h)){console.error("Invalid numeric value:",u);return}if(t<0&&h<=a||t>0&&h>=s)return;let f=h+t;if(String(t).includes(".")&&(f=parseFloat(f.toFixed(2))),f=Math.max(a,Math.min(s,f)),f===h)return;n.dataset.actualValue=f;const m=n.dataset.suffix||"",g=n.dataset.specialValueText||"",w=n.dataset.minVal!==""?parseFloat(n.dataset.minVal):a;g&&f===w?n.textContent=g:m?String(t).includes(".")?n.textContent=f.toFixed(1)+m:n.textContent=f+m:String(t).includes(".")?n.textContent=f.toFixed(1):n.textContent=f,ft(i,f,a,s),Ue(i.dataset.param,f);const k=i.dataset.param;k&&!i.dataset.storage&&Ge(k,f,!0)}const l=e.target.closest("[data-param]");l&&l.dataset.storage==="local"&&ee(()=>{const u=l.dataset.param,h=mt(!0)[u];h!==void 0&&Ie(u,h)},50);return}if(e.target.matches(".join button")&&!e.target.parentElement.querySelector(".stepper-value")){e.target.parentElement.querySelectorAll("button").forEach(t=>t.classList.remove("btn-primary")),e.target.classList.add("btn-primary");const n=e.target.closest("[data-param]");n&&n.dataset.storage==="local"&&ee(()=>{const t=n.dataset.param,a=mt(!0)[t];a!==void 0&&Ie(t,a)},50);return}if(e.target.matches("#files-table a, #files-breadcrumbs a, #files-breadcrumbs ul a")){if(e.target.href&&e.target.href.includes("/api/play"))return;const i=e.target.dataset.path;i!==void 0&&(e.preventDefault(),Ke(i));return}}),document.addEventListener("change",e=>{const i=e.target.closest("[data-param]");if(i){if(i.dataset.storage==="local")ee(()=>{const n=i.dataset.param,t=mt(!0)[n];t!==void 0&&Ie(n,t)},50);else if(e.target.matches('input[type="checkbox"]')){const n=i.dataset.param,t=e.target.checked;Ge(n,t,!1),Ue(n,t?1:0)}}}),document.addEventListener("input",e=>{const i=e.target.closest('[data-param][data-storage="local"]');i&&e.target.matches(".text-input")&&Ie(i.dataset.param,e.target.value)}),document.addEventListener("select-change",e=>{const{key:i,value:n}=e.detail,t=e.target.closest("[data-param]");t&&(t.dataset.storage==="local"?Ie(i,n):(Ge(i,n,!1),Ue(i,n)))})}async function li(){Dn(),In(),Zn({hudPage:ai,hudContent:oi,uiCanvas:si}),ri(),$e.init(),K.init(an),await an()}window.addEventListener("beforeunload",nn),window.addEventListener("unload",nn),li()})(); diff --git a/dragonpilot/selfdrive/controls/lib/accel_eq.py b/dragonpilot/selfdrive/controls/lib/accel_eq.py new file mode 100644 index 000000000..df5876108 --- /dev/null +++ b/dragonpilot/selfdrive/controls/lib/accel_eq.py @@ -0,0 +1,157 @@ +""" +Copyright (c) 2026, Rick Lan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, and/or sublicense, +for non-commercial purposes only, subject to the following conditions: + +- The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. +- Commercial use (e.g. use in a product, service, or activity intended to + generate revenue) is prohibited without explicit written permission from + the copyright holder. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Acceleration EQ — planner-side reader. Loads the active profile's max-accel +curve from the dp_lon_accel_profiles param (validated, with a stock fallback) +and exposes it to the longitudinal planner. The editor UI lives in dashy. +""" + +import math +import os + +import numpy as np + +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog +from opendbc.car.interfaces import ACCEL_MAX + +PROFILES_KEY = "dp_lon_accel_profiles" +STOCK_NAME = "Stock" # the protected default — a live mirror of injected stock, never a stored curve + +SPEED_CEIL = 60.0 +MIN_GAP = 0.5 +MIN_PTS = 2 +MAX_PTS = 12 # our own sanity bound (np.interp has no limit); must match dashy's MAX_PTS +MAX_ACCEL_CEIL = ACCEL_MAX + + +def _validate_curve(curve, ceil): + """Return a sorted, clamped (bp, v) tuple, or None if unusable.""" + if not isinstance(curve, dict): + return None + bp, v = curve.get("bp"), curve.get("v") + if not isinstance(bp, list) or not isinstance(v, list): + return None + if len(bp) != len(v) or not (MIN_PTS <= len(bp) <= MAX_PTS): + return None + try: + pairs = sorted(((float(b), float(a)) for b, a in zip(bp, v, strict=True)), key=lambda p: p[0]) + except (TypeError, ValueError): + return None + if not all(math.isfinite(b) and math.isfinite(a) for b, a in pairs): + return None + out_bp, out_v = [], [] + for b, a in pairs: + b = min(max(b, 0.0), SPEED_CEIL) + if out_bp and b - out_bp[-1] < MIN_GAP: + return None + out_bp.append(b) + out_v.append(min(max(a, 0.0), ceil)) + return out_bp, out_v + + +def _select_active(data, name): + """The profile with exactly this name, or None (caller falls back to stock). + No first-profile fallback — an unknown name means 'use stock'.""" + if not isinstance(data, dict) or not name: + return None + profiles = data.get("profiles") + if not isinstance(profiles, list): + return None + return next((p for p in profiles + if isinstance(p, dict) and p.get("name") == name), None) + + +def _resolve_active_name(data, personality): + """Profile name to use: when use_personality is on, the personality-mapped name + (None if unmapped → stock); otherwise the manual 'active' selection (None if + unset → stock). The caller treats None / no match as 'use stock'.""" + if not isinstance(data, dict): + return None + if data.get("use_personality"): + pmap = data.get("personality_map") + name = pmap.get(str(personality)) if isinstance(pmap, dict) else None + else: + name = data.get("active") + return name if isinstance(name, str) and name else None + + +class AccelEq: + def __init__(self, stock_bp, stock_v, params=None): + # Stock fallback is injected by the planner (its A_CRUISE_MAX_* table), so + # there's a single source of truth and accel_eq stays a leaf — it never + # imports the planner (which imports AccelEq → would be circular). + self._stock_bp, self._stock_v = list(stock_bp), list(stock_v) + self._params = params if params is not None else Params() + self._personality = -1 # unknown sentinel → the first maybe_refresh() with a real value (0/1/2) forces a resolve + self._doc = None # cached parsed profiles JSON (re-read only on mtime change) + self._max_bp, self._max_v = list(stock_bp), list(stock_v) + self._reload_doc() + self._last_mtime = self._mtime(PROFILES_KEY) + + def _mtime(self, key): + try: + return os.stat(self._params.get_param_path(key)).st_mtime + except OSError: + return None + + def maybe_refresh(self, personality): + # The JSON read is the only expensive part, so gate it on the param mtime + # and cache the parsed doc. A personality change (button press) doesn't + # touch the param at all — just re-resolve the active curve from the cache. + mtime = self._mtime(PROFILES_KEY) + if mtime != self._last_mtime: + self._last_mtime = mtime + self._personality = personality + self._reload_doc() # JSON changed → re-read + re-resolve + elif personality != self._personality: + self._personality = personality + self._resolve() # selection changed → re-resolve cache, no I/O + + def _reload_doc(self): + # Read + parse + cache the profiles JSON (the expensive step), then resolve. + try: + self._doc = self._params.get(PROFILES_KEY) # parsed dict, or None + except Exception as e: # never let tuning data crash the planner + cloudlog.warning(f"AccelEq: failed to read profiles, using stock: {e}") + self._doc = None + self._resolve() + + def _resolve(self): + # Pick the active profile's curve from the cached doc. Pure in-memory — no + # param access, so it's cheap to call on every personality change. + max_bp, max_v = list(self._stock_bp), list(self._stock_v) + try: + data = self._doc + if data: + name = _resolve_active_name(data, self._personality) + # Anything unresolved — unmapped personality, unset/empty active, no + # matching profile, or the Stock baseline itself — falls through to the + # injected stock. Stock is a live mirror of injected stock, so its stored + # curve is ignored (a planner stock change always takes effect). + if name and name != STOCK_NAME: + prof = _select_active(data, name) + if prof is not None: + mc = _validate_curve(prof.get("max"), MAX_ACCEL_CEIL) + if mc is not None: + max_bp, max_v = mc + except Exception as e: # never let tuning data crash the planner + cloudlog.warning(f"AccelEq: failed to resolve profile, using stock: {e}") + self._max_bp, self._max_v = max_bp, max_v + + def max_accel(self, v_ego): + return float(np.interp(v_ego, self._max_bp, self._max_v)) diff --git a/dragonpilot/selfdrive/controls/lib/accel_logger.py b/dragonpilot/selfdrive/controls/lib/accel_logger.py new file mode 100644 index 000000000..bb281f5d7 --- /dev/null +++ b/dragonpilot/selfdrive/controls/lib/accel_logger.py @@ -0,0 +1,94 @@ +""" +Copyright (c) 2026, Rick Lan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, and/or sublicense, +for non-commercial purposes only, subject to the following conditions: + +- The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. +- Commercial use (e.g. use in a product, service, or activity intended to + generate revenue) is prohibited without explicit written permission from + the copyright holder. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Param-only settings entry for the Acceleration EQ feature. +No UI fields (section/type/title), so the native dp settings panel skips +these; generate_settings.py still emits them into common/params_keys.h. +The editor UI lives in the dashy web repo. +""" + +from cereal import car +from openpilot.common.constants import CV +from openpilot.common.realtime import DT_MDL +from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState + +V_MIN = 1.0 # m/s — exclude creep/stop +TTC_MIN = 0.5 # s — lead must be at least this far in time +LAT_ACCEL_MAX = 1.0 # m/s² — exclude curves +FLUSH_DT = 60.0 # s — append the buffer to disk at most this often + +# Accel-log data is observational telemetry, not config, so it lives as a CSV on the +# drive-log partition (where rlogs live) rather than in the params store. +LOG_PATH = "/data/media/0/realdata/accel_log.csv" + + +def _should_log(long_off, gas, brake, blinker, in_drive, moving, a_ego, lead_ttc, lat_accel): + """True only for a clean, free, straight-line human-acceleration sample.""" + return (long_off and gas and a_ego > 0.0 + and not brake and not blinker + and in_drive and moving + and lead_ttc > TTC_MIN and lat_accel < LAT_ACCEL_MAX) + + +class AccelLogger: + """Logs the driver's natural acceleration — clean, free, straight-line manual + samples — to a CSV (columns: vEgo m/s, aEgo m/s²). Samples are buffered in RAM + and appended once a minute to spare the flash; up to FLUSH_DT of samples are + lost on an ungraceful shutdown, which is negligible for an aggregate. The gate + makes clean samples rare, so the file grows very slowly (no cap needed). + Fully exception-isolated — it can never perturb the planner.""" + + def __init__(self, CP, path=None): + self._CP = CP + self._path = path if path is not None else LOG_PATH + self._buf = [] + self._frames = 0 + self._flush_every = max(1, int(FLUSH_DT / DT_MDL)) + + def _flush(self): + # Take + clear the buffer first, so RAM stays bounded even if the write + # fails (e.g. path not writable) — those rows are simply dropped. + rows, self._buf = self._buf, [] + try: + with open(self._path, "a") as f: + f.writelines(f"{v:.3f},{a:.3f}\n" for v, a in rows) + except Exception as e: + cloudlog.warning(f"AccelLogger: write failed (dropped {len(rows)} rows): {e}") + + def update(self, sm): + try: + self._frames += 1 + cs = sm['carState'] + v_ego = cs.vEgo + long_off = sm['controlsState'].longControlState == LongCtrlState.off + in_drive = cs.gearShifter == car.CarState.GearShifter.drive + moving = (not cs.standstill) and v_ego > V_MIN + blinker = cs.leftBlinker or cs.rightBlinker + lat_accel = abs(v_ego ** 2 * cs.steeringAngleDeg * CV.DEG_TO_RAD + / (self._CP.steerRatio * self._CP.wheelbase)) + lead = sm['radarState'].leadOne + lead_ttc = (lead.dRel / max(v_ego, 0.1)) if lead.status else float('inf') + + if _should_log(long_off, cs.gasPressed, cs.brakePressed, + blinker, in_drive, moving, cs.aEgo, lead_ttc, lat_accel): + self._buf.append((v_ego, cs.aEgo)) + + if self._buf and self._frames % self._flush_every == 0: + self._flush() + except Exception as e: + cloudlog.warning(f"AccelLogger.update failed (ignored): {e}") diff --git a/dragonpilot/selfdrive/controls/lib/tests/test_accel_eq.py b/dragonpilot/selfdrive/controls/lib/tests/test_accel_eq.py new file mode 100644 index 000000000..b368937b1 --- /dev/null +++ b/dragonpilot/selfdrive/controls/lib/tests/test_accel_eq.py @@ -0,0 +1,269 @@ +from dragonpilot.selfdrive.controls.lib.accel_eq import _validate_curve, MAX_ACCEL_CEIL, _select_active, MAX_PTS + +def test_valid_curve_passes_through_sorted(): + bp, v = _validate_curve({"bp": [0, 10, 25, 40], "v": [1.6, 1.2, 0.8, 0.6]}, MAX_ACCEL_CEIL) + assert bp == [0.0, 10.0, 25.0, 40.0] + assert v == [1.6, 1.2, 0.8, 0.6] + +def test_unsorted_speeds_are_sorted_together(): + bp, v = _validate_curve({"bp": [40, 0, 10], "v": [0.6, 1.6, 1.2]}, MAX_ACCEL_CEIL) + assert bp == [0.0, 10.0, 40.0] + assert v == [1.6, 1.2, 0.6] + +def test_value_clamped_to_ceiling(): + _, v = _validate_curve({"bp": [0, 20], "v": [99.0, -5.0]}, MAX_ACCEL_CEIL) + assert v == [MAX_ACCEL_CEIL, 0.0] + +def test_too_few_points_rejected(): + assert _validate_curve({"bp": [0], "v": [1.6]}, MAX_ACCEL_CEIL) is None + +def test_too_many_points_rejected(): + n = MAX_PTS + 1 + assert _validate_curve({"bp": list(range(n)), "v": [1.0] * n}, MAX_ACCEL_CEIL) is None + +def test_mismatched_lengths_rejected(): + assert _validate_curve({"bp": [0, 10], "v": [1.6]}, MAX_ACCEL_CEIL) is None + +def test_duplicate_speeds_rejected(): + assert _validate_curve({"bp": [10, 10], "v": [1.0, 0.9]}, MAX_ACCEL_CEIL) is None + +def test_non_numeric_rejected(): + assert _validate_curve({"bp": [0, "x"], "v": [1.6, 1.2]}, MAX_ACCEL_CEIL) is None + +def test_non_finite_rejected(): + assert _validate_curve({"bp": [0, float("inf")], "v": [1.6, 1.2]}, MAX_ACCEL_CEIL) is None + +def test_none_and_non_dict_rejected(): + assert _validate_curve(None, MAX_ACCEL_CEIL) is None + assert _validate_curve([1, 2], MAX_ACCEL_CEIL) is None + + +DATA = {"profiles": [ + {"name": "Eco", "max": {"bp": [0, 20], "v": [1.0, 0.8]}}, + {"name": "Sport", "max": {"bp": [0, 20], "v": [2.0, 1.6]}}, +]} + +def test_select_named_profile(): + assert _select_active(DATA, "Sport")["name"] == "Sport" + +def test_unknown_name_returns_none(): + assert _select_active(DATA, "Nope") is None # no first-profile fallback + +def test_empty_name_returns_none(): + assert _select_active(DATA, "") is None + +def test_no_profiles_returns_none(): + assert _select_active({"profiles": []}, "x") is None + assert _select_active({}, "x") is None + assert _select_active(None, "x") is None + +def test_unnamed_profiles_ignored(): + bad = {"profiles": [{"max": {}}, {"name": 5}]} + assert _select_active(bad, "x") is None + + +import numpy as np +from openpilot.common.params import Params +from dragonpilot.selfdrive.controls.lib.accel_eq import AccelEq, PROFILES_KEY +# Stock fallback is the planner's A_CRUISE_MAX table, injected into AccelEq. +from openpilot.selfdrive.controls.lib.longitudinal_planner import ( + A_CRUISE_MAX_BP as STOCK_MAX_BP, A_CRUISE_MAX_VALS as STOCK_MAX_V, +) + +def test_unset_uses_stock(): + c = AccelEq(STOCK_MAX_BP, STOCK_MAX_V, Params()) + for v in (0., 5., 10., 25., 40., 55.): + assert c.max_accel(v) == float(np.interp(v, STOCK_MAX_BP, STOCK_MAX_V)) + +def test_active_profile_drives_curve(): + p = Params() + p.put(PROFILES_KEY, {"active": "Sport", "profiles": [ + {"name": "Sport", "max": {"bp": [0, 40], "v": [2.0, 1.0]}, + "turn": {"bp": [20, 40], "v": [1.7, 3.2]}}, + ]}, block=True) + c = AccelEq(STOCK_MAX_BP, STOCK_MAX_V, p) + assert c.max_accel(0.) == 2.0 + assert c.max_accel(40.) == 1.0 + +def test_switching_active_changes_curve_on_refresh(): + p = Params() + p.put(PROFILES_KEY, {"active": "Eco", "profiles": [ + {"name": "Eco", "max": {"bp": [0, 40], "v": [1.0, 0.5]}}, + {"name": "Sport", "max": {"bp": [0, 40], "v": [2.0, 1.0]}}, + ]}, block=True) + c = AccelEq(STOCK_MAX_BP, STOCK_MAX_V, p) + assert c.max_accel(0.) == 1.0 + p.put(PROFILES_KEY, {"active": "Sport", "profiles": [ + {"name": "Eco", "max": {"bp": [0, 40], "v": [1.0, 0.5]}}, + {"name": "Sport", "max": {"bp": [0, 40], "v": [2.0, 1.0]}}, + ]}, block=True) + c.maybe_refresh(1) + assert c.max_accel(0.) == 2.0 + +def test_no_work_when_unchanged(monkeypatch): + p = Params() + c = AccelEq(STOCK_MAX_BP, STOCK_MAX_V, p) + c.maybe_refresh(1) # warm up: first call resolves the -1 sentinel to a real personality + calls = [] + monkeypatch.setattr(c, "_reload_doc", lambda: calls.append("doc")) + monkeypatch.setattr(c, "_resolve", lambda: calls.append("resolve")) + c.maybe_refresh(1) + c.maybe_refresh(1) + assert calls == [] # mtime + personality unchanged → no read, no resolve + +def test_personality_change_resolves_without_reread(monkeypatch): + # A personality change must re-resolve from the cached doc, never re-read the param. + p = Params() + p.put(PROFILES_KEY, { + "use_personality": True, + "personality_map": {"0": "Sport", "1": "Stock", "2": "Eco"}, + "profiles": [ + {"name": "Stock", "max": {"bp": [0, 40], "v": [1.6, 0.6]}}, + {"name": "Sport", "max": {"bp": [0, 40], "v": [2.0, 1.0]}}, + {"name": "Eco", "max": {"bp": [0, 40], "v": [1.0, 0.5]}}, + ], + }, block=True) + c = AccelEq(STOCK_MAX_BP, STOCK_MAX_V, p) + reads = [] + monkeypatch.setattr(c, "_reload_doc", lambda: reads.append(1)) + c.maybe_refresh(0) # aggressive → Sport, resolved from cache + assert c.max_accel(0.) == 2.0 + assert reads == [] # no JSON re-read on a personality change + +def test_malformed_profiles_falls_back_to_stock(): + p = Params() + # write raw invalid json bytes directly + p.put(PROFILES_KEY, {"active": "Bad", "profiles": [{"name": "Bad", "max": {"bp": [0], "v": [9]}}]}, block=True) + c = AccelEq(STOCK_MAX_BP, STOCK_MAX_V, p) + assert c.max_accel(0.) == float(np.interp(0., STOCK_MAX_BP, STOCK_MAX_V)) + +def test_deleted_after_set_reverts_to_stock(): + p = Params() + p.put(PROFILES_KEY, {"active": "S", "profiles": [{"name": "S", "max": {"bp": [0, 40], "v": [2.0, 1.0]}}]}, block=True) + c = AccelEq(STOCK_MAX_BP, STOCK_MAX_V, p) + assert c.max_accel(0.) == 2.0 + p.remove(PROFILES_KEY) + c.maybe_refresh(1) + assert c.max_accel(0.) == float(np.interp(0., STOCK_MAX_BP, STOCK_MAX_V)) + +def test_invalid_or_empty_doc_falls_back_to_stock_regardless_of_personality(): + # A doc that isn't a usable dict (non-dict, empty, or no profiles) → stock, + # for every personality. Mirrors Params.get() returning None on bad/empty JSON. + c = AccelEq(STOCK_MAX_BP, STOCK_MAX_V, Params()) + stock0 = float(np.interp(0., STOCK_MAX_BP, STOCK_MAX_V)) + for bad in (None, [1, 2, 3], {}, {"use_personality": True, "personality_map": {"0": "X"}}): + c._doc = bad + for pers in (0, 1, 2): + c._personality = pers + c._resolve() + assert c.max_accel(0.) == stock0 + + +from openpilot.selfdrive.controls.lib.longitudinal_planner import ( + get_max_accel, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS, +) + +def test_accelcurves_matches_legacy_get_max_accel_when_unset(): + c = AccelEq(STOCK_MAX_BP, STOCK_MAX_V, Params()) + for v in (0., 7., 10., 18., 25., 33., 40., 50.): + assert c.max_accel(v) == float(get_max_accel(v)) + assert get_max_accel(v) == float(np.interp(v, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS)) + + +from dragonpilot.selfdrive.controls.lib.accel_eq import _resolve_active_name + +PDATA = {"use_personality": True, + "personality_map": {"0": "Sport", "1": "Stock", "2": "Eco"}, + "profiles": [{"name": "Stock"}, {"name": "Sport"}, {"name": "Eco"}]} + +def test_personality_maps_to_profile(): + assert _resolve_active_name(PDATA, 0) == "Sport" + assert _resolve_active_name(PDATA, 2) == "Eco" + +def test_personality_off_uses_active(): + d = {"use_personality": False, "active": "Manual", "profiles": []} + assert _resolve_active_name(d, 0) == "Manual" + +def test_personality_off_no_active_returns_none(): + assert _resolve_active_name({"use_personality": False}, 0) is None # → stock + +def test_personality_unmapped_returns_none(): + d = {"use_personality": True, "personality_map": {"0": "Sport"}, "active": "Manual"} + assert _resolve_active_name(d, 1) is None # 1 unmapped → stock, not manual + +def test_personality_missing_map_returns_none(): + assert _resolve_active_name({"use_personality": True}, 0) is None + +def test_resolve_handles_non_dict(): + assert _resolve_active_name(None, 0) is None + + +def test_personality_selects_profile_in_accelcurves(): + p = Params() + p.put(PROFILES_KEY, { + "use_personality": True, + "personality_map": {"0": "Sport", "1": "Stock", "2": "Eco"}, + "profiles": [ + {"name": "Stock", "max": {"bp": [0, 40], "v": [1.6, 0.6]}}, + {"name": "Sport", "max": {"bp": [0, 40], "v": [2.0, 1.0]}}, + {"name": "Eco", "max": {"bp": [0, 40], "v": [1.0, 0.5]}}, + ], + }, block=True) + c = AccelEq(STOCK_MAX_BP, STOCK_MAX_V, p) + c.maybe_refresh(0) # aggressive (live personality from the planner) + assert c.max_accel(0.) == 2.0 # Sport + c.maybe_refresh(2) # relaxed + assert c.max_accel(0.) == 1.0 # Eco + +def test_personality_off_uses_manual_active_in_accelcurves(): + p = Params() + p.put(PROFILES_KEY, { + "active": "Stock", + "use_personality": False, + "personality_map": {"0": "Sport"}, + "profiles": [ + {"name": "Stock", "max": {"bp": [0, 40], "v": [1.6, 0.6]}}, + {"name": "Sport", "max": {"bp": [0, 40], "v": [2.0, 1.0]}}, + ], + }, block=True) + c = AccelEq(STOCK_MAX_BP, STOCK_MAX_V, p) + c.maybe_refresh(0) # personality 0 would map to Sport, but use_personality is off + assert c.max_accel(0.) == 1.6 # manual "active": Stock wins → injected stock + +def test_active_in_json_drives_curve_without_legacy_key(): + p = Params() + p.put(PROFILES_KEY, { + "active": "Sport", + "profiles": [ + {"name": "Stock", "max": {"bp": [0, 40], "v": [1.6, 0.6]}}, + {"name": "Sport", "max": {"bp": [0, 40], "v": [2.0, 1.0]}}, + ], + }, block=True) + c = AccelEq(STOCK_MAX_BP, STOCK_MAX_V, p) + assert c.max_accel(0.) == 2.0 + +def test_no_active_falls_back_to_stock(): + # No "active" and personality off → injected stock, NOT the first profile. + p = Params() + p.put(PROFILES_KEY, { + "profiles": [ + {"name": "Sport", "max": {"bp": [0, 40], "v": [2.0, 1.0]}}, + {"name": "Eco", "max": {"bp": [0, 40], "v": [1.0, 0.5]}}, + ], + }, block=True) + c = AccelEq(STOCK_MAX_BP, STOCK_MAX_V, p) + assert c.max_accel(0.) == float(np.interp(0., STOCK_MAX_BP, STOCK_MAX_V)) # stock, not Sport + assert c.max_accel(0.) != 2.0 + + +def test_stock_profile_ignores_stored_curve(): + # A stale/legacy "Stock" with a stored curve must NOT override the injected + # (live) stock table — Stock is a mirror, so a planner stock change wins. + p = Params() + p.put(PROFILES_KEY, { + "active": "Stock", + "profiles": [{"name": "Stock", "max": {"bp": [0, 40], "v": [0.1, 0.1]}}], + }, block=True) + c = AccelEq(STOCK_MAX_BP, STOCK_MAX_V, p) + assert c.max_accel(0.) == float(np.interp(0., STOCK_MAX_BP, STOCK_MAX_V)) + assert c.max_accel(0.) != 0.1 diff --git a/dragonpilot/selfdrive/controls/lib/tests/test_accel_logger.py b/dragonpilot/selfdrive/controls/lib/tests/test_accel_logger.py new file mode 100644 index 000000000..627d40824 --- /dev/null +++ b/dragonpilot/selfdrive/controls/lib/tests/test_accel_logger.py @@ -0,0 +1,95 @@ +from types import SimpleNamespace +from cereal import car +from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState +from dragonpilot.selfdrive.controls.lib.accel_logger import _should_log, AccelLogger + + +def _clean(**over): + args = dict(long_off=True, gas=True, brake=False, blinker=False, + in_drive=True, moving=True, a_ego=0.8, lead_ttc=9.9, lat_accel=0.2) + args.update(over) + return _should_log(**args) + +def test_clean_sample_logs(): + assert _clean() is True + +def test_each_condition_blocks(): + assert _clean(long_off=False) is False # op long active + assert _clean(gas=False) is False + assert _clean(a_ego=0.0) is False # not accelerating + assert _clean(brake=True) is False + assert _clean(blinker=True) is False + assert _clean(in_drive=False) is False + assert _clean(moving=False) is False + assert _clean(lead_ttc=1.0) is False # lead too close + assert _clean(lat_accel=5.0) is False # in a curve + + +DRIVE = car.CarState.GearShifter.drive +CP = SimpleNamespace(steerRatio=15.0, wheelbase=2.7) + +def _sm(**over): + cs = dict(vEgo=10.0, aEgo=0.8, gasPressed=True, brakePressed=False, + steeringPressed=False, leftBlinker=False, rightBlinker=False, + standstill=False, gearShifter=DRIVE, steeringAngleDeg=0.0) + cs.update(over) + return { + 'carState': SimpleNamespace(**cs), + 'controlsState': SimpleNamespace(longControlState=LongCtrlState.off), + 'radarState': SimpleNamespace(leadOne=SimpleNamespace(status=False, dRel=0.0)), + } + +def test_clean_sample_buffers(tmp_path): + log = AccelLogger(CP, path=str(tmp_path / "h.csv")) + log.update(_sm(vEgo=10.0, aEgo=0.8)) + assert log._buf == [(10.0, 0.8)] + +def test_gate_blocks_dirty_sample(tmp_path): + log = AccelLogger(CP, path=str(tmp_path / "h.csv")) + sm = _sm() + sm['controlsState'] = SimpleNamespace(longControlState=LongCtrlState.pid) # op long active + log.update(sm) + assert log._buf == [] + +def test_in_curve_sample_rejected_end_to_end(tmp_path): + # large steering -> lateral accel exceeds LAT_ACCEL_MAX -> not buffered + log = AccelLogger(CP, path=str(tmp_path / "h.csv")) + log.update(_sm(vEgo=10.0, aEgo=0.8, steeringAngleDeg=45.0)) + assert log._buf == [] + +def test_flush_appends_and_clears(tmp_path): + p = tmp_path / "h.csv" + log = AccelLogger(CP, path=str(p)) + log.update(_sm(vEgo=10.0, aEgo=0.8)) + log.update(_sm(vEgo=12.0, aEgo=0.5)) + log._flush() + assert log._buf == [] + assert p.read_text().splitlines() == ["10.000,0.800", "12.000,0.500"] + # second flush appends (does not truncate) + log.update(_sm(vEgo=5.0, aEgo=1.0)) + log._flush() + assert p.read_text().splitlines()[-1] == "5.000,1.000" + +def test_auto_flush_on_cadence(tmp_path): + p = tmp_path / "h.csv" + log = AccelLogger(CP, path=str(p)) + log._flush_every = 1 # flush every frame + log.update(_sm(vEgo=10.0, aEgo=0.8)) + assert p.read_text().splitlines() == ["10.000,0.800"] + assert log._buf == [] + +def test_flush_failure_drops_rows_no_raise(tmp_path): + # parent dir does not exist -> open() fails -> rows dropped, no raise, RAM bounded + log = AccelLogger(CP, path=str(tmp_path / "missing" / "h.csv")) + log.update(_sm()) + log._flush() + assert log._buf == [] + +def test_update_never_raises(tmp_path): + log = AccelLogger(CP, path=str(tmp_path / "h.csv")) + log.update({}) # missing keys -> swallowed + assert log._buf == [] + +def test_logger_is_pure_observation(tmp_path): + log = AccelLogger(CP, path=str(tmp_path / "h.csv")) + assert log.update(_sm()) is None diff --git a/dragonpilot/selfdrive/ui/update_translations.py b/dragonpilot/selfdrive/ui/update_translations.py index 8d8cebcec..2604ec730 100755 --- a/dragonpilot/selfdrive/ui/update_translations.py +++ b/dragonpilot/selfdrive/ui/update_translations.py @@ -22,6 +22,24 @@ def update_translations(): ret = os.system(cmd) assert ret == 0 + # Also extract dashy's web UI strings (JavaScript) into the same template. + # dashy is a sibling repo — build_and_deploy.sh copies only the minified dist + # into the tree (where tr() calls are renamed and unscannable), so scan its + # web/src and append the tr()/tr_noop() literals via --join-existing. Override + # the location with DASHY_SRC; skipped quietly if the source isn't present. + dashy_src = os.environ.get("DASHY_SRC") or os.path.join(BASEDIR, "..", "dashy", "web", "src") + dashy_src = os.path.realpath(dashy_src) + if os.path.isdir(dashy_src): + js_files = [os.path.relpath(os.path.join(root, fn), dashy_src) + for root, _, filenames in os.walk(dashy_src) + for fn in filenames if fn.endswith(".js")] + if js_files: + cmd = ("xgettext -L JavaScript --keyword=tr --keyword=tr_noop --from-code=UTF-8 " + "--join-existing " + f"-D {dashy_src} -o {POT_FILE} {' '.join(js_files)}") + ret = os.system(cmd) + assert ret == 0 + # Generate/update translation files for each language for name in multilang.languages.values(): po_file = os.path.join(TRANSLATIONS_DIR, f"dragonpilot_{name}.po") diff --git a/dragonpilot/settings/__init__.py b/dragonpilot/settings/__init__.py index 7f68b7f26..5e7463123 100644 --- a/dragonpilot/settings/__init__.py +++ b/dragonpilot/settings/__init__.py @@ -80,6 +80,10 @@ _KNOWN_ITEM_KEYS = _UI_REQUIRED_KEYS | { # text_input_item: text field that POSTs typed value to the named action endpoint. # action_item: button that POSTs to the named action endpoint with no payload. "action", + # Setting a needs_restart param also writes OnroadCycleRequested on save + # (dashy), mirroring upstream's TogglesLayout. Behavioral only — ignored by + # generate_settings.py (not a param-storage field). + "needs_restart", # Param-storage fields (consumed by generate_settings.py, ignored by UI) "flags", "param_type", } diff --git a/dragonpilot/settings/min-feat.lon.accel-eq.py b/dragonpilot/settings/min-feat.lon.accel-eq.py new file mode 100644 index 000000000..c7bc1b797 --- /dev/null +++ b/dragonpilot/settings/min-feat.lon.accel-eq.py @@ -0,0 +1,31 @@ +""" +Copyright (c) 2026, Rick Lan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, and/or sublicense, +for non-commercial purposes only, subject to the following conditions: + +- The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. +- Commercial use (e.g. use in a product, service, or activity intended to + generate revenue) is prohibited without explicit written permission from + the copyright holder. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Param-only settings entry for the Acceleration EQ feature. +No UI fields (section/type/title), so the native dp settings panel skips +these; generate_settings.py still emits them into common/params_keys.h. +The editor UI lives in the dashy web repo. +""" + +ITEMS = [ + # The whole EQ doc (profiles, curves, personality links, and the manual + # `active` selection) lives in this one JSON param. The planner reads it; + # the dashy web UI writes it. + {"key": "dp_lon_accel_profiles", "flags": "PERSISTENT", "param_type": "JSON"}, + # Accel samples are logged to a CSV on the drive-log partition (AccelLogger), + # not a param — see dragonpilot/selfdrive/controls/lib/accel_logger.py. +] diff --git a/dragonpilot/settings/openpilot.toggles.py b/dragonpilot/settings/openpilot.toggles.py index 835dd1016..c956c6bb8 100644 --- a/dragonpilot/settings/openpilot.toggles.py +++ b/dragonpilot/settings/openpilot.toggles.py @@ -26,6 +26,7 @@ ITEMS = [ "section": _SEC, "key": "OpenpilotEnabledToggle", "type": "toggle_item", "title": lambda: tr("Enable openpilot"), "description": lambda: tr(_TOGGLES_DESC["OpenpilotEnabledToggle"]), + "needs_restart": True, "condition": _DASHY, }, { @@ -55,12 +56,14 @@ ITEMS = [ "section": _SEC, "key": "RecordFront", "type": "toggle_item", "title": lambda: tr("Record and Upload Driver Camera"), "description": lambda: tr(_TOGGLES_DESC["RecordFront"]), + "needs_restart": True, "condition": _DASHY, }, { "section": _SEC, "key": "RecordAudio", "type": "toggle_item", "title": lambda: tr("Record and Upload Microphone Audio"), "description": lambda: tr(_TOGGLES_DESC["RecordAudio"]), + "needs_restart": True, "condition": _DASHY, }, { diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index 0d14e2f9f..bfeb6202a 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -17,6 +17,8 @@ from openpilot.common.swaglog import cloudlog from dragonpilot.selfdrive.controls.lib.acm import ACM from dragonpilot.selfdrive.controls.lib.aem import AEM from dragonpilot.selfdrive.controls.lib.apm import APM +from dragonpilot.selfdrive.controls.lib.accel_eq import AccelEq +from dragonpilot.selfdrive.controls.lib.accel_logger import AccelLogger A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6] A_CRUISE_MAX_BP = [0., 10.0, 25., 40.] @@ -75,6 +77,8 @@ class LongitudinalPlanner: self.acm = ACM() self.aem = AEM() self.apm = APM() + self.accel_eq = AccelEq(A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS) + self.accel_logger = AccelLogger(CP) @staticmethod def parse_model(model_msg): @@ -118,7 +122,9 @@ class LongitudinalPlanner: # No change cost when user is controlling the speed, or when standstill prev_accel_constraint = not (reset_state or sm['carState'].standstill) - accel_clip = [ACCEL_MIN, get_max_accel(v_ego)] + self.accel_eq.maybe_refresh(sm['selfdriveState'].personality.raw) + self.accel_logger.update(sm) + accel_clip = [ACCEL_MIN, self.accel_eq.max_accel(v_ego)] steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)