diff --git a/openpilot/selfdrive/carrot/server/features/dashcam/catalog.py b/openpilot/selfdrive/carrot/server/features/dashcam/catalog.py
index bef3d02a0f..71a8869081 100644
--- a/openpilot/selfdrive/carrot/server/features/dashcam/catalog.py
+++ b/openpilot/selfdrive/carrot/server/features/dashcam/catalog.py
@@ -14,8 +14,13 @@
DASHCAM_SEGMENT_SECONDS = 60
+QCAMERA_UPLOAD_NAMES = ("qcamera.ts", "qcamera.mp4")
RLOG_SOURCE_NAMES = ("rlog.zst", "rlog.bz2", "rlog")
QLOG_SOURCE_NAMES = ("qlog.zst", "qlog.bz2", "qlog")
+UPLOAD_SOURCE_GROUPS = (
+ ("qcamera", QCAMERA_UPLOAD_NAMES),
+ ("rlog", RLOG_SOURCE_NAMES),
+)
_MODERN_ROUTE_PATTERN = re.compile(r"^([0-9a-fA-F]{8})--([0-9a-fA-F]{10})$")
# A finished segment's recording end time never changes, so once we've stat'd a
@@ -242,13 +247,32 @@ def route_time_bounds(segments_asc: list[str]) -> tuple[int, int]:
def segment_file_summary(segment_dir_path: str) -> list[dict[str, Any]]:
+ """Return the original files selected for a segment upload.
+
+ Upload exactly one qcamera source and one rlog source. Prefer logger output
+ over browser-oriented derivatives and never include reduced or auxiliary
+ artifacts. An rlog is required because the uploaded segment cannot be
+ analyzed without it; qcamera is optional so log-only segments remain useful.
+ """
out: list[dict[str, Any]] = []
- for name in ("qcamera.mp4", "qcamera.ts", "rlog.zst", "rlog.bz2", "rlog", "qlog.zst", "qlog.bz2", "qlog"):
- path = os.path.join(segment_dir_path, name)
- if os.path.isfile(path):
+ for kind, names in UPLOAD_SOURCE_GROUPS:
+ for name in names:
+ path = os.path.join(segment_dir_path, name)
try:
+ if not os.path.isfile(path):
+ continue
size = os.path.getsize(path)
except OSError:
- size = 0
- out.append({"name": name, "size": size, "sizeLabel": file_size_label(size)})
+ continue
+ if size <= 0:
+ continue
+ out.append({
+ "kind": kind,
+ "name": name,
+ "size": size,
+ "sizeLabel": file_size_label(size),
+ })
+ break
+ if not any(item["kind"] == "rlog" for item in out):
+ raise web.HTTPNotFound(text="rlog not found")
return out
diff --git a/openpilot/selfdrive/carrot/server/features/dashcam/upload_jobs.py b/openpilot/selfdrive/carrot/server/features/dashcam/upload_jobs.py
index c9b868090e..2a25f143bc 100644
--- a/openpilot/selfdrive/carrot/server/features/dashcam/upload_jobs.py
+++ b/openpilot/selfdrive/carrot/server/features/dashcam/upload_jobs.py
@@ -4,6 +4,7 @@
import os
import time
import uuid
+from collections import deque
from datetime import datetime
from typing import Any
@@ -23,6 +24,28 @@
UPLOAD_JOB_KEEP_COUNT = 12
UPLOAD_JOB_MAX_LOG_CHARS = 60000
UPLOAD_JOB_STALE_SECONDS = 30 * 60
+UPLOAD_PHASE_QUEUED = "queued"
+UPLOAD_PHASE_PREPARING = "preparing"
+UPLOAD_PHASE_UPLOADING = "uploading"
+UPLOAD_PHASE_NOTIFYING = "notifying"
+UPLOAD_PHASE_CANCELING = "canceling"
+UPLOAD_PHASE_COMPLETE = "complete"
+UPLOAD_PHASE_CANCELED = "canceled"
+UPLOAD_PHASE_FAILED = "failed"
+UPLOAD_PREPARING_END_PERCENT = 8
+UPLOAD_TRANSFERRING_END_PERCENT = 97
+UPLOAD_NOTIFYING_START_PERCENT = 98
+UPLOAD_NOTIFYING_END_PERCENT = 99
+UPLOAD_PHASES = frozenset({
+ UPLOAD_PHASE_QUEUED,
+ UPLOAD_PHASE_PREPARING,
+ UPLOAD_PHASE_UPLOADING,
+ UPLOAD_PHASE_NOTIFYING,
+ UPLOAD_PHASE_CANCELING,
+ UPLOAD_PHASE_COMPLETE,
+ UPLOAD_PHASE_CANCELED,
+ UPLOAD_PHASE_FAILED,
+})
_jobs: dict[str, dict[str, Any]] = {}
@@ -72,19 +95,63 @@ def progress(
current: int | None = None,
total: int | None = None,
percent: int | None = None,
+ phase: str | None = None,
+ phase_current: int | None = None,
+ phase_total: int | None = None,
+ bytes_current: int | None = None,
+ bytes_total: int | None = None,
+ bytes_per_second: int | None = None,
) -> None:
+ changed = False
if message is not None:
- job["message"] = str(message)
+ normalized_message = str(message)
+ changed = changed or job.get("message") != normalized_message
+ job["message"] = normalized_message
+ if phase is not None:
+ normalized_phase = str(phase).strip().lower()
+ if normalized_phase not in UPLOAD_PHASES:
+ raise ValueError(f"unsupported upload phase: {phase}")
+ changed = changed or job.get("phase") != normalized_phase
+ job["phase"] = normalized_phase
if current is not None:
- job["step_current"] = max(0, int(current))
+ normalized_current = max(0, int(current))
+ changed = changed or job.get("step_current") != normalized_current
+ job["step_current"] = normalized_current
if total is not None:
- job["step_total"] = max(0, int(total))
+ normalized_total = max(0, int(total))
+ changed = changed or job.get("step_total") != normalized_total
+ job["step_total"] = normalized_total
+ if phase_current is not None:
+ normalized_phase_current = max(0, int(phase_current))
+ changed = changed or job.get("phase_current") != normalized_phase_current
+ job["phase_current"] = normalized_phase_current
+ if phase_total is not None:
+ normalized_phase_total = max(0, int(phase_total))
+ changed = changed or job.get("phase_total") != normalized_phase_total
+ job["phase_total"] = normalized_phase_total
+ if bytes_current is not None:
+ normalized_bytes_current = max(0, int(bytes_current))
+ changed = changed or job.get("bytes_current") != normalized_bytes_current
+ job["bytes_current"] = normalized_bytes_current
+ if bytes_total is not None:
+ normalized_bytes_total = max(0, int(bytes_total))
+ changed = changed or job.get("bytes_total") != normalized_bytes_total
+ job["bytes_total"] = normalized_bytes_total
+ if bytes_per_second is not None:
+ normalized_bytes_per_second = max(0, int(bytes_per_second))
+ changed = changed or job.get("bytes_per_second") != normalized_bytes_per_second
+ job["bytes_per_second"] = normalized_bytes_per_second
if percent is None:
- c = job.get("step_current")
- t = job.get("step_total")
- if isinstance(c, int) and isinstance(t, int) and t > 0:
- percent = int(max(0, min(100, round((c / t) * 100))))
- job["progress"] = percent
+ percent = job.get("progress")
+ if percent is not None:
+ normalized_percent = int(max(0, min(100, round(float(percent)))))
+ previous_percent = job.get("progress")
+ if isinstance(previous_percent, (int, float)) and job.get("status") == "running":
+ normalized_percent = max(int(previous_percent), normalized_percent)
+ changed = changed or previous_percent != normalized_percent
+ job["progress"] = normalized_percent
+ if changed:
+ job["revision"] = max(0, int(job.get("revision") or 0)) + 1
touch(job)
@@ -104,7 +171,7 @@ def cancel_job(job_id: str) -> dict[str, Any]:
if job.get("status") in ("done", "failed", "canceled"):
return {"ok": True, "already_done": True, **snapshot(job)}
job["cancel_requested"] = True
- progress(job, message="Canceling upload")
+ progress(job, message="Canceling upload", phase=UPLOAD_PHASE_CANCELING)
append(job, "Cancel requested")
return {"ok": True, **snapshot(job)}
@@ -119,9 +186,16 @@ def snapshot(job: dict[str, Any]) -> dict[str, Any]:
"cancel_requested": bool(job.get("cancel_requested")),
"log": job.get("log") or "",
"progress": job.get("progress"),
+ "revision": max(0, int(job.get("revision") or 0)),
+ "phase": job.get("phase") or UPLOAD_PHASE_QUEUED,
"message": job.get("message") or "",
"step_current": job.get("step_current"),
"step_total": job.get("step_total"),
+ "phase_current": job.get("phase_current"),
+ "phase_total": job.get("phase_total"),
+ "bytes_current": job.get("bytes_current"),
+ "bytes_total": job.get("bytes_total"),
+ "bytes_per_second": job.get("bytes_per_second"),
"error": job.get("error"),
"created_at": job.get("created_at"),
"updated_at": job.get("updated_at"),
@@ -137,11 +211,22 @@ def finish(
error: str | None = None,
status: str | None = None,
) -> None:
- job["status"] = status or ("done" if ok else "failed")
+ final_status = status or ("done" if ok else "failed")
+ job["status"] = final_status
+ job["phase"] = (
+ UPLOAD_PHASE_CANCELED
+ if final_status == "canceled"
+ else UPLOAD_PHASE_COMPLETE
+ if ok
+ else UPLOAD_PHASE_FAILED
+ )
job["result"] = result or {"ok": bool(ok)}
job["error"] = error or (None if ok else job["result"].get("error"))
if ok:
job["progress"] = 100
+ job["phase_current"] = 1
+ job["phase_total"] = 1
+ job["revision"] = max(0, int(job.get("revision") or 0)) + 1
touch(job)
prune()
@@ -205,9 +290,16 @@ def create_job(segments: list[str]) -> dict[str, Any]:
"status": "running",
"log": "",
"progress": 0,
+ "revision": 0,
+ "phase": UPLOAD_PHASE_QUEUED,
"message": "",
"step_current": 0,
"step_total": len(segments),
+ "phase_current": 0,
+ "phase_total": len(segments),
+ "bytes_current": 0,
+ "bytes_total": 0,
+ "bytes_per_second": 0,
"error": None,
"result": None,
"cancel_requested": False,
@@ -258,7 +350,16 @@ async def run_upload_segments(segments: list[str], job: dict[str, Any] | None =
job["remote_base_path"] = remote_base_path
job["upload_target"] = "web"
job["partial_results"] = []
- progress(job, message="Preparing upload", current=0, total=total, percent=0)
+ progress(
+ job,
+ message="Preparing upload",
+ current=0,
+ total=total,
+ percent=0,
+ phase=UPLOAD_PHASE_PREPARING,
+ phase_current=0,
+ phase_total=total,
+ )
ensure_not_canceled(job)
@@ -269,21 +370,139 @@ async def run_upload_segments(segments: list[str], job: dict[str, Any] | None =
concurrency = max(1, min(6, int(os.environ.get("CARROT_WEB_UPLOAD_CONCURRENCY", "3") or "3")))
except Exception:
concurrency = 3
+
+ prepare_sem = asyncio.Semaphore(concurrency)
+
+ async def prepare_one(idx0: int, segment: str) -> tuple[int, list[Any], Exception | None]:
+ try:
+ async with prepare_sem:
+ return idx0, await asyncio.to_thread(segment_file_summary, segment_dir(segment)), None
+ except Exception as exc:
+ return idx0, [], exc
+
+ prepared: list[tuple[list[Any], Exception | None] | None] = [None] * total
+ prepared_count = 0
+ prepare_tasks = [
+ asyncio.create_task(prepare_one(idx0, segment))
+ for idx0, segment in enumerate(segments)
+ ]
+ try:
+ for prepare_task in asyncio.as_completed(prepare_tasks):
+ idx0, files, prepare_error = await prepare_task
+ prepared[idx0] = (files, prepare_error)
+ prepared_count += 1
+ ensure_not_canceled(job)
+ if job:
+ prepare_percent = (
+ round((prepared_count / total) * UPLOAD_PREPARING_END_PERCENT)
+ if total > 0
+ else UPLOAD_PREPARING_END_PERCENT
+ )
+ progress(
+ job,
+ message=f"Prepared {prepared_count}/{total}",
+ current=prepared_count,
+ total=total,
+ percent=prepare_percent,
+ phase=UPLOAD_PHASE_PREPARING,
+ phase_current=prepared_count,
+ phase_total=total,
+ )
+ except BaseException:
+ for prepare_task in prepare_tasks:
+ if not prepare_task.done():
+ prepare_task.cancel()
+ await asyncio.gather(*prepare_tasks, return_exceptions=True)
+ raise
+
+ normalized_prepared = [
+ item if item is not None else ([], RuntimeError("segment preparation did not complete"))
+ for item in prepared
+ ]
+ total_bytes = sum(
+ max(0, int(item.get("size") or 0))
+ for files, error in normalized_prepared
+ if error is None
+ for item in files
+ )
+ if job:
+ progress(
+ job,
+ message="Uploading files",
+ current=0,
+ total=total,
+ percent=UPLOAD_PREPARING_END_PERCENT,
+ phase=UPLOAD_PHASE_UPLOADING,
+ phase_current=0,
+ phase_total=total_bytes if total_bytes > 0 else total,
+ bytes_current=0,
+ bytes_total=total_bytes,
+ bytes_per_second=0,
+ )
+
sem = asyncio.Semaphore(concurrency)
completed = 0
+ logical_bytes = 0
+ transmitted_bytes = 0
+ logical_by_file: dict[tuple[int, str], int] = {}
+ transfer_started = time.monotonic()
+ speed_samples: deque[tuple[float, int]] = deque([(transfer_started, 0)])
+
+ def current_percent() -> int:
+ byte_ratio = logical_bytes / total_bytes if total_bytes > 0 else 0
+ step_ratio = completed / total if total > 0 else 0
+ transfer_ratio = max(0.0, min(1.0, max(byte_ratio, step_ratio)))
+ transfer_span = UPLOAD_TRANSFERRING_END_PERCENT - UPLOAD_PREPARING_END_PERCENT
+ return min(
+ UPLOAD_TRANSFERRING_END_PERCENT,
+ UPLOAD_PREPARING_END_PERCENT + round(transfer_ratio * transfer_span),
+ )
async def upload_one(idx0: int, segment: str) -> None:
- nonlocal completed
+ nonlocal completed, logical_bytes, transmitted_bytes
idx = idx0 + 1
- files: list[Any] = []
+ files, prepare_error = normalized_prepared[idx0]
+
+ def on_file_progress(filename: str, sent: int, file_size: int, delta: int) -> None:
+ nonlocal logical_bytes, transmitted_bytes
+ if not job:
+ return
+ key = (idx0, filename)
+ previous = logical_by_file.get(key, 0)
+ bounded = max(0, min(int(sent), max(0, int(file_size))))
+ current = max(previous, bounded)
+ logical_by_file[key] = current
+ logical_bytes += current - previous
+ transmitted_bytes += max(0, int(delta))
+
+ now = time.monotonic()
+ speed_samples.append((now, transmitted_bytes))
+ while len(speed_samples) > 2 and now - speed_samples[0][0] > 3.0:
+ speed_samples.popleft()
+ sample_time, sample_bytes = speed_samples[0]
+ elapsed = max(0.001, now - sample_time)
+ current_bytes = min(total_bytes, logical_bytes)
+ progress(
+ job,
+ percent=current_percent(),
+ phase=UPLOAD_PHASE_UPLOADING,
+ phase_current=current_bytes if total_bytes > 0 else completed,
+ phase_total=total_bytes if total_bytes > 0 else total,
+ bytes_current=current_bytes,
+ bytes_total=total_bytes,
+ bytes_per_second=max(0, round((transmitted_bytes - sample_bytes) / elapsed)),
+ )
+
async with sem:
if is_cancel_requested(job):
return
if job:
append(job, f"[{idx}/{total}] {segment}")
try:
+ if prepare_error is not None:
+ raise prepare_error
segment_path = segment_dir(segment)
- files = await asyncio.to_thread(segment_file_summary, segment_path)
+
def should_cancel() -> bool:
if job:
touch(job)
@@ -296,6 +515,8 @@ def should_cancel() -> bool:
base_url,
token,
should_cancel if job else None,
+ filenames=[str(item["name"]) for item in files],
+ on_progress=on_file_progress if job else None,
)
results[idx0] = {
"segment": segment,
@@ -325,7 +546,16 @@ def should_cancel() -> bool:
completed += 1
if job:
job["partial_results"] = [r for r in results if r is not None]
- progress(job, message=f"Uploaded {completed}/{total}", current=completed, total=total)
+ progress(
+ job,
+ message=f"Uploaded {completed}/{total}",
+ current=completed,
+ total=total,
+ percent=current_percent(),
+ phase=UPLOAD_PHASE_UPLOADING,
+ phase_current=min(total_bytes, logical_bytes) if total_bytes > 0 else completed,
+ phase_total=total_bytes if total_bytes > 0 else total,
+ )
await asyncio.gather(*(upload_one(i, seg) for i, seg in enumerate(segments)))
@@ -348,17 +578,48 @@ def should_cancel() -> bool:
response_payload["shareText"] = upload.upload_share_text(response_payload)
if job:
- progress(job, message="Sending notification", current=total, total=total, percent=98)
+ progress(
+ job,
+ message="Sending notification",
+ current=total,
+ total=total,
+ percent=UPLOAD_NOTIFYING_START_PERCENT,
+ phase=UPLOAD_PHASE_NOTIFYING,
+ phase_current=0,
+ phase_total=2,
+ )
ensure_not_canceled(job)
response_payload["webComplete"] = await send_web_upload_complete(
base_url,
token,
response_payload,
)
+ if job:
+ progress(
+ job,
+ message="Sending notification",
+ current=total,
+ total=total,
+ percent=UPLOAD_NOTIFYING_END_PERCENT,
+ phase=UPLOAD_PHASE_NOTIFYING,
+ phase_current=1,
+ phase_total=2,
+ )
response_payload["discord"] = await upload.send_discord_webhook(
upload.discord_webhook_url(params),
response_payload,
)
+ if job:
+ progress(
+ job,
+ message="Finalizing upload",
+ current=total,
+ total=total,
+ percent=UPLOAD_NOTIFYING_END_PERCENT,
+ phase=UPLOAD_PHASE_NOTIFYING,
+ phase_current=2,
+ phase_total=2,
+ )
return response_payload
@@ -386,7 +647,7 @@ async def run_job(job: dict[str, Any]) -> None:
}
result["shareText"] = upload.upload_share_text(result)
append(job, "CANCELED")
- progress(job, message="Upload canceled", percent=0)
+ progress(job, message="Upload canceled", percent=0, phase=UPLOAD_PHASE_CANCELED)
finish(job, ok=False, result=result, error=str(exc), status="canceled")
except Exception as exc:
result = {"ok": False, "error": str(exc)}
diff --git a/openpilot/selfdrive/carrot/server/tests/test_web_upload.py b/openpilot/selfdrive/carrot/server/tests/test_web_upload.py
index 9e1b9a5588..8760cef618 100644
--- a/openpilot/selfdrive/carrot/server/tests/test_web_upload.py
+++ b/openpilot/selfdrive/carrot/server/tests/test_web_upload.py
@@ -3,8 +3,10 @@
from pathlib import Path
import pytest
+from aiohttp import web
from openpilot.selfdrive.carrot import web_upload
+from openpilot.selfdrive.carrot.server.features.dashcam import catalog
from openpilot.selfdrive.carrot.server.features.dashcam import upload
from openpilot.selfdrive.carrot.server.features.dashcam import upload_jobs
from openpilot.selfdrive.carrot.server.services import dashcam_upload_report
@@ -56,6 +58,104 @@ def test_dashcam_finished_task_cannot_leave_running_job():
upload_jobs.jobs().clear()
+def test_dashcam_upload_job_exposes_stable_phase_codes():
+ upload_jobs.jobs().clear()
+ job = upload_jobs.create_job(["route--0"])
+
+ assert upload_jobs.snapshot(job)["phase"] == "queued"
+
+ upload_jobs.progress(job, current=1, total=1, phase=upload_jobs.UPLOAD_PHASE_UPLOADING)
+ assert upload_jobs.snapshot(job)["phase"] == "uploading"
+
+ upload_jobs.finish(job, ok=True, result={"ok": True})
+ snapshot = upload_jobs.snapshot(job)
+ assert snapshot["status"] == "done"
+ assert snapshot["phase"] == "complete"
+ upload_jobs.jobs().clear()
+
+
+def test_dashcam_upload_progress_is_monotonic_and_revisioned():
+ upload_jobs.jobs().clear()
+ job = upload_jobs.create_job(["route--0", "route--1"])
+ assert upload_jobs.snapshot(job)["revision"] == 0
+
+ upload_jobs.progress(
+ job,
+ phase=upload_jobs.UPLOAD_PHASE_PREPARING,
+ current=1,
+ total=2,
+ phase_current=1,
+ phase_total=2,
+ percent=4,
+ )
+ preparing = upload_jobs.snapshot(job)
+ assert preparing["progress"] == 4
+ assert preparing["phase_current"] == 1
+ assert preparing["phase_total"] == 2
+ assert preparing["revision"] == 1
+
+ upload_jobs.progress(
+ job,
+ phase=upload_jobs.UPLOAD_PHASE_UPLOADING,
+ percent=2,
+ bytes_current=128,
+ bytes_total=1024,
+ bytes_per_second=512,
+ )
+ uploading = upload_jobs.snapshot(job)
+ assert uploading["progress"] == 4
+ assert uploading["bytes_current"] == 128
+ assert uploading["bytes_total"] == 1024
+ assert uploading["bytes_per_second"] == 512
+ assert uploading["revision"] == 2
+
+ upload_jobs.finish(job, ok=True, result={"ok": True})
+ complete = upload_jobs.snapshot(job)
+ assert complete["progress"] == 100
+ assert complete["revision"] == 3
+ upload_jobs.jobs().clear()
+
+
+def test_dashcam_upload_cancel_and_failure_keep_visible_progress():
+ upload_jobs.jobs().clear()
+
+ canceled_job = upload_jobs.create_job(["route--0"])
+ upload_jobs.progress(
+ canceled_job,
+ phase=upload_jobs.UPLOAD_PHASE_UPLOADING,
+ percent=41,
+ )
+ canceled = upload_jobs.cancel_job(canceled_job["id"])
+ assert canceled["phase"] == "canceling"
+ assert canceled["progress"] == 41
+ upload_jobs.finish(
+ canceled_job,
+ ok=False,
+ status="canceled",
+ result={"ok": False, "canceled": True},
+ )
+ canceled = upload_jobs.snapshot(canceled_job)
+ assert canceled["phase"] == "canceled"
+ assert canceled["progress"] == 41
+
+ failed_job = upload_jobs.create_job(["route--1"])
+ upload_jobs.progress(
+ failed_job,
+ phase=upload_jobs.UPLOAD_PHASE_UPLOADING,
+ percent=62,
+ )
+ upload_jobs.finish(
+ failed_job,
+ ok=False,
+ error="network failed",
+ result={"ok": False, "error": "network failed"},
+ )
+ failed = upload_jobs.snapshot(failed_job)
+ assert failed["phase"] == "failed"
+ assert failed["progress"] == 62
+ upload_jobs.jobs().clear()
+
+
def test_dashcam_start_job_finalizes_unhandled_task_failure(monkeypatch):
upload_jobs.jobs().clear()
@@ -197,8 +297,15 @@ def test_dashcam_upload_report_does_not_merge_nonconsecutive_segments():
def test_dashcam_upload_completion_notifies_web_server_and_discord(monkeypatch):
segment = "00000cfb--69588de3d7--10"
notifications = []
+ uploaded_files = []
+ progress_snapshots = []
async def fake_upload_folder(*args, **kwargs):
+ uploaded_files.extend(kwargs["filenames"])
+ on_progress = kwargs.get("on_progress")
+ if on_progress:
+ on_progress("qcamera.ts", 12, 12, 12)
+ on_progress("rlog.zst", 34, 34, 34)
return True
async def fake_web_complete(base_url, token, payload):
@@ -219,11 +326,23 @@ async def fake_discord(webhook_url, payload):
monkeypatch.setattr(upload, "discord_webhook_url", lambda params: "https://discord.example/webhook")
monkeypatch.setattr(upload, "send_discord_webhook", fake_discord)
monkeypatch.setattr(upload_jobs, "segment_dir", lambda value: "/tmp/segment")
- monkeypatch.setattr(upload_jobs, "segment_file_summary", lambda value: [])
+ monkeypatch.setattr(upload_jobs, "segment_file_summary", lambda value: [
+ {"kind": "qcamera", "name": "qcamera.ts", "size": 12},
+ {"kind": "rlog", "name": "rlog.zst", "size": 34},
+ ])
monkeypatch.setattr(upload_jobs, "upload_folder_to_web", fake_upload_folder)
monkeypatch.setattr(upload_jobs, "send_web_upload_complete", fake_web_complete)
+ real_progress = upload_jobs.progress
+
+ def capture_progress(job, **kwargs):
+ real_progress(job, **kwargs)
+ progress_snapshots.append(upload_jobs.snapshot(job))
- result = asyncio.run(upload_jobs.run_upload_segments([segment]))
+ monkeypatch.setattr(upload_jobs, "progress", capture_progress)
+
+ upload_jobs.jobs().clear()
+ job = upload_jobs.create_job([segment])
+ result = asyncio.run(upload_jobs.run_upload_segments([segment], job))
assert result["ok"] is True
assert result["webComplete"] == {"ok": True, "status": 200}
@@ -232,6 +351,28 @@ async def fake_discord(webhook_url, payload):
("web", "https://upload.example", "session-token", segment),
("discord", "https://discord.example/webhook", "shared upload report"),
]
+ assert uploaded_files == ["qcamera.ts", "rlog.zst"]
+ snapshot = upload_jobs.snapshot(job)
+ assert snapshot["bytes_current"] == 46
+ assert snapshot["bytes_total"] == 46
+ assert snapshot["bytes_per_second"] >= 0
+ assert snapshot["step_current"] == 1
+ assert snapshot["step_total"] == 1
+ assert snapshot["progress"] == 99
+ assert snapshot["phase"] == "notifying"
+ assert snapshot["phase_current"] == 2
+ assert snapshot["phase_total"] == 2
+ assert [item["progress"] for item in progress_snapshots] == sorted(
+ item["progress"] for item in progress_snapshots
+ )
+ assert {"preparing", "uploading", "notifying"} <= {
+ item["phase"] for item in progress_snapshots
+ }
+ assert any(
+ item["phase"] == "preparing" and 0 < item["progress"] <= upload_jobs.UPLOAD_PREPARING_END_PERCENT
+ for item in progress_snapshots
+ )
+ upload_jobs.jobs().clear()
def test_tmux_target_uses_automatic_session_token(monkeypatch):
@@ -452,6 +593,84 @@ def test_dashcam_web_upload_streams_and_verifies_every_file(tmp_path: Path, monk
assert [request[2]["X-File-Size"] for request in session.requests] == ["11", "8"]
+def test_dashcam_web_upload_reports_chunk_progress(tmp_path: Path, monkeypatch):
+ (tmp_path / "qcamera.ts").write_bytes(b"camera-data")
+ FakeSession.instances = []
+ FakeSession.size_deltas = []
+ monkeypatch.setattr(web_upload, "ClientSession", FakeSession)
+ updates = []
+
+ assert asyncio.run(web_upload.upload_folder_to_web(
+ str(tmp_path),
+ "device",
+ "route|0",
+ "https://upload.example",
+ "token",
+ on_progress=lambda *args: updates.append(args),
+ ))
+
+ assert updates == [
+ ("qcamera.ts", 0, 11, 0),
+ ("qcamera.ts", 11, 11, 11),
+ ]
+
+
+def test_dashcam_upload_summary_selects_only_original_qcamera_and_rlog(tmp_path: Path):
+ (tmp_path / "qcamera.ts").write_bytes(b"original-video")
+ (tmp_path / "qcamera.mp4").write_bytes(b"converted-video")
+ (tmp_path / "rlog.zst").write_bytes(b"original-rlog")
+ (tmp_path / "rlog.bz2").write_bytes(b"fallback-rlog")
+ (tmp_path / "qlog.zst").write_bytes(b"reduced-log")
+ (tmp_path / "fcamera.hevc").write_bytes(b"auxiliary-video")
+
+ files = catalog.segment_file_summary(str(tmp_path))
+
+ assert [(item["kind"], item["name"], item["size"]) for item in files] == [
+ ("qcamera", "qcamera.ts", len(b"original-video")),
+ ("rlog", "rlog.zst", len(b"original-rlog")),
+ ]
+
+
+def test_dashcam_upload_summary_allows_rlog_without_qcamera(tmp_path: Path):
+ (tmp_path / "rlog.bz2").write_bytes(b"original-rlog")
+
+ files = catalog.segment_file_summary(str(tmp_path))
+
+ assert [(item["kind"], item["name"]) for item in files] == [("rlog", "rlog.bz2")]
+
+
+def test_dashcam_upload_summary_requires_rlog(tmp_path: Path):
+ (tmp_path / "qcamera.ts").write_bytes(b"original-video")
+
+ with pytest.raises(web.HTTPNotFound) as exc_info:
+ catalog.segment_file_summary(str(tmp_path))
+ assert exc_info.value.text == "rlog not found"
+
+
+def test_dashcam_web_upload_honors_explicit_file_selection(tmp_path: Path, monkeypatch):
+ (tmp_path / "qcamera.ts").write_bytes(b"camera-data")
+ (tmp_path / "rlog.zst").write_bytes(b"log-data")
+ (tmp_path / "qlog.zst").write_bytes(b"excluded-data")
+ FakeSession.instances = []
+ FakeSession.size_deltas = []
+ monkeypatch.setattr(web_upload, "ClientSession", FakeSession)
+
+ assert asyncio.run(web_upload.upload_folder_to_web(
+ str(tmp_path),
+ "device",
+ "route|0",
+ "https://upload.example",
+ "token",
+ filenames=("qcamera.ts", "rlog.zst"),
+ ))
+
+ session = FakeSession.instances[-1]
+ assert [request[0].rsplit("/", 1)[-1] for request in session.requests] == [
+ "qcamera.ts",
+ "rlog.zst",
+ ]
+
+
def test_dashcam_web_upload_retries_size_mismatch(tmp_path: Path, monkeypatch):
(tmp_path / "qlog.zst").write_bytes(b"retry-me")
FakeSession.instances = []
diff --git a/openpilot/selfdrive/carrot/web/css/generated/design-system.css b/openpilot/selfdrive/carrot/web/css/generated/design-system.css
index 287c6e6ec2..0c125c9cbe 100644
--- a/openpilot/selfdrive/carrot/web/css/generated/design-system.css
+++ b/openpilot/selfdrive/carrot/web/css/generated/design-system.css
@@ -1,2 +1,2 @@
/* @generated by web/build.mjs; edit web/src/entries/design_system.css instead. */
-.card{background:var(--md-surface-cont);border:1px solid var(--md-stroke-soft);border-radius:var(--r-md);padding:var(--sp-lg);margin-bottom:var(--sp-lg)}.value-surface{background:var(--md-surface-cont);border:1px solid var(--md-stroke-soft);padding:var(--sp-sm) var(--sp-md);border-radius:var(--control-radius);font-size:var(--fs-body-sm);color:var(--md-on-surface-var)}.c-switch{--switch-w: 52px;--switch-h: 32px;position:relative;display:inline-flex;flex:0 0 var(--switch-w);width:var(--switch-w);height:var(--switch-h);cursor:pointer;-webkit-tap-highlight-color:transparent}.c-switch--sm{--switch-w: 44px;--switch-h: 28px}.c-switch__input{position:absolute;opacity:0;pointer-events:none}.c-switch__track{position:absolute;inset:0;border:1px solid color-mix(in srgb,var(--md-outline-var) 52%,transparent);border-radius:var(--r-round);background:var(--md-surface-cont-h);transition:background var(--motion-quick) var(--ease-standard),border-color var(--motion-quick) var(--ease-standard)}.c-switch__track:after{content:"";position:absolute;top:50%;left:4px;width:calc(var(--switch-h) - 10px);height:calc(var(--switch-h) - 10px);border-radius:var(--r-round);background:var(--md-on-surface-var);box-shadow:var(--shadow-1);transform:translateY(-50%);transition:transform var(--motion-base) var(--ease-standard),background var(--motion-quick) var(--ease-standard)}.c-switch__input:checked+.c-switch__track{border-color:color-mix(in srgb,var(--md-primary) 74%,var(--md-outline-var));background:color-mix(in srgb,var(--md-primary) 80%,var(--md-surface-cont-h))}.c-switch__input:checked+.c-switch__track:after{transform:translate(calc(var(--switch-w) - var(--switch-h)),-50%);background:var(--md-on-primary)}.c-switch__input:focus-visible+.c-switch__track{outline:var(--focus-ring-width) solid var(--focus-ring-color);outline-offset:var(--focus-ring-offset)}.c-switch__input:disabled+.c-switch__track{opacity:var(--state-disabled-opacity)}.c-close{--close-size: 32px;--close-icon: 16px;--close-radius: 10px;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:var(--close-size);height:var(--close-size);padding:0;border:1px solid color-mix(in srgb,var(--md-outline-var) 40%,transparent);border-radius:var(--close-radius);background:color-mix(in srgb,var(--md-surface-cont) 82%,#000);color:var(--md-on-surface);cursor:pointer;-webkit-tap-highlight-color:transparent;transition:background var(--motion-quick) var(--ease-standard),color var(--motion-quick) var(--ease-standard),border-color var(--motion-quick) var(--ease-standard),transform var(--motion-quick) var(--ease-standard)}.c-close svg{width:var(--close-icon);height:var(--close-icon);display:block}.c-close svg path{fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round}.c-close:hover{background:color-mix(in srgb,var(--md-primary) 14%,var(--md-surface-cont));border-color:color-mix(in srgb,var(--md-primary) 32%,var(--md-outline-var));color:var(--md-on-surface)}.c-close:focus-visible{outline:var(--focus-ring-width) solid var(--focus-ring-color);outline-offset:var(--focus-ring-offset)}.c-close:active{transform:scale(.94)}.c-close--sm{--close-size: 26px;--close-icon: 14px;--close-radius: 8px}.btn{padding:10px 20px;border:1px solid color-mix(in srgb,var(--md-outline-var) 46%,transparent);background:var(--md-surface-cont-h);color:var(--md-on-surface);border-radius:var(--control-radius);cursor:pointer;font-size:var(--fs-label-lg);font-weight:750;min-height:var(--control-min-height);box-sizing:border-box;transition:background .14s ease,border-color .14s ease,color .14s ease,transform .12s ease;letter-spacing:.1px}.btn:hover,.btn:focus-visible{border-color:color-mix(in srgb,var(--md-primary) 38%,var(--md-outline-var));background:color-mix(in srgb,var(--md-surface-cont-h) 88%,var(--md-primary));outline:none}.btn:active{background:color-mix(in srgb,var(--md-surface-cont-hh) 88%,var(--md-primary));transform:translateY(1px)}.btn.active{border-color:color-mix(in srgb,var(--md-primary) 48%,var(--md-outline-var));background:var(--md-primary-state);color:var(--md-primary);font-weight:850}.smallBtn{padding:var(--sp-sm) var(--sp-md);border-radius:var(--control-radius);border:1px solid color-mix(in srgb,var(--md-outline-var) 46%,transparent);background:var(--md-surface-cont-h);color:var(--md-on-surface);cursor:pointer;font-size:var(--fs-label-lg);font-weight:750;min-height:44px;box-sizing:border-box;transition:background .14s ease,border-color .14s ease,color .14s ease,transform .12s ease}.smallBtn:hover,.smallBtn:focus-visible{border-color:color-mix(in srgb,var(--md-primary) 38%,var(--md-outline-var));background:color-mix(in srgb,var(--md-surface-cont-h) 88%,var(--md-primary));outline:none}.smallBtn:active{background:color-mix(in srgb,var(--md-surface-cont-hh) 88%,var(--md-primary));transform:translateY(1px)}.btn--filled,.smallBtn.btn--filled{background:var(--md-action-filled);border-color:color-mix(in srgb,var(--md-action-filled) 78%,var(--md-outline-var));color:var(--md-on-action-filled)}.btn--danger{color:var(--md-error)}.btn--danger:active{background:var(--md-error-cont);color:var(--md-on-error-cont)}.val{min-width:84px;text-align:center;font-weight:700;font-family:var(--font-mono);color:var(--md-on-surface)}.input-text{flex:1;min-width:200px;padding:var(--sp-md) var(--sp-lg);border-radius:var(--r-md);border:1px solid var(--md-outline-var);background:var(--md-surface-cont);color:var(--md-on-surface);font-size:var(--fs-body-md);font-family:var(--font-mono);box-sizing:border-box;outline:none;transition:border-color .15s}.input-text:focus{border-color:var(--md-primary)}.chip{--chip-font-size: var(--fs-label-sm);--chip-font-weight: 750;--chip-min-height: 22px;--chip-padding-block: 2px;--chip-padding-inline: 10px;display:inline-flex;align-items:center;gap:6px;min-height:var(--chip-min-height);padding:var(--chip-padding-block) var(--chip-padding-inline);border:1px solid color-mix(in srgb,var(--md-outline-var) 42%,transparent);border-radius:var(--r-round);background:color-mix(in srgb,var(--md-surface-cont-h) 80%,var(--md-surface-cont));color:var(--md-on-surface-var);font-size:var(--chip-font-size);font-weight:var(--chip-font-weight);line-height:1;white-space:nowrap}.chip--compact{--chip-font-size: 10px;--chip-font-weight: 820;--chip-min-height: 18px;--chip-padding-block: 0px;--chip-padding-inline: 7px}.chip--accent{border-color:color-mix(in srgb,var(--md-primary) 38%,transparent);background:color-mix(in srgb,var(--md-primary) 14%,var(--md-surface-cont-h));color:color-mix(in srgb,var(--md-primary) 70%,var(--md-on-surface))}.chip--danger{border-color:color-mix(in srgb,var(--md-error) 42%,transparent);background:color-mix(in srgb,var(--md-error) 12%,var(--md-surface-cont-h));color:color-mix(in srgb,var(--md-error) 70%,var(--md-on-surface))}.chip--success{border-color:color-mix(in srgb,var(--md-success) 42%,transparent);background:var(--md-success-cont);color:var(--md-on-success-cont)}.chip--warning{border-color:color-mix(in srgb,var(--md-warning) 42%,transparent);background:var(--md-warning-cont);color:var(--md-on-warning-cont)}.chip--info{border-color:color-mix(in srgb,var(--md-info) 42%,transparent);background:var(--md-info-cont);color:var(--md-on-info-cont)}.app-dialog{position:fixed;top:var(--app-vv-top, 0px);left:0;right:0;height:var(--app-vv-height, 100dvh);z-index:var(--z-modal);display:flex;align-items:center;justify-content:center;padding:var(--sp-lg);pointer-events:none}#appDialog{z-index:calc(var(--z-modal) + 10)}.app-dialog[hidden]{display:none!important}.app-dialog.is-open{pointer-events:auto}.app-dialog__backdrop{position:absolute;inset:0;border:0;padding:0;background:var(--popup-backdrop);opacity:0;backdrop-filter:blur(2px) saturate(106%);-webkit-backdrop-filter:blur(2px) saturate(106%);transition:opacity var(--motion-base) var(--ease-linear)}.app-dialog__sheet{position:relative;z-index:1;width:min(100%,var(--dialog-sheet-width));max-height:var(--dialog-sheet-max-height);display:flex;flex-direction:column;padding:var(--sp-lg);border:1px solid var(--popup-border-color);border-radius:var(--popup-radius);background:var(--popup-bg);box-shadow:var(--popup-shadow);opacity:0;transform:translateY(14px) scale(.985);transition:opacity var(--motion-base) var(--ease-emphasized),transform var(--motion-medium) var(--ease-emphasized)}.app-dialog.is-open .app-dialog__backdrop,.app-dialog.is-open .app-dialog__sheet{opacity:1;transform:translateY(0) scale(1)}.app-dialog__head{margin-bottom:var(--sp-sm);flex-shrink:0}.app-dialog__title{font-size:var(--fs-title-sm);font-weight:800;color:var(--md-on-surface)}.app-dialog__body{color:var(--md-on-surface-var);font-size:var(--fs-body-md);line-height:1.55;white-space:pre-wrap;word-break:break-word;flex:1 1 auto;min-height:0;overflow-y:auto}.app-dialog__metaList{display:flex;flex-direction:column;gap:6px}.app-dialog__metaLine{color:var(--md-on-surface-var);font-size:var(--fs-body-md);line-height:1.5}.app-dialog__metaSubtle{margin-top:4px;color:color-mix(in srgb,var(--md-on-surface-var) 82%,var(--md-outline));font-size:12px;line-height:1.45}.app-dialog__choices{--dialog-choice-heading-fg: var(--md-on-surface-var);--dialog-choice-heading-size: var(--fs-label-sm);display:grid;gap:6px;margin-top:var(--sp-md);overflow-y:auto;min-height:0;flex:1 1 auto;padding-right:2px}.app-dialog__choices[hidden]{display:none!important}.app-dialog__choiceBtn{display:inline-flex;align-items:center;justify-content:center;width:100%;min-width:0;padding:0 12px;border-radius:var(--dialog-choice-radius);overflow:hidden;text-overflow:ellipsis;min-height:var(--control-min-height);font-size:var(--fs-label-md);font-weight:800;letter-spacing:0}.app-dialog__choiceBtn.is-current{border-color:color-mix(in srgb,var(--md-primary) 56%,var(--md-stroke-soft));background:color-mix(in srgb,var(--md-surface-cont-hh) 74%,var(--md-primary-state));color:var(--md-on-surface);font-weight:800}.app-dialog__choices--list{gap:var(--sp-sm);padding:0;border:0;background:transparent}.app-dialog__choices--list .app-dialog__choiceGroup{display:flex;flex-direction:column;gap:inherit;min-width:0}.app-dialog__choiceHeading{padding:var(--sp-xs) var(--sp-sm) 0;color:var(--dialog-choice-heading-fg);font-size:var(--dialog-choice-heading-size);font-weight:800;line-height:1.4}.app-dialog__choices--list .app-dialog__choiceBtn--action{justify-content:space-between;text-align:left;white-space:normal;line-height:1.25;min-height:48px;padding:var(--sp-md) var(--sp-lg);border:1px solid var(--popup-item-border);border-radius:var(--popup-item-radius);background:var(--popup-item-bg);box-shadow:none}.app-dialog__choices--list .app-dialog__choiceBtn--action:hover,.app-dialog__choices--list .app-dialog__choiceBtn--action:focus-visible{border-color:color-mix(in srgb,var(--md-primary) 38%,var(--md-outline-var));background:color-mix(in srgb,var(--md-surface-cont-h) 88%,var(--md-primary));outline:none}.app-dialog__choices--list .app-dialog__choiceBtn--action:active{transform:translateY(1px)}.app-dialog__choices--list .app-dialog__choiceBtn--action.is-current{border-color:var(--popup-item-active-border);background:var(--popup-item-active-bg);color:var(--md-primary);font-weight:850}.app-dialog__choices--list .app-dialog__choiceBtn--action.is-current:after{content:"";display:block;width:6px;height:11px;margin-left:12px;border-right:2px solid color-mix(in srgb,var(--md-primary) 82%,var(--md-on-surface));border-bottom:2px solid color-mix(in srgb,var(--md-primary) 82%,var(--md-on-surface));transform:rotate(45deg);flex:0 0 auto}.app-dialog__choices--value-grid{grid-template-columns:repeat(var(--app-dialog-choice-columns, 5),minmax(0,1fr));gap:6px}.app-dialog__choices--value-grid .app-dialog__choiceBtn--value{min-height:42px;padding:0 6px;justify-content:center;border:1px solid var(--popup-item-border);border-radius:var(--popup-item-radius);background:var(--popup-item-bg);text-align:center;white-space:nowrap;font-variant-numeric:tabular-nums}.app-dialog__choices--value-grid .app-dialog__choiceBtn--value:hover,.app-dialog__choices--value-grid .app-dialog__choiceBtn--value:focus-visible{border-color:color-mix(in srgb,var(--md-primary) 38%,var(--md-outline-var));background:color-mix(in srgb,var(--popup-item-bg) 90%,var(--md-primary));outline:none}.app-dialog__choices--value-grid .app-dialog__choiceBtn--value.is-current{border-color:var(--popup-item-active-border);background:var(--popup-item-active-bg);color:var(--md-primary);font-weight:850}.app-dialog__inputWrap{margin-top:var(--sp-md)}.app-dialog__inputWrap[hidden]{display:none!important}.app-dialog__input{width:100%;min-width:0}.app-dialog__actions{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;margin-top:var(--sp-md);flex-shrink:0}.app-dialog__actions .smallBtn,.app-dialog__actions .btn{border-radius:var(--dialog-control-radius)}.app-dialog--choice-grid .app-dialog__sheet,.app-dialog--choice-value-grid .app-dialog__sheet{width:min(100%,var(--dialog-sheet-width));max-height:min(82dvh,640px);padding:var(--sp-lg)}.app-dialog--choice-grid .app-dialog__body,.app-dialog--choice-value-grid .app-dialog__body{flex:0 0 auto;font-size:var(--fs-label-sm);line-height:1.4}#appDialogConfirm.btn--filled{background:var(--md-action-filled);border-color:color-mix(in srgb,var(--md-action-filled) 78%,var(--md-outline-var));color:var(--md-on-action-filled);box-shadow:none}#appDialogConfirm.btn--filled:hover,#appDialogConfirm.btn--filled:focus-visible{background:color-mix(in srgb,var(--md-action-filled) 88%,#fff);border-color:color-mix(in srgb,var(--md-action-filled) 84%,var(--md-outline-var));color:var(--md-on-action-filled)}#appDialogConfirm.btn--filled:active{background:color-mix(in srgb,var(--md-action-filled) 86%,#2a1204)}@media(max-width:640px)and (not (min-aspect-ratio:13/10)){.app-dialog{align-items:center;padding:calc(12px + env(safe-area-inset-top,0px)) 8px calc(12px + env(safe-area-inset-bottom,0px))}.app-dialog__sheet{width:calc(100vw - 16px);max-height:var(--dialog-sheet-mobile-max-height);padding:14px;border-radius:var(--dialog-sheet-radius-mobile);transform:translateY(10px) scale(.985)}.app-dialog__head{margin-bottom:6px}.app-dialog__title{font-size:var(--fs-title-xs)}.app-dialog__body{font-size:var(--fs-body-sm)}.app-dialog__choiceBtn{min-height:42px}.app-dialog__choiceBtn--value{min-height:40px}}.app-dialog--form .app-dialog__body[hidden]{display:none!important}.app-dialog--form .app-dialog__inputWrap{display:grid;gap:var(--sp-xs);margin-top:var(--sp-sm)}.app-dialog--input{align-items:center}html[data-kb-open="1"] .app-dialog--input{align-items:flex-end;padding-bottom:calc(var(--kb-gap) + var(--kb-inset) + env(safe-area-inset-bottom,0px))}.app-dialog__inputError{color:var(--md-error);font-size:var(--fs-label-sm);line-height:1.35;overflow-wrap:anywhere}.app-dialog__inputError[hidden]{display:none!important}.ui-dropdown-menu{position:relative;display:inline-flex;align-items:center;justify-content:flex-end}.ui-dropdown-menu__button{appearance:none;-webkit-appearance:none;min-width:var(--menu-trigger-size);min-height:var(--menu-trigger-size);border:1px solid color-mix(in srgb,var(--md-outline-var) 46%,transparent);border-radius:8px;background:var(--md-surface-cont);color:var(--md-on-surface);display:inline-flex;align-items:center;justify-content:center;gap:6px;cursor:pointer;font:inherit;font-size:13px;font-weight:800;line-height:1;transition:background .14s ease,border-color .14s ease,color .14s ease,transform .12s ease}.ui-menu-icon{width:var(--menu-icon-size);height:var(--menu-icon-size);flex:0 0 var(--menu-icon-size);display:inline-flex;flex-direction:column;align-items:stretch;justify-content:center;gap:var(--menu-icon-line-gap);color:currentColor;pointer-events:none}.ui-menu-icon__line{display:block;width:100%;height:var(--menu-icon-line-height);border-radius:var(--r-round);background:currentColor}.ui-dropdown-menu__button:hover,.ui-dropdown-menu__button:focus-visible,.ui-dropdown-menu.is-open .ui-dropdown-menu__button{border-color:color-mix(in srgb,var(--md-primary) 38%,var(--md-outline-var));background:color-mix(in srgb,var(--md-surface-cont-h) 88%,var(--md-primary));outline:none}.ui-dropdown-menu__button:active{transform:translateY(1px)}.ui-dropdown-menu__panel{min-width:156px;display:grid;gap:6px;padding:var(--sp-sm);border:1px solid var(--popup-border-color);border-radius:var(--popup-radius);background:var(--popup-bg);color:var(--md-on-surface);box-shadow:var(--popup-shadow)}.ui-dropdown-menu__panel[hidden]{display:none!important}.ui-dropdown-menu__item{width:100%;min-height:40px;padding:0 12px;border:1px solid var(--popup-item-border);border-radius:var(--popup-item-radius);background:var(--popup-item-bg);color:var(--md-on-surface);display:flex;align-items:center;justify-content:flex-start;gap:8px;font:inherit;font-size:13px;font-weight:800;text-align:left;cursor:pointer;transition:background .14s ease,color .14s ease}.ui-dropdown-menu__item:hover,.ui-dropdown-menu__item:focus-visible{border-color:color-mix(in srgb,var(--md-primary) 38%,var(--md-outline-var));background:color-mix(in srgb,var(--popup-item-bg) 90%,var(--md-primary));color:var(--md-on-surface);outline:none}.ui-popover{position:relative}.ui-popover__panel{min-width:156px;padding:var(--sp-sm) var(--sp-md);border:1px solid var(--popup-border-color);border-radius:var(--popup-radius);background:var(--popup-bg);color:var(--md-on-surface);box-shadow:var(--popup-shadow)}.ui-popover__panel[hidden]{display:none!important}.ui-popover__text{display:block;color:var(--md-on-surface);font-size:var(--fs-body-sm);font-weight:750;line-height:1.4;overflow-wrap:anywhere}.app-toast-host{position:fixed;left:var(--sp-lg);right:var(--sp-lg);bottom:calc(var(--nav-bar-height) + 18px + env(safe-area-inset-bottom,0px));z-index:var(--z-toast);display:flex;flex-direction:column;align-items:center;gap:var(--sp-sm);pointer-events:none}body[data-page=terminal] .app-toast-host{top:auto;bottom:var(--terminal-toast-bottom, calc(var(--nav-bar-height) + 96px + env(safe-area-inset-bottom, 0px)));left:var(--terminal-toast-left, var(--sp-lg));right:auto;width:var(--terminal-toast-width, calc(100vw - (var(--sp-lg) * 2)));z-index:calc(var(--z-toast) - 2)}.app-toast{display:flex;align-items:center;gap:12px;width:min(100%,440px);padding:14px 18px;border:1px solid var(--toast-border);border-radius:var(--toast-radius);background:var(--toast-bg);color:var(--toast-fg);box-shadow:var(--toast-shadow);font-size:15px;font-weight:600;line-height:1.45;opacity:0;transform:translateY(16px);transition:opacity .2s ease,transform .26s cubic-bezier(.2,0,0,1)}.app-toast.is-visible{opacity:1;transform:translateY(0)}.app-toast__icon{flex:none;width:var(--toast-icon-size);height:var(--toast-icon-size);border-radius:50%;display:grid;place-items:center;background:var(--toast-icon-default);color:var(--toast-icon-fg);font:900 14px/1 system-ui,sans-serif}.app-toast__msg{flex:1;min-width:0;white-space:pre-wrap;overflow-wrap:anywhere}.app-toast.is-info .app-toast__icon{background:var(--toast-icon-info)}.app-toast.is-success .app-toast__icon{background:var(--toast-icon-success)}.app-toast.is-warn .app-toast__icon{background:var(--toast-icon-warn)}.app-toast.is-error .app-toast__icon{background:var(--toast-icon-error)}.c-state-surface{--state-surface-text: var(--md-on-surface);--state-surface-feature-color: var(--md-on-surface);--state-surface-title-color: color-mix(in srgb, var(--md-on-surface-var) 82%, var(--md-surface));--state-surface-description-color: color-mix(in srgb, var(--md-on-surface-var) 78%, transparent);--state-surface-font-weight: 700;--state-surface-overlay-bg: color-mix(in srgb, var(--md-surface) 84%, var(--drive-workspace-bg));--state-surface-max-width: 42ch;--state-surface-feature-size-min: 30px;--state-surface-feature-size-max: 50px;--state-surface-title-size-min: 34px;--state-surface-title-size-max: 58px;--state-surface-description-size: 14px;--state-surface-copy-gap: var(--sp-sm);--state-surface-action-gap: var(--sp-lg);--state-surface-action-row-gap: var(--sp-sm);--state-surface-action-min-height: 46px;--state-surface-action-max-width: 280px;--state-surface-action-font-size: 16px;--state-surface-secondary-bg: color-mix(in srgb, var(--md-surface-cont-h) 86%, transparent);--state-surface-status-accent: var(--md-outline-var);--state-surface-fog-neutral: color-mix(in srgb, var(--md-on-surface-var) 7%, transparent);--state-surface-fog-accent: color-mix(in srgb, var(--md-primary) 8%, transparent);--state-surface-fog-blur: 64px;--state-surface-fog-duration: 14s;--state-surface-fog-start-opacity: .62;--state-surface-fog-active-opacity: 1;position:absolute;z-index:var(--drive-layer-overlay);isolation:isolate;inset:0;display:grid;min-width:0;min-height:0;container-type:inline-size;place-items:safe center;padding:clamp(var(--sp-md),4cqi,var(--sp-2xl));overflow:auto;overscroll-behavior:contain;background:var(--state-surface-overlay-bg);color:var(--state-surface-text);pointer-events:auto}.c-state-surface:before,.c-state-surface:after{position:absolute;border-radius:var(--r-round);content:"";filter:blur(var(--state-surface-fog-blur));opacity:var(--state-surface-fog-active-opacity);pointer-events:none;will-change:opacity,transform}.c-state-surface:before{inset:6% 12%;background:var(--state-surface-fog-neutral);animation:state-surface-fog-neutral var(--state-surface-fog-duration) var(--ease-standard) infinite alternate}.c-state-surface:after{right:22%;bottom:18%;width:24%;height:30%;background:var(--state-surface-fog-accent);animation:state-surface-fog-accent var(--state-surface-fog-duration) var(--ease-standard) infinite alternate-reverse}.c-state-surface[hidden]{display:none!important}.c-state-surface--info{--state-surface-status-accent: var(--md-info)}.c-state-surface--warning{--state-surface-status-accent: var(--md-warning)}.c-state-surface--error{--state-surface-status-accent: var(--md-error)}.c-state-surface__body{position:relative;z-index:1;display:grid;width:min(100%,var(--state-surface-max-width));margin-block:auto;place-items:center;gap:var(--state-surface-action-gap);text-align:center}.c-state-surface__copy{display:grid;min-width:0;place-items:center;gap:var(--state-surface-copy-gap)}.c-state-surface__feature,.c-state-surface__title,.c-state-surface__description{max-inline-size:100%;margin:0;overflow-wrap:anywhere;text-wrap:balance;word-break:keep-all}.c-state-surface__feature,.c-state-surface__title{font-weight:var(--state-surface-font-weight)}.c-state-surface__feature{color:var(--state-surface-feature-color);font-size:clamp(var(--state-surface-feature-size-min),5cqi,var(--state-surface-feature-size-max));line-height:1.06;letter-spacing:-.05em}.c-state-surface__title{color:var(--state-surface-title-color);font-size:clamp(var(--state-surface-title-size-min),6.5cqi,var(--state-surface-title-size-max));line-height:1.02;letter-spacing:-.055em}.c-state-surface__description{color:var(--state-surface-description-color);font-size:var(--state-surface-description-size);font-weight:650;line-height:1.4}.c-state-surface__actions{display:grid;width:min(100%,var(--state-surface-action-max-width));grid-template-columns:minmax(0,1fr);gap:var(--state-surface-action-row-gap)}.c-state-surface__actions[data-layout=single]{grid-template-columns:minmax(0,1fr)}.c-state-surface__actions[data-layout=split]{grid-template-columns:repeat(2,minmax(0,1fr))}.c-state-surface__action{width:100%;min-width:0;min-height:var(--state-surface-action-min-height);margin:0;padding-inline:var(--sp-lg);overflow:hidden;font-size:var(--state-surface-action-font-size);text-overflow:ellipsis;white-space:nowrap}.c-state-surface__action--primary,.c-state-surface__action:where([data-tone=primary]){background:var(--md-action-filled);border-color:color-mix(in srgb,var(--md-action-filled) 78%,var(--md-outline-var));color:var(--md-on-action-filled)}.c-state-surface__action--secondary,.c-state-surface__action:where([data-tone=secondary]){background:var(--state-surface-secondary-bg);color:var(--state-surface-text)}.c-state-surface__action--danger,.c-state-surface__action:where([data-tone=danger]){border-color:color-mix(in srgb,var(--md-error) 54%,var(--md-outline-var));background:var(--md-error-cont);color:var(--md-on-error-cont)}.c-state-surface__action:disabled{opacity:var(--state-disabled-opacity);cursor:not-allowed}.c-state-surface[data-pending=true] .c-state-surface__action:disabled{cursor:wait}.c-state-surface--compact{place-items:start center;padding:var(--sp-sm);overflow:visible;background:transparent;pointer-events:none}.c-state-surface--compact:before,.c-state-surface--compact:after{display:none}.c-state-surface--compact .c-state-surface__body{width:min(100%,560px);margin:0;padding:10px 14px;place-items:stretch;gap:var(--sp-sm);border:1px solid color-mix(in srgb,var(--state-surface-status-accent) 54%,var(--md-outline-var));border-left-width:3px;border-radius:var(--r-md);background:color-mix(in srgb,var(--md-surface-cont-h) 94%,transparent);box-shadow:0 8px 24px #00000052;text-align:left;pointer-events:auto}.c-state-surface--compact .c-state-surface__copy{place-items:start;gap:2px}.c-state-surface--compact .c-state-surface__feature{font-size:12px;line-height:1.25;letter-spacing:0}.c-state-surface--compact .c-state-surface__title{font-size:15px;line-height:1.3;letter-spacing:-.02em}.c-state-surface--compact .c-state-surface__description{font-size:12px;font-weight:650;line-height:1.35}.c-state-surface--compact .c-state-surface__feature,.c-state-surface--compact .c-state-surface__title,.c-state-surface--compact .c-state-surface__description{max-inline-size:100%;overflow-wrap:anywhere;text-wrap:pretty}.c-state-surface--compact .c-state-surface__actions{width:100%;max-width:none;pointer-events:auto}@keyframes state-surface-fog-neutral{0%{opacity:var(--state-surface-fog-start-opacity);transform:translate(-4%,3%) scale(.9)}to{opacity:var(--state-surface-fog-active-opacity);transform:translate(4%,-3%) scale(1.08)}}@keyframes state-surface-fog-accent{0%{transform:translate(8%,6%) scale(.88)}to{transform:translate(-8%,-6%) scale(1.12)}}@media(prefers-reduced-motion:reduce){.c-state-surface:before,.c-state-surface:after{animation:none;will-change:auto}}@media(prefers-contrast:more){.c-state-surface{--state-surface-title-color: var(--md-on-surface);--state-surface-fog-start-opacity: .3;--state-surface-fog-active-opacity: .42}}.c-segmented-control{--segmented-control-gap: var(--sp-xs);--segmented-control-padding: 0;--segmented-control-item-min-height: var(--control-min-height);--segmented-control-item-padding: var(--sp-sm) var(--sp-md);--segmented-control-item-flex: 1 0 max-content;--segmented-control-item-font-size: inherit;--segmented-control-item-font-weight: 750;--segmented-control-item-radius: var(--control-radius);--segmented-control-item-border: color-mix(in srgb, var(--md-outline-var) 46%, transparent);--segmented-control-item-bg: var(--md-surface-cont-h);--segmented-control-item-fg: var(--md-on-surface-var);--segmented-control-selected-border: var(--md-outline);--segmented-control-selected-bg: var(--md-surface-cont-hh);--segmented-control-selected-fg: var(--md-on-surface);--segmented-control-hover-border: var(--segmented-control-selected-border);--segmented-control-hover-fg: var(--segmented-control-selected-fg);display:flex;width:100%;min-width:0;max-width:100%;gap:var(--segmented-control-gap);padding:var(--segmented-control-padding);overflow-x:auto;overflow-y:hidden;overscroll-behavior-inline:contain;scrollbar-width:thin;-webkit-overflow-scrolling:touch}.c-segmented-control__item{appearance:none;flex:var(--segmented-control-item-flex);min-width:0;min-height:var(--segmented-control-item-min-height);padding:var(--segmented-control-item-padding);border:1px solid var(--segmented-control-item-border);border-radius:var(--segmented-control-item-radius);background:var(--segmented-control-item-bg);color:var(--segmented-control-item-fg);font:inherit;font-size:var(--segmented-control-item-font-size);font-weight:var(--segmented-control-item-font-weight);line-height:1;text-align:center;white-space:nowrap;cursor:pointer}.c-segmented-control__item:hover{border-color:var(--segmented-control-hover-border);color:var(--segmented-control-hover-fg)}.c-segmented-control__item:focus-visible{outline:var(--focus-ring-width) solid var(--focus-ring-color);outline-offset:var(--focus-ring-offset)}.c-segmented-control__item[aria-selected=true],.c-segmented-control__item[aria-pressed=true],.c-segmented-control__item.is-active,.c-segmented-control__item.is-selected{border-color:var(--segmented-control-selected-border);background:var(--segmented-control-selected-bg);color:var(--segmented-control-selected-fg)}.c-segmented-control__item:disabled,.c-segmented-control__item[aria-disabled=true]{opacity:var(--state-disabled-opacity);cursor:not-allowed;pointer-events:none}.c-segmented-control--equal{--segmented-control-item-flex: 1 1 0}.c-segmented-control--equal>.c-segmented-control__item{min-inline-size:0}.c-segmented-control__label{min-inline-size:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
+.card{background:var(--md-surface-cont);border:1px solid var(--md-stroke-soft);border-radius:var(--r-md);padding:var(--sp-lg);margin-bottom:var(--sp-lg)}.value-surface{background:var(--md-surface-cont);border:1px solid var(--md-stroke-soft);padding:var(--sp-sm) var(--sp-md);border-radius:var(--control-radius);font-size:var(--fs-body-sm);color:var(--md-on-surface-var)}.c-switch{--switch-w: 52px;--switch-h: 32px;position:relative;display:inline-flex;flex:0 0 var(--switch-w);width:var(--switch-w);height:var(--switch-h);cursor:pointer;-webkit-tap-highlight-color:transparent}.c-switch--sm{--switch-w: 44px;--switch-h: 28px}.c-switch__input{position:absolute;opacity:0;pointer-events:none}.c-switch__track{position:absolute;inset:0;border:1px solid color-mix(in srgb,var(--md-outline-var) 52%,transparent);border-radius:var(--r-round);background:var(--md-surface-cont-h);transition:background var(--motion-quick) var(--ease-standard),border-color var(--motion-quick) var(--ease-standard)}.c-switch__track:after{content:"";position:absolute;top:50%;left:4px;width:calc(var(--switch-h) - 10px);height:calc(var(--switch-h) - 10px);border-radius:var(--r-round);background:var(--md-on-surface-var);box-shadow:var(--shadow-1);transform:translateY(-50%);transition:transform var(--motion-base) var(--ease-standard),background var(--motion-quick) var(--ease-standard)}.c-switch__input:checked+.c-switch__track{border-color:color-mix(in srgb,var(--md-primary) 74%,var(--md-outline-var));background:color-mix(in srgb,var(--md-primary) 80%,var(--md-surface-cont-h))}.c-switch__input:checked+.c-switch__track:after{transform:translate(calc(var(--switch-w) - var(--switch-h)),-50%);background:var(--md-on-primary)}.c-switch__input:focus-visible+.c-switch__track{outline:var(--focus-ring-width) solid var(--focus-ring-color);outline-offset:var(--focus-ring-offset)}.c-switch__input:disabled+.c-switch__track{opacity:var(--state-disabled-opacity)}.c-close{--close-size: 32px;--close-icon: 16px;--close-radius: 10px;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:var(--close-size);height:var(--close-size);padding:0;border:1px solid color-mix(in srgb,var(--md-outline-var) 40%,transparent);border-radius:var(--close-radius);background:color-mix(in srgb,var(--md-surface-cont) 82%,#000);color:var(--md-on-surface);cursor:pointer;-webkit-tap-highlight-color:transparent;transition:background var(--motion-quick) var(--ease-standard),color var(--motion-quick) var(--ease-standard),border-color var(--motion-quick) var(--ease-standard),transform var(--motion-quick) var(--ease-standard)}.c-close svg{width:var(--close-icon);height:var(--close-icon);display:block}.c-close svg path{fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round}.c-close:hover{background:color-mix(in srgb,var(--md-primary) 14%,var(--md-surface-cont));border-color:color-mix(in srgb,var(--md-primary) 32%,var(--md-outline-var));color:var(--md-on-surface)}.c-close:focus-visible{outline:var(--focus-ring-width) solid var(--focus-ring-color);outline-offset:var(--focus-ring-offset)}.c-close:active{transform:scale(.94)}.c-close--sm{--close-size: 26px;--close-icon: 14px;--close-radius: 8px}.btn{padding:10px 20px;border:1px solid color-mix(in srgb,var(--md-outline-var) 46%,transparent);background:var(--md-surface-cont-h);color:var(--md-on-surface);border-radius:var(--control-radius);cursor:pointer;font-size:var(--fs-label-lg);font-weight:750;min-height:var(--control-min-height);box-sizing:border-box;transition:background .14s ease,border-color .14s ease,color .14s ease,transform .12s ease;letter-spacing:.1px}.btn:hover,.btn:focus-visible{border-color:color-mix(in srgb,var(--md-primary) 38%,var(--md-outline-var));background:color-mix(in srgb,var(--md-surface-cont-h) 88%,var(--md-primary));outline:none}.btn:active{background:color-mix(in srgb,var(--md-surface-cont-hh) 88%,var(--md-primary));transform:translateY(1px)}.btn.active{border-color:color-mix(in srgb,var(--md-primary) 48%,var(--md-outline-var));background:var(--md-primary-state);color:var(--md-primary);font-weight:850}.smallBtn{padding:var(--sp-sm) var(--sp-md);border-radius:var(--control-radius);border:1px solid color-mix(in srgb,var(--md-outline-var) 46%,transparent);background:var(--md-surface-cont-h);color:var(--md-on-surface);cursor:pointer;font-size:var(--fs-label-lg);font-weight:750;min-height:44px;box-sizing:border-box;transition:background .14s ease,border-color .14s ease,color .14s ease,transform .12s ease}.smallBtn:hover,.smallBtn:focus-visible{border-color:color-mix(in srgb,var(--md-primary) 38%,var(--md-outline-var));background:color-mix(in srgb,var(--md-surface-cont-h) 88%,var(--md-primary));outline:none}.smallBtn:active{background:color-mix(in srgb,var(--md-surface-cont-hh) 88%,var(--md-primary));transform:translateY(1px)}.btn--filled,.smallBtn.btn--filled{background:var(--md-action-filled);border-color:color-mix(in srgb,var(--md-action-filled) 78%,var(--md-outline-var));color:var(--md-on-action-filled)}.btn--danger{color:var(--md-error)}.btn--danger:active{background:var(--md-error-cont);color:var(--md-on-error-cont)}.val{min-width:84px;text-align:center;font-weight:700;font-family:var(--font-mono);color:var(--md-on-surface)}.input-text{flex:1;min-width:200px;padding:var(--sp-md) var(--sp-lg);border-radius:var(--r-md);border:1px solid var(--md-outline-var);background:var(--md-surface-cont);color:var(--md-on-surface);font-size:var(--fs-body-md);font-family:var(--font-mono);box-sizing:border-box;outline:none;transition:border-color .15s}.input-text:focus{border-color:var(--md-primary)}.chip{--chip-font-size: var(--fs-label-sm);--chip-font-weight: 750;--chip-min-height: 22px;--chip-padding-block: 2px;--chip-padding-inline: 10px;display:inline-flex;align-items:center;gap:6px;min-height:var(--chip-min-height);padding:var(--chip-padding-block) var(--chip-padding-inline);border:1px solid color-mix(in srgb,var(--md-outline-var) 42%,transparent);border-radius:var(--r-round);background:color-mix(in srgb,var(--md-surface-cont-h) 80%,var(--md-surface-cont));color:var(--md-on-surface-var);font-size:var(--chip-font-size);font-weight:var(--chip-font-weight);line-height:1;white-space:nowrap}.chip--compact{--chip-font-size: 10px;--chip-font-weight: 820;--chip-min-height: 18px;--chip-padding-block: 0px;--chip-padding-inline: 7px}.chip--accent{border-color:color-mix(in srgb,var(--md-primary) 38%,transparent);background:color-mix(in srgb,var(--md-primary) 14%,var(--md-surface-cont-h));color:color-mix(in srgb,var(--md-primary) 70%,var(--md-on-surface))}.chip--danger{border-color:color-mix(in srgb,var(--md-error) 42%,transparent);background:color-mix(in srgb,var(--md-error) 12%,var(--md-surface-cont-h));color:color-mix(in srgb,var(--md-error) 70%,var(--md-on-surface))}.chip--success{border-color:color-mix(in srgb,var(--md-success) 42%,transparent);background:var(--md-success-cont);color:var(--md-on-success-cont)}.chip--warning{border-color:color-mix(in srgb,var(--md-warning) 42%,transparent);background:var(--md-warning-cont);color:var(--md-on-warning-cont)}.chip--info{border-color:color-mix(in srgb,var(--md-info) 42%,transparent);background:var(--md-info-cont);color:var(--md-on-info-cont)}.app-dialog{position:fixed;top:var(--app-vv-top, 0px);left:0;right:0;height:var(--app-vv-height, 100dvh);z-index:var(--z-modal);display:flex;align-items:center;justify-content:center;padding:var(--sp-lg);pointer-events:none}#appDialog{z-index:calc(var(--z-modal) + 10)}.app-dialog[hidden]{display:none!important}.app-dialog.is-open{pointer-events:auto}.app-dialog__backdrop{position:absolute;inset:0;border:0;padding:0;background:var(--popup-backdrop);opacity:0;backdrop-filter:blur(2px) saturate(106%);-webkit-backdrop-filter:blur(2px) saturate(106%);transition:opacity var(--motion-base) var(--ease-linear)}.app-dialog__sheet{position:relative;z-index:1;width:min(100%,var(--dialog-sheet-width));max-height:var(--dialog-sheet-max-height);display:flex;flex-direction:column;padding:var(--sp-lg);border:1px solid var(--popup-border-color);border-radius:var(--popup-radius);background:var(--popup-bg);box-shadow:var(--popup-shadow);opacity:0;transform:translateY(14px) scale(.985);transition:opacity var(--motion-base) var(--ease-emphasized),transform var(--motion-medium) var(--ease-emphasized)}.app-dialog.is-open .app-dialog__backdrop,.app-dialog.is-open .app-dialog__sheet{opacity:1;transform:translateY(0) scale(1)}.app-dialog__head{margin-bottom:var(--sp-sm);flex-shrink:0}.app-dialog__title{font-size:var(--fs-title-sm);font-weight:800;color:var(--md-on-surface)}.app-dialog__body{color:var(--md-on-surface-var);font-size:var(--fs-body-md);line-height:1.55;white-space:pre-wrap;word-break:break-word;flex:1 1 auto;min-height:0;overflow-y:auto}.app-dialog__metaList{display:flex;flex-direction:column;gap:var(--sp-sm)}.app-dialog__metaLine{color:var(--md-on-surface-var);font-size:var(--fs-body-md);line-height:1.5}.app-dialog__metaSubtle{margin-top:4px;color:color-mix(in srgb,var(--md-on-surface-var) 82%,var(--md-outline));font-size:12px;line-height:1.45}.app-dialog__metaGroup{display:flex;flex-direction:column;gap:var(--sp-sm);padding-top:var(--sp-xs)}.app-dialog__metaLabel{color:var(--md-on-surface-var);font-size:var(--fs-label-sm);font-weight:800}.app-dialog__metaCodeList{display:flex;flex-direction:column;gap:var(--sp-xs)}.app-dialog__metaCode{overflow-wrap:anywhere;color:var(--md-on-surface);font:700 var(--fs-label-sm)/1.45 var(--font-mono)}.app-dialog__metaSummary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-sm) var(--sp-lg);color:var(--md-on-surface-var);font-size:var(--fs-label-md)}.app-dialog__metaSummary strong{margin-left:auto;color:var(--md-on-surface);font-weight:850}.app-dialog__uploadBrief{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:var(--sp-md);min-width:0;padding:var(--sp-xs) 0}.app-dialog__uploadBriefCopy,.app-dialog__uploadBriefAmount{display:grid;min-width:0;gap:2px}.app-dialog__uploadBriefTitle{color:var(--md-on-surface);font-size:var(--fs-body-md);font-weight:850;line-height:1.35}.app-dialog__uploadBriefKinds,.app-dialog__uploadBriefAmount span{color:var(--md-on-surface-var);font-size:var(--fs-label-sm);font-weight:750;line-height:1.4}.app-dialog__uploadBriefKinds{overflow-wrap:anywhere}.app-dialog__uploadBriefAmount{justify-items:end;text-align:right;white-space:nowrap}.app-dialog__uploadBriefAmount strong{color:var(--md-on-surface);font-size:var(--fs-title-xs);font-weight:850;line-height:1.2}.app-dialog__metaResultList{display:flex;flex-direction:column;gap:var(--sp-xs)}.app-dialog__metaResult{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-md);min-width:0;padding:var(--sp-xs) 0}.app-dialog__metaResult .app-dialog__metaCode{min-width:0}.app-dialog__metaOverflow{padding:var(--sp-xs) 0;color:var(--md-error);font-size:var(--fs-label-sm);font-weight:800}.app-dialog__metaState{flex:0 0 auto;color:var(--md-on-surface-var);font-size:var(--fs-label-sm);font-weight:850}.app-dialog__metaState[data-tone=success]{color:var(--md-success)}.app-dialog__metaState[data-tone=error]{color:var(--md-error)}.app-dialog--progress .app-dialog__body{flex:0 0 auto;overflow:visible}.app-dialog__choices{--dialog-choice-heading-fg: var(--md-on-surface-var);--dialog-choice-heading-size: var(--fs-label-sm);display:grid;gap:6px;margin-top:var(--sp-md);overflow-y:auto;min-height:0;flex:1 1 auto;padding-right:2px}.app-dialog__choices[hidden]{display:none!important}.app-dialog__choiceBtn{display:inline-flex;align-items:center;justify-content:center;width:100%;min-width:0;padding:0 12px;border-radius:var(--dialog-choice-radius);overflow:hidden;text-overflow:ellipsis;min-height:var(--control-min-height);font-size:var(--fs-label-md);font-weight:800;letter-spacing:0}.app-dialog__choiceBtn.is-current{border-color:color-mix(in srgb,var(--md-primary) 56%,var(--md-stroke-soft));background:color-mix(in srgb,var(--md-surface-cont-hh) 74%,var(--md-primary-state));color:var(--md-on-surface);font-weight:800}.app-dialog__choices--list{gap:var(--sp-sm);padding:0;border:0;background:transparent}.app-dialog__choices--list .app-dialog__choiceGroup{display:flex;flex-direction:column;gap:inherit;min-width:0}.app-dialog__choiceHeading{padding:var(--sp-xs) var(--sp-sm) 0;color:var(--dialog-choice-heading-fg);font-size:var(--dialog-choice-heading-size);font-weight:800;line-height:1.4}.app-dialog__choices--list .app-dialog__choiceBtn--action{justify-content:space-between;text-align:left;white-space:normal;line-height:1.25;min-height:48px;padding:var(--sp-md) var(--sp-lg);border:1px solid var(--popup-item-border);border-radius:var(--popup-item-radius);background:var(--popup-item-bg);box-shadow:none}.app-dialog__choices--list .app-dialog__choiceBtn--action:hover,.app-dialog__choices--list .app-dialog__choiceBtn--action:focus-visible{border-color:color-mix(in srgb,var(--md-primary) 38%,var(--md-outline-var));background:color-mix(in srgb,var(--md-surface-cont-h) 88%,var(--md-primary));outline:none}.app-dialog__choices--list .app-dialog__choiceBtn--action:active{transform:translateY(1px)}.app-dialog__choices--list .app-dialog__choiceBtn--action.is-current{border-color:var(--popup-item-active-border);background:var(--popup-item-active-bg);color:var(--md-primary);font-weight:850}.app-dialog__choices--list .app-dialog__choiceBtn--action.is-current:after{content:"";display:block;width:6px;height:11px;margin-left:12px;border-right:2px solid color-mix(in srgb,var(--md-primary) 82%,var(--md-on-surface));border-bottom:2px solid color-mix(in srgb,var(--md-primary) 82%,var(--md-on-surface));transform:rotate(45deg);flex:0 0 auto}.app-dialog__choices--value-grid{grid-template-columns:repeat(var(--app-dialog-choice-columns, 5),minmax(0,1fr));gap:6px}.app-dialog__choices--value-grid .app-dialog__choiceBtn--value{min-height:42px;padding:0 6px;justify-content:center;border:1px solid var(--popup-item-border);border-radius:var(--popup-item-radius);background:var(--popup-item-bg);text-align:center;white-space:nowrap;font-variant-numeric:tabular-nums}.app-dialog__choices--value-grid .app-dialog__choiceBtn--value:hover,.app-dialog__choices--value-grid .app-dialog__choiceBtn--value:focus-visible{border-color:color-mix(in srgb,var(--md-primary) 38%,var(--md-outline-var));background:color-mix(in srgb,var(--popup-item-bg) 90%,var(--md-primary));outline:none}.app-dialog__choices--value-grid .app-dialog__choiceBtn--value.is-current{border-color:var(--popup-item-active-border);background:var(--popup-item-active-bg);color:var(--md-primary);font-weight:850}.app-dialog__inputWrap{margin-top:var(--sp-md)}.app-dialog__inputWrap[hidden]{display:none!important}.app-dialog__input{width:100%;min-width:0}.app-dialog__actions{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;margin-top:var(--sp-md);flex-shrink:0}.app-dialog__actions .smallBtn,.app-dialog__actions .btn{border-radius:var(--dialog-control-radius)}.app-dialog--choice-grid .app-dialog__sheet,.app-dialog--choice-value-grid .app-dialog__sheet{width:min(100%,var(--dialog-sheet-width));max-height:min(82dvh,640px);padding:var(--sp-lg)}.app-dialog--choice-grid .app-dialog__body,.app-dialog--choice-value-grid .app-dialog__body{flex:0 0 auto;font-size:var(--fs-label-sm);line-height:1.4}#appDialogConfirm.btn--filled{background:var(--md-action-filled);border-color:color-mix(in srgb,var(--md-action-filled) 78%,var(--md-outline-var));color:var(--md-on-action-filled);box-shadow:none}#appDialogConfirm.btn--filled:hover,#appDialogConfirm.btn--filled:focus-visible{background:color-mix(in srgb,var(--md-action-filled) 88%,#fff);border-color:color-mix(in srgb,var(--md-action-filled) 84%,var(--md-outline-var));color:var(--md-on-action-filled)}#appDialogConfirm.btn--filled:active{background:color-mix(in srgb,var(--md-action-filled) 86%,#2a1204)}@media(max-width:640px)and (not (min-aspect-ratio:13/10)){.app-dialog{align-items:center;padding:calc(12px + env(safe-area-inset-top,0px)) 8px calc(12px + env(safe-area-inset-bottom,0px))}.app-dialog__sheet{width:calc(100vw - 16px);max-height:var(--dialog-sheet-mobile-max-height);padding:14px;border-radius:var(--dialog-sheet-radius-mobile);transform:translateY(10px) scale(.985)}.app-dialog__head{margin-bottom:6px}.app-dialog__title{font-size:var(--fs-title-xs)}.app-dialog__body{font-size:var(--fs-body-sm)}.app-dialog__choiceBtn{min-height:42px}.app-dialog__choiceBtn--value{min-height:40px}}.app-dialog--form .app-dialog__body[hidden]{display:none!important}.app-dialog--form .app-dialog__inputWrap{display:grid;gap:var(--sp-xs);margin-top:var(--sp-sm)}.app-dialog--input{align-items:center}html[data-kb-open="1"] .app-dialog--input{align-items:flex-end;padding-bottom:calc(var(--kb-gap) + var(--kb-inset) + env(safe-area-inset-bottom,0px))}.app-dialog__inputError{color:var(--md-error);font-size:var(--fs-label-sm);line-height:1.35;overflow-wrap:anywhere}.app-dialog__inputError[hidden]{display:none!important}.app-progress{display:grid;gap:var(--sp-md)}.app-progress__header{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-md);min-width:0}.app-progress__message{min-width:0;color:var(--md-on-surface);font-size:var(--fs-body-md);font-weight:800;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.app-progress__value{flex:0 0 auto;color:var(--md-primary);font-size:var(--fs-title-xs);font-weight:900;font-variant-numeric:tabular-nums}.app-progress__track{display:block;width:100%;min-width:0;height:10px;overflow:hidden;appearance:none;-webkit-appearance:none;box-sizing:border-box;border:0;border-radius:var(--r-round);background:color-mix(in srgb,var(--md-primary) 14%,var(--md-surface-cont-h));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--md-primary) 18%,transparent)}.app-progress__track::-webkit-progress-bar{border-radius:inherit;background:color-mix(in srgb,var(--md-primary) 14%,var(--md-surface-cont-h))}.app-progress__track::-webkit-progress-value{border-radius:inherit;background:linear-gradient(90deg,color-mix(in srgb,var(--md-primary) 76%,var(--md-action-filled)),var(--md-primary));box-shadow:0 0 8px color-mix(in srgb,var(--md-primary) 62%,transparent);transition:width var(--motion-medium) var(--ease-linear)}.app-progress__track::-moz-progress-bar{border-radius:inherit;background:linear-gradient(90deg,color-mix(in srgb,var(--md-primary) 76%,var(--md-action-filled)),var(--md-primary));box-shadow:0 0 8px color-mix(in srgb,var(--md-primary) 62%,transparent);transition:width var(--motion-medium) var(--ease-linear)}.app-progress__track:indeterminate{background:linear-gradient(90deg,transparent 0 22%,var(--md-primary) 40% 60%,transparent 78% 100%) 0 0 / 220% 100% no-repeat,color-mix(in srgb,var(--md-primary) 14%,var(--md-surface-cont-h));animation:app-progress-indeterminate 1.05s var(--ease-linear) infinite}.app-progress__summary{color:var(--md-on-surface-var);font-size:var(--fs-label-md);font-weight:750;font-variant-numeric:tabular-nums}@keyframes app-progress-indeterminate{0%{background-position:100% 0,0 0}to{background-position:-100% 0,0 0}}@media(prefers-reduced-motion:reduce){.app-progress__track::-webkit-progress-value,.app-progress__track::-moz-progress-bar{transition:none}.app-progress__track:indeterminate{animation:none;background-position:50% 0,0 0}}.ui-dropdown-menu{position:relative;display:inline-flex;align-items:center;justify-content:flex-end}.ui-dropdown-menu__button{appearance:none;-webkit-appearance:none;min-width:var(--menu-trigger-size);min-height:var(--menu-trigger-size);border:1px solid color-mix(in srgb,var(--md-outline-var) 46%,transparent);border-radius:8px;background:var(--md-surface-cont);color:var(--md-on-surface);display:inline-flex;align-items:center;justify-content:center;gap:6px;cursor:pointer;font:inherit;font-size:13px;font-weight:800;line-height:1;transition:background .14s ease,border-color .14s ease,color .14s ease,transform .12s ease}.ui-menu-icon{width:var(--menu-icon-size);height:var(--menu-icon-size);flex:0 0 var(--menu-icon-size);display:inline-flex;flex-direction:column;align-items:stretch;justify-content:center;gap:var(--menu-icon-line-gap);color:currentColor;pointer-events:none}.ui-menu-icon__line{display:block;width:100%;height:var(--menu-icon-line-height);border-radius:var(--r-round);background:currentColor}.ui-dropdown-menu__button:hover,.ui-dropdown-menu__button:focus-visible,.ui-dropdown-menu.is-open .ui-dropdown-menu__button{border-color:color-mix(in srgb,var(--md-primary) 38%,var(--md-outline-var));background:color-mix(in srgb,var(--md-surface-cont-h) 88%,var(--md-primary));outline:none}.ui-dropdown-menu__button:active{transform:translateY(1px)}.ui-dropdown-menu__panel{min-width:156px;display:grid;gap:6px;padding:var(--sp-sm);border:1px solid var(--popup-border-color);border-radius:var(--popup-radius);background:var(--popup-bg);color:var(--md-on-surface);box-shadow:var(--popup-shadow)}.ui-dropdown-menu__panel[hidden]{display:none!important}.ui-dropdown-menu__item{width:100%;min-height:40px;padding:0 12px;border:1px solid var(--popup-item-border);border-radius:var(--popup-item-radius);background:var(--popup-item-bg);color:var(--md-on-surface);display:flex;align-items:center;justify-content:flex-start;gap:8px;font:inherit;font-size:13px;font-weight:800;text-align:left;cursor:pointer;transition:background .14s ease,color .14s ease}.ui-dropdown-menu__item:hover,.ui-dropdown-menu__item:focus-visible{border-color:color-mix(in srgb,var(--md-primary) 38%,var(--md-outline-var));background:color-mix(in srgb,var(--popup-item-bg) 90%,var(--md-primary));color:var(--md-on-surface);outline:none}.ui-popover{position:relative}.ui-popover__panel{min-width:156px;padding:var(--sp-sm) var(--sp-md);border:1px solid var(--popup-border-color);border-radius:var(--popup-radius);background:var(--popup-bg);color:var(--md-on-surface);box-shadow:var(--popup-shadow)}.ui-popover__panel[hidden]{display:none!important}.ui-popover__text{display:block;color:var(--md-on-surface);font-size:var(--fs-body-sm);font-weight:750;line-height:1.4;overflow-wrap:anywhere}.app-toast-host{position:fixed;left:var(--sp-lg);right:var(--sp-lg);bottom:calc(var(--nav-bar-height) + 18px + env(safe-area-inset-bottom,0px));z-index:var(--z-toast);display:flex;flex-direction:column;align-items:center;gap:var(--sp-sm);pointer-events:none}body[data-page=terminal] .app-toast-host{top:auto;bottom:var(--terminal-toast-bottom, calc(var(--nav-bar-height) + 96px + env(safe-area-inset-bottom, 0px)));left:var(--terminal-toast-left, var(--sp-lg));right:auto;width:var(--terminal-toast-width, calc(100vw - (var(--sp-lg) * 2)));z-index:calc(var(--z-toast) - 2)}.app-toast{display:flex;align-items:center;gap:12px;width:min(100%,440px);padding:14px 18px;border:1px solid var(--toast-border);border-radius:var(--toast-radius);background:var(--toast-bg);color:var(--toast-fg);box-shadow:var(--toast-shadow);font-size:15px;font-weight:600;line-height:1.45;opacity:0;transform:translateY(16px);transition:opacity .2s ease,transform .26s cubic-bezier(.2,0,0,1)}.app-toast.is-visible{opacity:1;transform:translateY(0)}.app-toast__icon{flex:none;width:var(--toast-icon-size);height:var(--toast-icon-size);border-radius:50%;display:grid;place-items:center;background:var(--toast-icon-default);color:var(--toast-icon-fg);font:900 14px/1 system-ui,sans-serif}.app-toast__msg{flex:1;min-width:0;white-space:pre-wrap;overflow-wrap:anywhere}.app-toast.is-info .app-toast__icon{background:var(--toast-icon-info)}.app-toast.is-success .app-toast__icon{background:var(--toast-icon-success)}.app-toast.is-warn .app-toast__icon{background:var(--toast-icon-warn)}.app-toast.is-error .app-toast__icon{background:var(--toast-icon-error)}.c-state-surface{--state-surface-text: var(--md-on-surface);--state-surface-feature-color: var(--md-on-surface);--state-surface-title-color: color-mix(in srgb, var(--md-on-surface-var) 82%, var(--md-surface));--state-surface-description-color: color-mix(in srgb, var(--md-on-surface-var) 78%, transparent);--state-surface-font-weight: 700;--state-surface-overlay-bg: color-mix(in srgb, var(--md-surface) 84%, var(--drive-workspace-bg));--state-surface-max-width: 42ch;--state-surface-feature-size-min: 30px;--state-surface-feature-size-max: 50px;--state-surface-title-size-min: 34px;--state-surface-title-size-max: 58px;--state-surface-description-size: 14px;--state-surface-copy-gap: var(--sp-sm);--state-surface-action-gap: var(--sp-lg);--state-surface-action-row-gap: var(--sp-sm);--state-surface-action-min-height: 46px;--state-surface-action-max-width: 280px;--state-surface-action-font-size: 16px;--state-surface-secondary-bg: color-mix(in srgb, var(--md-surface-cont-h) 86%, transparent);--state-surface-status-accent: var(--md-outline-var);--state-surface-fog-neutral: color-mix(in srgb, var(--md-on-surface-var) 7%, transparent);--state-surface-fog-accent: color-mix(in srgb, var(--md-primary) 8%, transparent);--state-surface-fog-blur: 64px;--state-surface-fog-duration: 14s;--state-surface-fog-start-opacity: .62;--state-surface-fog-active-opacity: 1;position:absolute;z-index:var(--drive-layer-overlay);isolation:isolate;inset:0;display:grid;min-width:0;min-height:0;container-type:inline-size;place-items:safe center;padding:clamp(var(--sp-md),4cqi,var(--sp-2xl));overflow:auto;overscroll-behavior:contain;background:var(--state-surface-overlay-bg);color:var(--state-surface-text);pointer-events:auto}.c-state-surface:before,.c-state-surface:after{position:absolute;border-radius:var(--r-round);content:"";filter:blur(var(--state-surface-fog-blur));opacity:var(--state-surface-fog-active-opacity);pointer-events:none;will-change:opacity,transform}.c-state-surface:before{inset:6% 12%;background:var(--state-surface-fog-neutral);animation:state-surface-fog-neutral var(--state-surface-fog-duration) var(--ease-standard) infinite alternate}.c-state-surface:after{right:22%;bottom:18%;width:24%;height:30%;background:var(--state-surface-fog-accent);animation:state-surface-fog-accent var(--state-surface-fog-duration) var(--ease-standard) infinite alternate-reverse}.c-state-surface[hidden]{display:none!important}.c-state-surface--info{--state-surface-status-accent: var(--md-info)}.c-state-surface--warning{--state-surface-status-accent: var(--md-warning)}.c-state-surface--error{--state-surface-status-accent: var(--md-error)}.c-state-surface__body{position:relative;z-index:1;display:grid;width:min(100%,var(--state-surface-max-width));margin-block:auto;place-items:center;gap:var(--state-surface-action-gap);text-align:center}.c-state-surface__copy{display:grid;min-width:0;place-items:center;gap:var(--state-surface-copy-gap)}.c-state-surface__feature,.c-state-surface__title,.c-state-surface__description{max-inline-size:100%;margin:0;overflow-wrap:anywhere;text-wrap:balance;word-break:keep-all}.c-state-surface__feature,.c-state-surface__title{font-weight:var(--state-surface-font-weight)}.c-state-surface__feature{color:var(--state-surface-feature-color);font-size:clamp(var(--state-surface-feature-size-min),5cqi,var(--state-surface-feature-size-max));line-height:1.06;letter-spacing:-.05em}.c-state-surface__title{color:var(--state-surface-title-color);font-size:clamp(var(--state-surface-title-size-min),6.5cqi,var(--state-surface-title-size-max));line-height:1.02;letter-spacing:-.055em}.c-state-surface__description{color:var(--state-surface-description-color);font-size:var(--state-surface-description-size);font-weight:650;line-height:1.4}.c-state-surface__actions{display:grid;width:min(100%,var(--state-surface-action-max-width));grid-template-columns:minmax(0,1fr);gap:var(--state-surface-action-row-gap)}.c-state-surface__actions[data-layout=single]{grid-template-columns:minmax(0,1fr)}.c-state-surface__actions[data-layout=split]{grid-template-columns:repeat(2,minmax(0,1fr))}.c-state-surface__action{width:100%;min-width:0;min-height:var(--state-surface-action-min-height);margin:0;padding-inline:var(--sp-lg);overflow:hidden;font-size:var(--state-surface-action-font-size);text-overflow:ellipsis;white-space:nowrap}.c-state-surface__action--primary,.c-state-surface__action:where([data-tone=primary]){background:var(--md-action-filled);border-color:color-mix(in srgb,var(--md-action-filled) 78%,var(--md-outline-var));color:var(--md-on-action-filled)}.c-state-surface__action--secondary,.c-state-surface__action:where([data-tone=secondary]){background:var(--state-surface-secondary-bg);color:var(--state-surface-text)}.c-state-surface__action--danger,.c-state-surface__action:where([data-tone=danger]){border-color:color-mix(in srgb,var(--md-error) 54%,var(--md-outline-var));background:var(--md-error-cont);color:var(--md-on-error-cont)}.c-state-surface__action:disabled{opacity:var(--state-disabled-opacity);cursor:not-allowed}.c-state-surface[data-pending=true] .c-state-surface__action:disabled{cursor:wait}.c-state-surface--compact{place-items:start center;padding:var(--sp-sm);overflow:visible;background:transparent;pointer-events:none}.c-state-surface--compact:before,.c-state-surface--compact:after{display:none}.c-state-surface--compact .c-state-surface__body{width:min(100%,560px);margin:0;padding:10px 14px;place-items:stretch;gap:var(--sp-sm);border:1px solid color-mix(in srgb,var(--state-surface-status-accent) 54%,var(--md-outline-var));border-left-width:3px;border-radius:var(--r-md);background:color-mix(in srgb,var(--md-surface-cont-h) 94%,transparent);box-shadow:0 8px 24px #00000052;text-align:left;pointer-events:auto}.c-state-surface--compact .c-state-surface__copy{place-items:start;gap:2px}.c-state-surface--compact .c-state-surface__feature{font-size:12px;line-height:1.25;letter-spacing:0}.c-state-surface--compact .c-state-surface__title{font-size:15px;line-height:1.3;letter-spacing:-.02em}.c-state-surface--compact .c-state-surface__description{font-size:12px;font-weight:650;line-height:1.35}.c-state-surface--compact .c-state-surface__feature,.c-state-surface--compact .c-state-surface__title,.c-state-surface--compact .c-state-surface__description{max-inline-size:100%;overflow-wrap:anywhere;text-wrap:pretty}.c-state-surface--compact .c-state-surface__actions{width:100%;max-width:none;pointer-events:auto}@keyframes state-surface-fog-neutral{0%{opacity:var(--state-surface-fog-start-opacity);transform:translate(-4%,3%) scale(.9)}to{opacity:var(--state-surface-fog-active-opacity);transform:translate(4%,-3%) scale(1.08)}}@keyframes state-surface-fog-accent{0%{transform:translate(8%,6%) scale(.88)}to{transform:translate(-8%,-6%) scale(1.12)}}@media(prefers-reduced-motion:reduce){.c-state-surface:before,.c-state-surface:after{animation:none;will-change:auto}}@media(prefers-contrast:more){.c-state-surface{--state-surface-title-color: var(--md-on-surface);--state-surface-fog-start-opacity: .3;--state-surface-fog-active-opacity: .42}}.c-segmented-control{--segmented-control-gap: var(--sp-xs);--segmented-control-padding: 0;--segmented-control-item-min-height: var(--control-min-height);--segmented-control-item-padding: var(--sp-sm) var(--sp-md);--segmented-control-item-flex: 1 0 max-content;--segmented-control-item-font-size: inherit;--segmented-control-item-font-weight: 750;--segmented-control-item-radius: var(--control-radius);--segmented-control-item-border: color-mix(in srgb, var(--md-outline-var) 46%, transparent);--segmented-control-item-bg: var(--md-surface-cont-h);--segmented-control-item-fg: var(--md-on-surface-var);--segmented-control-selected-border: var(--md-outline);--segmented-control-selected-bg: var(--md-surface-cont-hh);--segmented-control-selected-fg: var(--md-on-surface);--segmented-control-hover-border: var(--segmented-control-selected-border);--segmented-control-hover-fg: var(--segmented-control-selected-fg);display:flex;width:100%;min-width:0;max-width:100%;gap:var(--segmented-control-gap);padding:var(--segmented-control-padding);overflow-x:auto;overflow-y:hidden;overscroll-behavior-inline:contain;scrollbar-width:thin;-webkit-overflow-scrolling:touch}.c-segmented-control__item{appearance:none;flex:var(--segmented-control-item-flex);min-width:0;min-height:var(--segmented-control-item-min-height);padding:var(--segmented-control-item-padding);border:1px solid var(--segmented-control-item-border);border-radius:var(--segmented-control-item-radius);background:var(--segmented-control-item-bg);color:var(--segmented-control-item-fg);font:inherit;font-size:var(--segmented-control-item-font-size);font-weight:var(--segmented-control-item-font-weight);line-height:1;text-align:center;white-space:nowrap;cursor:pointer}.c-segmented-control__item:hover{border-color:var(--segmented-control-hover-border);color:var(--segmented-control-hover-fg)}.c-segmented-control__item:focus-visible{outline:var(--focus-ring-width) solid var(--focus-ring-color);outline-offset:var(--focus-ring-offset)}.c-segmented-control__item[aria-selected=true],.c-segmented-control__item[aria-pressed=true],.c-segmented-control__item.is-active,.c-segmented-control__item.is-selected{border-color:var(--segmented-control-selected-border);background:var(--segmented-control-selected-bg);color:var(--segmented-control-selected-fg)}.c-segmented-control__item:disabled,.c-segmented-control__item[aria-disabled=true]{opacity:var(--state-disabled-opacity);cursor:not-allowed;pointer-events:none}.c-segmented-control--equal{--segmented-control-item-flex: 1 1 0}.c-segmented-control--equal>.c-segmented-control__item{min-inline-size:0}.c-segmented-control__label{min-inline-size:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
diff --git a/openpilot/selfdrive/carrot/web/css/generated/logs.css b/openpilot/selfdrive/carrot/web/css/generated/logs.css
index cf3d0e3a89..0937a4b023 100644
--- a/openpilot/selfdrive/carrot/web/css/generated/logs.css
+++ b/openpilot/selfdrive/carrot/web/css/generated/logs.css
@@ -1,2 +1,2 @@
/* @generated by web/build.mjs; edit web/src/entries/logs.css instead. */
-.page--logs{height:calc(var(--app-vv-height, 100dvh) - var(--app-nav-bottom-gap, var(--nav-bar-height)));min-height:0;background:var(--md-surface);overflow:hidden}@media(min-aspect-ratio:13/10),(horizontal-viewport-segments:2),(vertical-viewport-segments:2),(min-width:640px)and (min-height:650px){.page--logs{height:var(--page-content-height, calc(var(--app-vv-height, 100dvh) - 24px))}}.logs-shell{--logs-content-inline: var(--logs-shell-inline, clamp(12px, 3.5vw, 24px));--logs-route-media-column: minmax(0, 1fr);--logs-scrollbar-gutter: 0px;--logs-tab-track-radius: var(--r-sm);--logs-tab-inset: var(--sp-xs);--logs-tab-min-height: 36px;--logs-tab-font-size: var(--fs-label-sm);--logs-tab-font-weight: 700;height:100%;min-height:0;display:flex;flex-direction:column;max-width:1480px;margin:0 auto;overflow:hidden}.logs-tabbar{position:sticky;top:0;z-index:12;display:flex;align-items:center;justify-content:center;gap:var(--sp-md);padding:8px calc(var(--logs-content-inline) + var(--logs-scrollbar-gutter)) 6px var(--logs-content-inline);background:color-mix(in srgb,var(--md-surface) 96%,transparent);border-bottom:0;backdrop-filter:blur(18px)}.logs-tabs{--segmented-control-gap: var(--logs-tab-inset);--segmented-control-padding: var(--logs-tab-inset);--segmented-control-item-min-height: var(--logs-tab-min-height);--segmented-control-item-padding: 0 var(--sp-sm);--segmented-control-item-font-size: var(--logs-tab-font-size);--segmented-control-item-font-weight: var(--logs-tab-font-weight);--segmented-control-item-radius: calc(var(--logs-tab-track-radius) - var(--logs-tab-inset));--segmented-control-item-border: transparent;--segmented-control-item-bg: transparent;--segmented-control-hover-border: transparent;--segmented-control-hover-fg: var(--md-on-surface);--segmented-control-selected-border: transparent;--segmented-control-selected-bg: var(--md-primary);--segmented-control-selected-fg: var(--md-on-primary);width:auto;flex:1 1 auto;border-radius:var(--logs-tab-track-radius);background:var(--md-surface-cont);border:1px solid color-mix(in srgb,var(--md-outline-var) 36%,transparent)}.logs-menu{flex:0 0 auto;align-self:center}.logs-menu__button{width:var(--menu-trigger-size);height:var(--menu-trigger-size);min-height:var(--menu-trigger-size);padding:0}.logs-menu__button .ui-menu-icon{width:var(--menu-icon-size);height:var(--menu-icon-size)}.logs-panel{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden}.logs-panel[hidden]{display:none}.logs-subheader{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:12px clamp(14px,3.5vw,26px) 8px;background:color-mix(in srgb,var(--md-surface-cont) 55%,transparent);border-bottom:1px solid color-mix(in srgb,var(--md-outline-var) 45%,transparent)}.logs-subheader__title{font-size:15px;line-height:1.25;font-weight:800;color:var(--md-on-surface)}.logs-subheader__desc{margin-top:4px;font-size:13px;line-height:1.35;color:var(--md-on-surface-var);opacity:.82}.dashcam-expand-btn,.dashcam-menu-btn{appearance:none;border:0;padding:0;background:transparent;color:var(--md-on-surface-var);display:inline-flex;align-items:center;justify-content:center;line-height:0;cursor:pointer;box-sizing:border-box}.dashcam-expand-btn:hover,.dashcam-menu-btn:hover{background:color-mix(in srgb,var(--md-primary) 10%,transparent);color:var(--md-on-surface)}.dashcam-expand-btn svg,.dashcam-menu-btn svg{width:22px;height:22px}.dashcam-status{flex:0 0 auto;padding:22px clamp(14px,3.5vw,26px);color:var(--md-on-surface-var);text-align:center;font-size:14px}.dashcam-status.is-error{color:#ffb4ab}.logs-empty-state{flex:0 0 auto;display:grid;justify-items:center;width:fit-content;max-width:min(100%,360px);margin:0 auto;padding:14px 18px;border:1px solid color-mix(in srgb,var(--md-outline-var) 28%,transparent);border-radius:8px;background:color-mix(in srgb,var(--md-surface-cont) 46%,transparent);color:var(--md-on-surface);text-align:center}.logs-empty-state__title{color:var(--md-on-surface);font-size:14px;font-weight:850;line-height:1.25}.dashcam-routes{flex:1 1 auto;min-height:0;overflow:auto;overscroll-behavior:contain;overflow-anchor:none;scrollbar-gutter:stable;padding:6px var(--logs-content-inline) calc(var(--app-nav-bottom-gap, var(--nav-bar-height)) + 16px + env(safe-area-inset-bottom,0px));display:flex;flex-direction:column;gap:10px}.dashcam-routes:has(.logs-empty-state),.screenrecord-list:has(.logs-empty-state){align-items:center;justify-content:center}.dashcam-virtual-spacer{flex:0 0 auto;width:100%;min-height:0;pointer-events:none}.dashcam-route-card{flex:0 0 auto;overflow:hidden;border-radius:8px;border:1px solid color-mix(in srgb,var(--md-outline-var) 50%,transparent);background:var(--md-surface-cont-h);color:var(--md-on-surface)}.dashcam-route-media{min-width:0}.dashcam-route-preview{position:relative;aspect-ratio:16 / 9;align-self:start;overflow:hidden;cursor:pointer;background:linear-gradient(180deg,var(--md-surface-cont-hh),var(--md-surface-cont))}.dashcam-route-media-info{display:none;padding:10px 12px 12px;border-top:1px solid color-mix(in srgb,var(--md-outline-var) 30%,transparent);background:color-mix(in srgb,var(--md-surface-cont-h) 92%,var(--md-surface));cursor:pointer}.dashcam-route-preview img,.dashcam-segment-thumb img{width:100%;height:100%;object-fit:cover;display:block}.logs-lazy-img{opacity:1;transition:opacity .16s ease}.logs-lazy-img[data-src]{opacity:0}.dashcam-segment-thumb.is-thumb-loading:after{content:"";position:absolute;inset:0;z-index:2;pointer-events:none;background:linear-gradient(100deg,var(--md-surface-cont-h) 0%,color-mix(in srgb,var(--md-on-surface) 10%,var(--md-surface-cont-h)) 50%,var(--md-surface-cont-h) 100%);background-size:220% 100%;animation:logsThumbSweep 1.25s ease-in-out infinite}@keyframes logsThumbSweep{0%{background-position:220% 0}to{background-position:-120% 0}}@media(prefers-reduced-motion:reduce){.dashcam-segment-thumb.is-thumb-loading:after{animation:none}}.dashcam-route-preview__shade{position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,.08),transparent 45%,rgba(0,0,0,.36));pointer-events:none}.dashcam-route-preview__chips{position:absolute;left:12px;right:12px;bottom:12px;display:flex;flex-wrap:wrap;gap:8px}.dashcam-route-preview__timestamp{position:absolute;top:12px;left:12px;z-index:1;padding:5px 8px;border-radius:7px;background:#0000007a;border:1px solid rgba(255,255,255,.16);color:#fff;font-size:12px;font-weight:700;line-height:1.35;font-variant-numeric:tabular-nums;text-shadow:0 1px 2px rgba(0,0,0,.45);backdrop-filter:blur(8px);pointer-events:none}.dashcam-route-preview .dashcam-route-preview__menu{position:absolute;top:10px;right:10px;z-index:3;width:36px;height:36px;color:#fff;background:#0000007a;border:1px solid rgba(255,255,255,.16);box-shadow:0 3px 12px #0003;backdrop-filter:blur(8px)}.dashcam-route-preview .dashcam-route-preview__menu:hover{background:#000000a3}.dashcam-chip{display:inline-flex;align-items:center;min-height:26px;padding:0 10px;border-radius:999px;background:#0000006b;border:1px solid rgba(255,255,255,.16);color:#fff;font-size:12px;font-weight:750;backdrop-filter:blur(10px)}.dashcam-play-mark{position:absolute;inset:0;display:grid;place-items:center}.dashcam-play-mark svg{width:54px;height:54px;padding:11px;border-radius:999px;color:#fff;background:#0000005c;border:1px solid rgba(255,255,255,.18);box-shadow:0 8px 24px #00000042}.dashcam-route-main{min-width:0;padding:12px 14px}.dashcam-route-head{display:flex;align-items:flex-start;gap:10px;cursor:pointer;border-radius:6px}.dashcam-route-head:hover{background:color-mix(in srgb,var(--md-primary) 7%,transparent)}.dashcam-route-titleblock{min-width:0;flex:1}.dashcam-route-title{font-size:16px;line-height:1.25;font-weight:850;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dashcam-route-subtitle{margin-top:4px;font-size:12.5px;color:var(--md-on-surface-var);opacity:.78;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dashcam-expand-btn{width:34px;height:34px;border-radius:999px;flex:0 0 auto;margin-right:8px}.dashcam-segments{margin-top:12px}.dashcam-segments.is-collapsed{display:none}.dashcam-selection-row{display:flex;align-items:center;gap:8px;overflow-x:auto;padding-bottom:8px;color:var(--md-on-surface-var);font-size:13px;font-weight:750}.dashcam-selection-count{flex:0 0 auto;color:var(--md-on-surface-var)}.dashcam-selection-row .smallBtn{white-space:nowrap;min-height:34px;padding-inline:12px;border-radius:var(--r-sm);font-size:12.5px;font-weight:800}.dashcam-segment-list{max-height:min(40vh,330px);overflow:auto;overscroll-behavior:contain;overflow-anchor:none;-webkit-overflow-scrolling:touch;touch-action:pan-y;scrollbar-width:none;display:flex;flex-direction:column;gap:8px}.dashcam-segment-list::-webkit-scrollbar{display:none}.dashcam-segment-loader{flex:0 0 auto;width:100%;height:22px;position:relative;overflow:hidden;overflow-anchor:none;border-radius:999px;background:transparent;transition:background-color .16s ease-out}.dashcam-segment-loader.is-loading{background:color-mix(in srgb,var(--md-outline-var) 18%,transparent)}.dashcam-segment-loader:before{content:"";position:absolute;inset:8px 20%;border-radius:inherit;background:var(--md-primary);opacity:0;transition:opacity .16s ease-out}.dashcam-segment-loader.is-loading:before{opacity:1;animation:dashcam-segment-loader .82s ease-in-out infinite}.dashcam-segment-tile{flex:0 0 auto;min-height:70px;display:flex;align-items:center;gap:10px;padding:8px;border-radius:8px;border:1px solid color-mix(in srgb,var(--md-outline-var) 42%,transparent);background:var(--md-surface-cont);cursor:pointer;content-visibility:auto;contain-intrinsic-size:70px 100%}@media(hover:hover){.dashcam-segment-tile:hover{border-color:color-mix(in srgb,var(--md-primary) 44%,var(--md-outline-var));background:color-mix(in srgb,var(--md-surface-cont-h) 84%,var(--md-primary))}}.dashcam-segment-tile--append{animation:dashcam-segment-append .18s cubic-bezier(.2,0,0,1) both}@keyframes dashcam-segment-append{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}@keyframes dashcam-segment-loader{0%{transform:translate(-160%)}to{transform:translate(160%)}}.dashcam-segment-thumb{position:relative;width:78px;height:54px;flex:0 0 78px;overflow:hidden;border-radius:6px;background:var(--md-surface-cont-hh)}.dashcam-segment-check{position:absolute;inset:0;display:grid;place-items:center;background:#0000001f}.dashcam-segment-check input{width:20px;height:20px;accent-color:var(--md-primary)}.dashcam-segment-inline-check{width:28px;height:28px;flex:0 0 28px;display:grid;place-items:center;border-radius:6px}.dashcam-segment-inline-check input{width:18px;height:18px;accent-color:var(--md-primary)}.dashcam-segment-body{min-width:0;flex:1}.dashcam-segment-topline{min-width:0;display:flex;align-items:center;gap:var(--sp-xs)}.dashcam-segment-badge{min-width:0;display:inline-flex;align-items:center;min-height:22px;padding:0 8px;border-radius:999px;border:1px solid color-mix(in srgb,var(--md-outline-var) 40%,transparent);background:var(--md-surface-cont-h);color:var(--md-on-surface-var);font-size:11px;font-weight:850;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dashcam-segment-read-status{min-width:0;display:inline-flex;flex:0 0 auto;margin-left:auto}.dashcam-segment-read-status[hidden]{display:none}.dashcam-segment-name{margin-top:6px;font-size:13px;font-weight:650;color:var(--md-on-surface);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dashcam-menu-btn{width:30px;height:30px;border-radius:999px;flex:0 0 auto}.dashcam-menu-btn svg{display:block;transform:translateZ(0)}.dashcam-group-menu-btn{margin-left:auto;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;padding-inline:12px}.dashcam-group-menu-btn svg{width:18px;height:18px}.dashcam-group-menu-btn--row{display:inline-flex;margin-left:auto}.logs-loading-list{display:flex;flex-direction:column;gap:10px;pointer-events:none}.logs-loading-card,.logs-loading-row{position:relative;overflow:hidden;border-radius:8px;border:1px solid color-mix(in srgb,var(--md-outline-var) 36%,transparent);background:var(--md-surface-cont-h);opacity:0;transform:translateY(12px);animation:uiStaggerIn .38s cubic-bezier(.2,0,0,1) both;animation-delay:min(calc(var(--i, 0) * 36ms),.42s)}.logs-loading-card{min-height:184px}.logs-loading-row{min-height:64px}.logs-loading-card:before,.logs-loading-row:before{content:"";position:absolute;inset:0;background:linear-gradient(100deg,transparent 0%,color-mix(in srgb,var(--md-on-surface) 7%,transparent) 48%,transparent 72%);transform:translate(-100%);animation:logsSkeletonSweep 1.35s ease-in-out infinite;animation-delay:calc(var(--i, 0) * 90ms);z-index:1}.logs-loading-card:after,.logs-loading-row:after{content:"";position:absolute;inset:12px;border-radius:6px;background:linear-gradient(var(--md-surface-cont),var(--md-surface-cont)) 0 0 / 100% 104px no-repeat,linear-gradient(var(--md-surface-cont),var(--md-surface-cont)) 0 122px / 62% 14px no-repeat,linear-gradient(var(--md-surface-cont),var(--md-surface-cont)) 0 146px / 38% 12px no-repeat;z-index:0}.logs-loading-row:after{inset:8px 10px;background:linear-gradient(var(--md-surface-cont),var(--md-surface-cont)) 0 0 / 76px 48px no-repeat,linear-gradient(var(--md-surface-cont),var(--md-surface-cont)) 90px 7px / 52% 14px no-repeat,linear-gradient(var(--md-surface-cont),var(--md-surface-cont)) 90px 31px / 34% 11px no-repeat}@keyframes logsSkeletonSweep{to{transform:translate(100%)}}@media(prefers-reduced-motion:reduce){.logs-loading-card,.logs-loading-row{animation:none;opacity:1;transform:none}.dashcam-segment-tile--append{animation:none}.dashcam-segment-loader.is-loading:before{animation:none}.logs-loading-card:before,.logs-loading-row:before{animation:none;opacity:0}}.dashcam-share-card{display:grid;gap:10px;min-height:0}.dashcam-share-card__summary{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;color:var(--md-on-surface);font-size:13px;font-weight:750}.dashcam-share-card__summary span:last-child{color:var(--md-on-surface-var);font-size:12px;font-weight:800;text-align:right}.dashcam-share-card pre{margin:0;max-height:min(42vh,360px);overflow:auto;padding:12px;border-radius:8px;border:1px solid color-mix(in srgb,var(--md-outline-var) 46%,transparent);background:color-mix(in srgb,var(--md-surface-cont-l) 84%,#000);color:var(--md-on-surface);font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;word-break:break-word}#appDialog:has(.dashcam-share-dialog) .app-dialog__choiceBtn{min-height:40px;text-align:center!important;justify-content:center}#appDialog:has(.dashcam-share-dialog){align-items:center;padding:8px}#appDialog:has(.dashcam-share-dialog) .app-dialog__sheet{max-height:calc(var(--app-vv-height, 100dvh) - 16px)}#appDialog:has(.dashcam-share-dialog) .app-dialog__body{min-height:0}#appDialog:has(.dashcam-share-dialog) .app-dialog__actions{margin-top:10px}.dashcam-upload-progress{position:fixed;inset:0;z-index:170;display:grid;place-items:center;padding:18px;background:var(--popup-backdrop);opacity:0;transition:opacity .16s ease}.dashcam-upload-progress.is-open{opacity:1}.dashcam-upload-progress__sheet{width:min(92vw,430px);padding:18px;border-radius:var(--popup-radius);border:1px solid var(--popup-border-color);background:var(--popup-bg);box-shadow:var(--popup-shadow)}.dashcam-upload-progress__title{color:var(--md-on-surface);font-size:17px;font-weight:850}.dashcam-upload-progress__message{margin-top:8px;color:var(--md-on-surface-var);font-size:13px;font-weight:700}.dashcam-upload-progress__bar{height:4px;margin-top:16px;overflow:hidden;border-radius:999px;background:color-mix(in srgb,var(--md-outline-var) 30%,transparent)}.dashcam-upload-progress__bar span{display:block;width:42%;height:100%;border-radius:inherit;background:var(--md-primary);animation:dashcam-upload-indeterminate 1.1s ease-in-out infinite}.dashcam-upload-progress__summary{margin-top:10px;color:var(--md-on-surface-var);font-size:12px;font-weight:800}.dashcam-upload-progress__actions{display:flex;justify-content:flex-end;margin-top:14px}.dashcam-upload-progress__cancel{min-height:38px;padding-inline:16px;border-radius:var(--dialog-control-radius);border-color:color-mix(in srgb,var(--md-error) 42%,var(--md-outline-var));color:color-mix(in srgb,var(--md-error) 82%,var(--md-on-surface));background:transparent}.dashcam-upload-progress__cancel:disabled{cursor:wait;opacity:.62}@media(min-aspect-ratio:13/10),(horizontal-viewport-segments:2),(vertical-viewport-segments:2),(min-width:640px)and (min-height:650px){.dashcam-share-card{gap:8px}.dashcam-share-card pre{max-height:min(42vh,190px);padding:10px;font-size:11px;line-height:1.45}#appDialog:has(.dashcam-share-dialog) .app-dialog__sheet{width:min(72vw,520px);max-height:calc(var(--app-vv-height, 100dvh) - 16px);padding:12px 14px}#appDialog:has(.dashcam-share-dialog) .app-dialog__head{margin-bottom:6px}#appDialog:has(.dashcam-share-dialog) .app-dialog__title{font-size:14px}#appDialog:has(.dashcam-share-dialog) .app-dialog__actions{margin-top:8px}.dashcam-upload-progress{padding:8px}.dashcam-upload-progress__sheet{width:min(70vw,520px);max-height:calc(var(--app-vv-height, 100dvh) - 16px);overflow:auto;padding:12px}.dashcam-upload-progress__title{font-size:14px}.dashcam-upload-progress__message{margin-top:5px;font-size:12px}.dashcam-upload-progress__bar{margin-top:10px}.dashcam-upload-progress__summary{margin-top:8px;font-size:11px}.dashcam-upload-progress__actions{margin-top:10px}.dashcam-upload-progress__cancel{min-height:34px}}@keyframes dashcam-upload-indeterminate{0%{transform:translate(-115%)}to{transform:translate(260%)}}.dashcam-player-overlay{position:fixed;inset:0;z-index:calc(var(--z-toast) - 1);display:grid;place-items:center;padding:16px;background:#000000ad;opacity:0;transition:opacity .16s ease}.dashcam-player-overlay.is-open{opacity:1}.dashcam-player-overlay.is-menu-open,.dashcam-player-overlay.is-action-open{z-index:calc(var(--z-modal) - 1)}.dashcam-player-dialog{width:auto;max-width:min(92vw,960px);max-height:min(78vh,680px)}.dashcam-player-frame{position:relative;overflow:hidden;border-radius:8px;border:1px solid rgba(255,255,255,.16);background:#000;box-shadow:0 20px 60px #00000070}.dashcam-player-video{width:auto;height:auto;max-width:min(92vw,960px);max-height:min(78vh,680px);display:block;background:#000;object-fit:contain}.dashcam-player-frame .plyr{width:auto;max-width:min(92vw,960px);max-height:min(78vh,680px);background:#000;--plyr-color-main: var(--md-primary)}.dashcam-player-frame .plyr video{max-width:min(92vw,960px);max-height:min(78vh,680px);object-fit:contain;background:#000}.dashcam-player-frame .plyr__controls{z-index:5}.dashcam-player-frame .plyr__control--overlaid{display:none!important}.dashcam-player-transport{z-index:4;pointer-events:auto}.dashcam-player-frame .plyr:-webkit-full-screen,.dashcam-player-frame .plyr:fullscreen,.dashcam-player-frame .plyr.plyr--fullscreen-active{width:100vw!important;height:100vh!important;max-width:none;max-height:none;display:grid;place-items:center;background:#000}.dashcam-player-frame .plyr:-webkit-full-screen video,.dashcam-player-frame .plyr:fullscreen video,.dashcam-player-frame .plyr.plyr--fullscreen-active video{width:100vw!important;height:100vh!important;max-width:none;max-height:none;object-fit:contain;object-position:center center}.dashcam-player-frame .plyr__video-wrapper{display:grid;place-items:center;background:#000}.dashcam-player-frame .plyr:-webkit-full-screen .plyr__video-wrapper,.dashcam-player-frame .plyr:fullscreen .plyr__video-wrapper,.dashcam-player-frame .plyr.plyr--fullscreen-active .plyr__video-wrapper{width:100vw;height:100vh}.dashcam-player-frame .plyr__control svg{display:block;margin:auto}.dashcam-player-overlay--dashcam .dashcam-player-dialog:not(.dashcam-player-dialog--browsable){width:min(84vw,760px);max-width:min(96vw,920px);max-height:min(84vh,760px)}.dashcam-player-overlay--dashcam .dashcam-player-video{width:100%;height:auto;max-width:none;max-height:min(84vh,760px)}.dashcam-player-overlay--dashcam .dashcam-player-frame .plyr,.dashcam-player-overlay--dashcam .dashcam-player-frame .plyr video{width:100%;max-width:none;max-height:min(84vh,760px)}.dashcam-player-top{position:absolute;left:0;right:0;top:0;z-index:3;display:flex;align-items:center;gap:8px;padding:10px;background:linear-gradient(180deg,rgba(0,0,0,.62),transparent);pointer-events:none;transition:opacity .2s ease,transform .2s ease}.dashcam-player-toast{display:none;position:absolute;top:50%;left:50%;z-index:5;padding:12px 22px;border-radius:999px;background:#000000bd;color:#fff;font-weight:700;font-size:18px;letter-spacing:.02em;white-space:nowrap;pointer-events:none;opacity:0;transform:translate(-50%,-50%) scale(.92);transition:opacity .14s ease,transform .14s ease;backdrop-filter:blur(8px);box-shadow:0 10px 28px #0000005c}.dashcam-player-toast.is-visible{opacity:1;transform:translate(-50%,-50%) scale(1)}.dashcam-player-title{min-width:0;flex:1;color:#fff;font-weight:800;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dashcam-player-heading{min-width:0;flex:1}.dashcam-player-subtitle{margin-top:2px;color:#fffc;font-size:11px;font-weight:650;line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-variant-numeric:tabular-nums}.dashcam-player-close{pointer-events:auto}.dashcam-player-action{width:var(--close-size, 32px);height:var(--close-size, 32px);border:1px solid color-mix(in srgb,var(--md-outline-var) 40%,transparent);border-radius:var(--close-radius, 10px);color:#fff;background:color-mix(in srgb,var(--md-surface-cont) 82%,#000);pointer-events:auto;transition:background var(--motion-quick) var(--ease-standard),border-color var(--motion-quick) var(--ease-standard),color var(--motion-quick) var(--ease-standard)}.dashcam-player-action svg{width:var(--close-icon, 16px);height:var(--close-icon, 16px)}.dashcam-player-action:hover{background:color-mix(in srgb,var(--md-primary) 14%,var(--md-surface-cont));border-color:color-mix(in srgb,var(--md-primary) 32%,var(--md-outline-var));color:#fff}.dashcam-player-action:disabled{opacity:.55;cursor:default}.screenrecord-list{flex:1 1 auto;min-height:0;overflow:auto;overscroll-behavior:contain;scrollbar-gutter:stable;padding:10px var(--logs-content-inline) calc(var(--app-nav-bottom-gap, var(--nav-bar-height)) + 16px + env(safe-area-inset-bottom,0px));display:flex;flex-direction:column;gap:8px}.dashcam-routes.is-loading-more:after,.screenrecord-list.is-loading-more:after{content:"";flex:0 0 auto;width:22px;height:22px;margin:10px auto 2px;border:2px solid color-mix(in srgb,var(--md-outline-var) 48%,transparent);border-top-color:var(--md-primary);border-radius:999px;animation:screenrecord-loading-spin .78s linear infinite}@keyframes screenrecord-loading-spin{to{transform:rotate(360deg)}}.screenrecord-row{flex:0 0 auto;min-height:64px;display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;border:1px solid color-mix(in srgb,var(--md-outline-var) 44%,transparent);background:var(--md-surface-cont-h);color:var(--md-on-surface);cursor:pointer;content-visibility:auto;contain-intrinsic-size:auto 64px}.screenrecord-list .screenrecord-row.ui-stagger-item{animation:none}.screenrecord-virtual-spacer{flex:0 0 auto;width:100%;min-height:0;pointer-events:none}.screenrecord-row:hover{border-color:color-mix(in srgb,var(--md-primary) 38%,var(--md-outline-var));background:color-mix(in srgb,var(--md-surface-cont-h) 88%,var(--md-primary))}.screenrecord-row__icon{width:38px;height:38px;flex:0 0 38px;display:grid;place-items:center;border-radius:8px;background:color-mix(in srgb,var(--md-primary) 12%,var(--md-surface-cont));color:var(--md-primary);overflow:hidden}.screenrecord-row__thumb{width:76px;height:48px;flex:0 0 76px;display:block;border-radius:6px;background:var(--md-surface-cont);overflow:hidden}.screenrecord-row__thumb img{width:100%;height:100%;object-fit:cover;display:block}.screenrecord-row__icon svg,.screenrecord-download svg{width:21px;height:21px}.screenrecord-row__main{min-width:0;flex:1 1 auto}.screenrecord-row__name{font-size:14px;line-height:1.25;font-weight:750;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.screenrecord-row__meta{margin-top:6px;display:flex;flex-wrap:wrap;gap:6px 10px;color:var(--md-on-surface-var);font-size:12px;line-height:1.25}.screenrecord-download{appearance:none;border:0;width:36px;height:36px;flex:0 0 36px;display:grid;place-items:center;border-radius:8px;color:var(--md-on-surface-var);background:transparent;cursor:pointer}.screenrecord-download:hover{color:var(--md-primary);background:color-mix(in srgb,var(--md-primary) 10%,transparent)}@media(min-width:900px){.logs-shell{--logs-content-inline: clamp(18px, 3vw, 34px);--logs-route-media-column: minmax(260px, 34%)}.dashcam-route-card{display:grid;grid-template-columns:var(--logs-route-media-column) minmax(0,1fr);align-items:start}.dashcam-route-preview{width:100%}.dashcam-route-media-info{display:block}.dashcam-route-main{padding:10px 14px 12px}.dashcam-route-head{justify-content:flex-end;min-height:34px}.dashcam-route-head .dashcam-route-titleblock{display:none}}@media(min-aspect-ratio:13/10),(horizontal-viewport-segments:2),(vertical-viewport-segments:2),(min-width:640px)and (min-height:650px){.logs-shell{--logs-route-media-column: minmax(230px, 32%)}.dashcam-route-card{display:grid;grid-template-columns:var(--logs-route-media-column) minmax(0,1fr);align-items:stretch;height:clamp(178px,45vh,236px)}.dashcam-route-media{min-height:0;height:100%;display:flex;flex-direction:column}.dashcam-route-preview{flex:1 1 auto;min-height:0;width:100%;aspect-ratio:auto}.dashcam-route-media-info{display:block;flex:0 0 auto;min-height:0;overflow:hidden}.dashcam-route-main{min-height:0;height:100%;display:flex;flex-direction:column;padding-top:8px;padding-right:0}.dashcam-route-head{display:none}.dashcam-segments,.dashcam-segments.is-collapsed{min-height:0;flex:1 1 auto;display:grid;grid-template-columns:minmax(0,1fr) 96px;gap:8px;margin-top:0}.dashcam-selection-row{grid-column:2;grid-row:1;min-height:0;height:100%;flex-direction:column;justify-content:center;align-items:center;gap:6px;box-sizing:border-box;overflow-y:auto;padding:8px;border-left:1px solid color-mix(in srgb,var(--md-outline-var) 38%,transparent);scrollbar-width:none;text-align:center}.dashcam-selection-row::-webkit-scrollbar{display:none}.dashcam-selection-row .dashcam-group-menu-btn--row{margin-left:0}.dashcam-segment-list{grid-column:1;grid-row:1;flex:1 1 auto;min-height:0;max-height:none}.dashcam-selection-count{text-align:center;font-size:12px}.dashcam-selection-row .smallBtn{width:100%;min-height:30px;padding-inline:8px;font-size:12px;white-space:normal;line-height:1.15;overflow-wrap:anywhere}.dashcam-selection-row .btn--filled{padding-inline:8px}.dashcam-segment-tile--compact{min-height:46px;gap:8px;padding:5px 8px 5px 5px;contain-intrinsic-size:46px 100%}.dashcam-segment-thumb--compact{width:56px;height:36px;flex:0 0 56px}.dashcam-segment-check--compact input{width:16px;height:16px}.dashcam-segment-tile--compact .dashcam-segment-body{min-width:0;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:8px}.dashcam-segment-tile--compact .dashcam-segment-topline{display:contents}.dashcam-segment-tile--compact .dashcam-segment-badge{grid-column:1;grid-row:1;min-height:20px;padding-inline:7px;font-size:10.5px}.dashcam-segment-tile--compact .dashcam-segment-name{min-width:0;grid-column:2;grid-row:1;margin-top:0;font-size:12.5px}.dashcam-segment-tile--compact .dashcam-segment-read-status{grid-column:3;grid-row:1;margin-left:0}.dashcam-segment-tile--compact .dashcam-menu-btn{width:26px;height:26px}}@media(max-height:620px)and (min-aspect-ratio:13/10),(max-height:620px)and (horizontal-viewport-segments:2),(max-height:620px)and (vertical-viewport-segments:2),(min-width:640px)and (min-height:650px)and (max-height:620px){.logs-tabbar{padding-top:6px;padding-bottom:4px;--logs-tab-min-height: 32px;--logs-tab-font-size: 12.5px}.logs-menu__button{min-height:38px}.dashcam-routes,.screenrecord-list{gap:8px;padding-top:4px}.dashcam-route-card{display:grid;grid-template-columns:var(--logs-route-media-column) minmax(0,1fr);align-items:stretch;height:clamp(168px,46vh,196px)}.dashcam-route-media-info{display:block;padding:8px 10px 10px}.dashcam-route-title{font-size:14px}.dashcam-route-subtitle{font-size:11.5px}.dashcam-selection-row{padding:6px 6px 6px 8px}.dashcam-segments,.dashcam-segments.is-collapsed{grid-template-columns:minmax(0,1fr) 86px}.dashcam-segment-list{max-height:none}.dashcam-player-dialog{width:auto;max-width:min(76vw,760px);max-height:68vh}.dashcam-player-video,.dashcam-player-frame .plyr,.dashcam-player-frame .plyr video{max-width:min(76vw,760px);max-height:68vh}.dashcam-player-title{font-size:12.5px}}.dashcam-player-dialog,.dashcam-segment-read-status{--logs-player-status-reading-surface: var(--md-primary);--logs-player-status-reading-ink: var(--md-on-primary);--logs-player-status-recent-surface: var(--md-primary-state-soft);--logs-player-status-recent-ink: var(--md-primary);--logs-player-status-recent-border: color-mix(in srgb, var(--md-primary) 36%, transparent)}.dashcam-player-dialog{--logs-player-dialog-radius: var(--r-md);--logs-player-dialog-border: color-mix(in srgb, var(--md-outline-var) 42%, transparent);--logs-player-dialog-surface: var(--md-surface-cont);--logs-player-dialog-wide: 88rem;--logs-player-dialog-max-height: 40rem;--logs-player-dialog-landscape-aspect: 12 / 5;--logs-player-dialog-portrait-wide: 42rem;--logs-player-dialog-portrait-max-height: 42rem;--logs-player-dialog-portrait-viewport-height: 80dvh;--logs-player-browser-pane-width: clamp(15.5rem, 22dvw, 19.5rem);--logs-player-browser-pane-compact-width: clamp(15rem, 31dvw, 16.5rem);--logs-player-browser-divider: color-mix(in srgb, var(--md-outline-var) 34%, transparent);--logs-player-browser-surface: var(--md-surface-cont);--logs-player-section-padding-inline: var(--sp-md);--logs-player-section-padding-block: var(--sp-sm);--logs-player-summary-gap: var(--sp-sm);--logs-player-summary-count-surface: var(--md-primary-state-soft);--logs-player-summary-count-ink: var(--md-primary);--logs-player-summary-muted: color-mix(in srgb, var(--md-on-surface-var) 72%, transparent);--logs-player-segment-list-gap: var(--sp-xs);--logs-player-segment-list-padding: var(--sp-sm);--logs-player-segment-item-min-height: 4.5rem;--logs-player-segment-item-padding: var(--sp-xs);--logs-player-segment-item-radius: var(--r-sm);--logs-player-segment-item-surface: color-mix(in srgb, var(--md-on-surface) 3%, transparent);--logs-player-segment-item-hover: color-mix(in srgb, var(--md-on-surface) 7%, transparent);--logs-player-segment-item-active: var(--md-primary-state-soft);--logs-player-segment-item-border: color-mix(in srgb, var(--md-outline-var) 28%, transparent);--logs-player-segment-item-active-border: color-mix(in srgb, var(--md-primary) 62%, var(--md-outline-var));--logs-player-segment-thumb-width: 5.5rem;--logs-player-segment-thumb-radius: var(--r-xs);--logs-player-segment-thumb-surface: var(--md-surface-cont-h);--logs-player-segment-muted: color-mix(in srgb, var(--md-on-surface-var) 70%, transparent)}.dashcam-player-layout{width:100%;min-width:0;min-height:0}.dashcam-player-dialog--browsable{--logs-player-browser-current-width: var(--logs-player-browser-pane-width);width:min(calc(var(--app-vv-width, 100dvw) - var(--sp-lg) - var(--sp-lg) - env(safe-area-inset-left,0px) - env(safe-area-inset-right,0px)),var(--logs-player-dialog-wide));height:auto;max-height:min(calc(var(--app-vv-height, 100dvh) - var(--sp-lg) - var(--sp-lg) - env(safe-area-inset-top,0px) - env(safe-area-inset-bottom,0px)),var(--logs-player-dialog-max-height));max-width:none;aspect-ratio:var(--logs-player-dialog-landscape-aspect);overflow:hidden;border:1px solid var(--logs-player-dialog-border);border-radius:var(--logs-player-dialog-radius);background:var(--logs-player-dialog-surface);container-name:logs-player-dialog;container-type:inline-size}.dashcam-player-dialog--browsable .dashcam-player-layout{height:100%;display:grid;grid-template-columns:minmax(0,1fr) var(--logs-player-browser-current-width)}.dashcam-player-dialog--browsable .dashcam-player-frame{min-width:0;min-height:0;height:100%;border:0;border-radius:0;box-shadow:none}.dashcam-player-dialog--browsable .dashcam-player-video,.dashcam-player-dialog--browsable .dashcam-player-frame .plyr,.dashcam-player-dialog--browsable .dashcam-player-frame .plyr video{width:100%;height:100%;max-width:none;max-height:none;object-fit:contain}.dashcam-player-browser{min-width:0;min-height:0;display:grid;grid-template-rows:auto minmax(0,1fr);overflow:hidden;border-left:1px solid var(--logs-player-browser-divider);background:var(--logs-player-browser-surface)}.dashcam-player-group-summary{min-width:0;display:grid;gap:var(--logs-player-summary-gap);padding:var(--logs-player-section-padding-block) var(--logs-player-section-padding-inline) var(--sp-md)}.dashcam-player-section-title{margin:0;color:var(--md-on-surface);font-size:var(--fs-title-xs);font-weight:850;line-height:1.3}.dashcam-player-group-summary__body{min-width:0;display:grid;gap:var(--sp-xs)}.dashcam-player-group-summary__main{min-width:0;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:var(--sp-sm)}.dashcam-player-group-summary__title{min-width:0;overflow:hidden;color:var(--md-on-surface);font-size:var(--fs-body-sm);font-weight:850;line-height:1.3;text-overflow:ellipsis;white-space:nowrap}.dashcam-player-group-summary__count{flex:0 0 auto;padding:var(--sp-xs) var(--sp-sm);border-radius:var(--r-round);background:var(--logs-player-summary-count-surface);color:var(--logs-player-summary-count-ink);font-size:var(--fs-label-sm);font-weight:850;line-height:1.2;white-space:nowrap}.dashcam-player-group-summary__time{min-width:0;overflow:hidden;color:var(--logs-player-summary-muted);font-size:var(--fs-label-sm);font-variant-numeric:tabular-nums;font-weight:700;line-height:1.35;text-overflow:ellipsis;white-space:nowrap}.dashcam-player-segment-region[hidden]{display:none!important}.dashcam-player-segment-region{min-width:0;min-height:0;overflow:hidden;border-top:1px solid var(--logs-player-browser-divider)}.dashcam-player-segment-browser{min-width:0;min-height:0;height:100%;display:grid;grid-template-rows:auto minmax(0,1fr)}.dashcam-player-segment-browser>.dashcam-player-section-title{padding:var(--logs-player-section-padding-block) var(--logs-player-section-padding-inline)}.dashcam-player-segment-list{min-width:0;min-height:0;display:grid;grid-auto-rows:max-content;align-content:start;gap:var(--logs-player-segment-list-gap);overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;padding:0 var(--logs-player-segment-list-padding) var(--logs-player-segment-list-padding);scrollbar-width:thin}.dashcam-player-segment-item{width:100%;min-width:0;min-height:var(--logs-player-segment-item-min-height);display:grid;grid-template-columns:var(--logs-player-segment-thumb-width) minmax(0,1fr);align-items:center;gap:var(--sp-sm);padding:var(--logs-player-segment-item-padding);border:1px solid var(--logs-player-segment-item-border);border-radius:var(--logs-player-segment-item-radius);background:var(--logs-player-segment-item-surface);color:var(--md-on-surface);font:inherit;text-align:left;cursor:pointer;transition:border-color var(--motion-quick) var(--ease-standard),background var(--motion-quick) var(--ease-standard)}.dashcam-player-segment-item:hover{background:var(--logs-player-segment-item-hover)}.dashcam-player-segment-item:focus-visible{border-color:var(--logs-player-segment-item-active-border);outline:var(--focus-ring-width) solid var(--focus-ring-color);outline-offset:var(--focus-ring-offset)}.dashcam-player-segment-item.is-active{border-color:var(--logs-player-segment-item-active-border);background:var(--logs-player-segment-item-active)}.dashcam-player-segment-thumb{position:relative;width:100%;overflow:hidden;border-radius:var(--logs-player-segment-thumb-radius);aspect-ratio:16 / 9;background:var(--logs-player-segment-thumb-surface)}.dashcam-player-segment-thumb img{width:100%;height:100%;display:block;object-fit:cover}.dashcam-player-segment-copy{min-width:0;display:grid;gap:var(--sp-xs)}.dashcam-player-segment-meta{min-width:0;display:flex;align-items:center;justify-content:space-between;gap:var(--sp-xs)}.dashcam-player-segment-status{min-width:0;display:inline-flex;flex:0 0 auto}.dashcam-player-segment-status[hidden]{display:none}.dashcam-player-segment-time,.dashcam-player-segment-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dashcam-player-segment-time{color:var(--md-on-surface);font-size:var(--fs-body-sm);font-variant-numeric:tabular-nums;font-weight:850;line-height:1.3}.dashcam-player-segment-name{color:var(--logs-player-segment-muted);font-size:var(--fs-label-sm);font-weight:700;line-height:1.3}.dashcam-player-status-tag{min-width:0;display:inline-flex;align-items:center;justify-content:center;padding:var(--sp-xs) var(--sp-sm);border:1px solid transparent;border-radius:var(--r-round);font-size:var(--fs-label-sm);font-weight:850;line-height:1.2;white-space:nowrap}.dashcam-player-status-tag[data-state=reading]{background:var(--logs-player-status-reading-surface);color:var(--logs-player-status-reading-ink)}.dashcam-player-status-tag[data-state=recent]{border-color:var(--logs-player-status-recent-border);background:var(--logs-player-status-recent-surface);color:var(--logs-player-status-recent-ink)}@container logs-player-dialog (max-width: 46rem){.dashcam-player-layout{--logs-player-segment-thumb-width: 4.75rem}}@media(orientation:portrait){.dashcam-player-dialog--browsable{width:min(calc(var(--app-vv-width, 100dvw) - var(--sp-md) - var(--sp-md) - env(safe-area-inset-left,0px) - env(safe-area-inset-right,0px)),var(--logs-player-dialog-portrait-wide));height:min(calc(var(--app-vv-height, 100dvh) - var(--sp-lg) - var(--sp-lg) - env(safe-area-inset-top,0px) - env(safe-area-inset-bottom,0px)),var(--logs-player-dialog-portrait-viewport-height),var(--logs-player-dialog-portrait-max-height));max-height:none;aspect-ratio:auto}.dashcam-player-dialog--browsable .dashcam-player-layout{grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr)}.dashcam-player-dialog--browsable .dashcam-player-frame{height:auto;aspect-ratio:16 / 9}.dashcam-player-browser{border-top:1px solid var(--logs-player-browser-divider);border-left:0}}@media(orientation:landscape)and (max-height:30rem){.dashcam-player-dialog--browsable{--logs-player-browser-current-width: var(--logs-player-browser-pane-compact-width);width:min(calc(var(--app-vv-width, 100dvw) - var(--sp-md) - var(--sp-md) - env(safe-area-inset-left,0px) - env(safe-area-inset-right,0px)),var(--logs-player-dialog-wide));max-height:calc(var(--app-vv-height, 100dvh) - var(--sp-md) - var(--sp-md) - env(safe-area-inset-top,0px) - env(safe-area-inset-bottom,0px))}}@media(prefers-reduced-motion:reduce){.dashcam-player-dialog--browsable,.dashcam-player-dialog--browsable *{transition:none!important}}.route-summary,.route-summary-state{--route-summary-gap: var(--sp-md);--route-summary-section-gap: var(--sp-sm);--route-summary-radius: var(--r-md);--route-summary-radius-small: var(--r-sm);--route-summary-border: color-mix(in srgb, var(--md-outline-var) 46%, transparent);--route-summary-divider: color-mix(in srgb, var(--md-outline-var) 30%, transparent);--route-summary-surface: var(--md-surface-cont-l);--route-summary-surface-raised: var(--md-surface-cont-h);--route-summary-text: var(--md-on-surface);--route-summary-muted: color-mix(in srgb, var(--md-on-surface-var) 76%, transparent);--route-summary-accent: var(--md-primary);--route-summary-accent-soft: var(--md-primary-state-soft);--route-summary-auto: var(--md-info);--route-summary-manual: var(--md-error);--route-summary-manual-soft: color-mix(in srgb, var(--md-error) 35%, transparent);--route-summary-gas: var(--md-warning);--route-summary-brake: var(--md-primary);--route-summary-event-over: var(--md-warning);--route-summary-event-over-soft: var(--md-warning-cont);--route-summary-event-hard: var(--md-error);--route-summary-event-hard-soft: color-mix(in srgb, var(--md-error-cont) 24%, var(--md-surface-cont-h));--route-summary-danger: var(--md-error);--route-summary-danger-soft: color-mix(in srgb, var(--md-error-cont) 24%, var(--md-surface-cont-h));--route-summary-progress-track: var(--md-surface-cont-h);--route-summary-progress-fill: var(--md-primary);--route-summary-progress-height: var(--sp-xs);--route-summary-progress-motion: var(--motion-medium);--route-summary-progress-cycle: calc(var(--motion-long) + var(--motion-long) + var(--motion-long));--route-summary-distribution-height: var(--sp-lg)}.route-summary,.route-summary-state{box-sizing:border-box;width:100%;color:var(--route-summary-text);font-family:var(--font-sans);text-align:left;white-space:normal}.route-summary{display:grid;gap:var(--route-summary-gap);container-name:route-summary;container-type:inline-size}#appDialog:has(.route-summary) .app-dialog__sheet{width:min(100%,var(--content-max-width))}.route-summary-hero__subtitle{display:grid;justify-items:start;gap:1px;margin:0;color:var(--route-summary-muted);font:700 var(--fs-label-sm)/1.45 var(--font-sans);font-variant-numeric:tabular-nums}.route-summary-hero__date,.route-summary-hero__range{white-space:nowrap}.route-summary__section{min-width:0;display:grid;align-content:start;gap:var(--route-summary-section-gap);padding:var(--sp-md);border:1px solid var(--route-summary-border);border-radius:var(--route-summary-radius);background:var(--route-summary-surface)}.route-summary__section-title{margin:0;color:var(--route-summary-text);font-size:var(--fs-title-xs);font-weight:800;line-height:1.35}.route-summary__dashboard{min-width:0;display:grid;align-items:start;gap:var(--route-summary-gap)}.route-summary-hero{min-width:0;display:grid;gap:var(--sp-md)}.route-summary-hero__time{min-width:0;display:grid;align-content:start;gap:var(--sp-xs)}.route-summary__metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start;gap:var(--sp-md);margin:0}.route-summary__metrics--distance{padding-top:var(--sp-xs)}.route-summary-metric{min-width:0;display:grid;gap:var(--sp-xs)}.route-summary-metric__label,.route-summary-detail__label{color:var(--route-summary-muted);font-size:var(--fs-label-sm);font-weight:700;line-height:1.35}.route-summary-metric__value{min-width:0;display:flex;align-items:baseline;gap:var(--sp-xs);margin:0;font-variant-numeric:tabular-nums}.route-summary-metric__number{overflow:hidden;font-size:var(--fs-title-md);font-weight:850;line-height:1.2;text-overflow:ellipsis;white-space:nowrap}.route-summary-metric[data-variant=hero] .route-summary-metric__number{color:var(--route-summary-accent);font-size:clamp(var(--fs-title-lg),5cqi,var(--fs-headline))}.route-summary-metric[data-variant=compact] .route-summary-metric__number{font-size:var(--fs-title-sm)}.route-summary-metric__unit{color:var(--route-summary-muted);font-size:var(--fs-label-sm);font-weight:700}.route-summary-events,.route-summary-details{display:grid;gap:0;overflow:hidden;border:0;border-radius:var(--route-summary-radius-small);background:transparent}.route-summary-event,.route-summary-detail{min-width:0;border-top:1px solid var(--route-summary-divider);background:transparent}.route-summary-event:first-child,.route-summary-detail:first-child{border-top:0}.route-summary-composition{display:grid;gap:var(--sp-xs) var(--sp-md)}.route-summary-composition__stat{min-width:0;display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:var(--sp-xs);align-items:baseline}.route-summary-composition__label,.route-summary-event__label{min-width:0;overflow:hidden;font-size:var(--fs-body-sm);font-weight:750;line-height:1.4;text-overflow:ellipsis;white-space:nowrap}.route-summary-composition__label{display:inline-flex;align-items:center;gap:var(--sp-xs)}.route-summary-composition__label:before{width:.55em;height:.55em;flex:0 0 auto;border-radius:var(--r-round);background:currentColor;content:""}.route-summary-composition__duration,.route-summary-composition__ratio{color:var(--route-summary-muted);font:750 var(--fs-label-sm)/1.35 var(--font-mono);font-variant-numeric:tabular-nums}.route-summary-composition__stat[data-kind=auto]{color:var(--route-summary-auto)}.route-summary-composition__stat[data-kind=manual]{color:var(--route-summary-manual)}.route-summary-composition__stat[data-kind=gas]{color:var(--route-summary-gas)}.route-summary-composition__stat[data-kind=brake]{color:var(--route-summary-brake)}.route-summary-composition__stat[data-kind] .route-summary-composition__duration,.route-summary-composition__stat[data-kind] .route-summary-composition__ratio{color:inherit}.route-summary-distribution{display:flex;width:100%;height:var(--route-summary-distribution-height);overflow:hidden;border:1px solid var(--route-summary-border);border-radius:var(--r-sm);background:var(--route-summary-surface-raised)}.route-summary-distribution__segment,.route-summary-distribution__manual{display:block;flex:var(--route-summary-weight, 0) 1 0%;height:100%;box-sizing:border-box}.route-summary-distribution__manual{min-width:0;display:flex;overflow:hidden;border:2px solid var(--route-summary-manual);background:var(--route-summary-manual-soft)}.route-summary-distribution__manual>*+*{border-left:1px solid var(--route-summary-surface)}.route-summary-distribution__segment[data-kind=auto]{background:var(--route-summary-auto)}.route-summary-distribution__segment[data-kind=manual]{background:var(--route-summary-manual)}.route-summary-distribution__segment[data-kind=gas]{background:var(--route-summary-gas)}.route-summary-distribution__segment[data-kind=brake]{background:var(--route-summary-brake)}.route-summary-distribution__segment[data-kind=other]{background:var(--route-summary-manual-soft)}.route-summary-event{display:grid;gap:var(--sp-sm);padding:var(--sp-sm) var(--sp-md)}.route-summary-event[data-tone=danger]{background:var(--route-summary-danger-soft)}.route-summary-event__head{display:flex;align-items:center;gap:var(--sp-sm)}.route-summary-event__count{min-width:2.25em;margin-left:auto;padding:2px var(--sp-sm);border-radius:var(--r-round);background:var(--route-summary-surface-raised);color:var(--route-summary-text);font:800 var(--fs-label-sm)/1.4 var(--font-mono);text-align:center}.route-summary-event[data-tone=danger] .route-summary-event__count{background:var(--route-summary-danger);color:var(--md-on-error)}.route-summary-event__times{display:flex;flex-wrap:wrap;gap:var(--sp-xs);margin:0;padding:0;list-style:none}.route-summary-event__time{display:inline-flex;align-items:baseline;gap:var(--sp-xs);padding:3px var(--sp-sm);border:1px solid color-mix(in srgb,var(--route-summary-event-over) 42%,transparent);border-radius:var(--r-sm);background:var(--route-summary-event-over-soft);color:var(--route-summary-event-over);font:700 var(--fs-label-sm)/1.35 var(--font-mono);font-variant-numeric:tabular-nums}.route-summary-event[data-tone=danger] .route-summary-event__time{border-color:color-mix(in srgb,var(--route-summary-event-hard) 46%,transparent);background:var(--route-summary-event-hard-soft);color:var(--route-summary-event-hard)}.route-summary-event__peak{color:inherit;font-weight:850}.route-summary-detail{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:var(--sp-md);align-items:baseline;padding:var(--sp-sm) var(--sp-md)}.route-summary-detail__value{margin:0;font:800 var(--fs-body-sm)/1.4 var(--font-mono);font-variant-numeric:tabular-nums;text-align:right}.route-summary__warning{margin:0;padding:var(--sp-sm) var(--sp-md);border-radius:var(--route-summary-radius-small);background:var(--route-summary-danger-soft);color:var(--route-summary-text);font-size:var(--fs-label-md);font-weight:700;line-height:1.45}.route-summary-metadata{overflow:hidden;border:1px solid var(--route-summary-border);border-radius:var(--route-summary-radius);background:var(--route-summary-surface)}.route-summary-metadata__summary{min-height:var(--control-min-height);display:flex;align-items:center;padding-inline:var(--sp-md);color:var(--route-summary-muted);font-size:var(--fs-label-md);font-weight:750;cursor:pointer}.route-summary-metadata__summary:focus-visible{outline:var(--focus-ring-width) solid var(--focus-ring-color);outline-offset:calc(0px - var(--focus-ring-width))}.route-summary-metadata__list{display:grid;gap:1px;margin:0;border-top:1px solid var(--route-summary-divider);background:var(--route-summary-divider)}.route-summary-metadata__row{min-width:0;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.5fr);gap:var(--sp-md);padding:var(--sp-sm) var(--sp-md);background:var(--route-summary-surface);font-size:var(--fs-label-sm);line-height:1.4}.route-summary-metadata__row dt{color:var(--route-summary-muted)}.route-summary-metadata__row dd{min-width:0;margin:0;overflow-wrap:anywhere;text-align:right}.route-summary-state{display:grid;place-content:center;gap:var(--sp-md);padding-block:var(--sp-xl);color:var(--route-summary-muted);font-size:var(--fs-body-sm);line-height:1.45;text-align:center}.route-summary__empty{min-height:calc(var(--control-min-height) + var(--control-min-height) + var(--sp-xl));display:grid;place-content:center;padding:var(--sp-lg);color:var(--route-summary-muted);font-size:var(--fs-body-sm);line-height:1.45;text-align:center}.route-summary-state__title{color:var(--route-summary-text);font-size:var(--fs-body-md);font-weight:800;line-height:1.4}.route-summary-state__description{color:var(--route-summary-muted);font-size:var(--fs-label-md);font-variant-numeric:tabular-nums;line-height:1.4}.route-summary-state__progress{position:relative;width:100%;height:var(--route-summary-progress-height);overflow:hidden;border-radius:var(--r-round);background:var(--route-summary-progress-track)}.route-summary-state__progress-fill{display:block;width:calc(var(--route-summary-progress, 0) * 1%);height:100%;border-radius:inherit;background:var(--route-summary-progress-fill);transition:width var(--route-summary-progress-motion) var(--ease-standard)}.route-summary-state__progress[data-mode=indeterminate] .route-summary-state__progress-fill{width:32%;animation:route-summary-progress-indeterminate var(--route-summary-progress-cycle) var(--ease-standard) infinite}@keyframes route-summary-progress-indeterminate{0%{transform:translate(-110%)}to{transform:translate(330%)}}@container route-summary (min-width: 540px){.route-summary-hero{grid-template-columns:minmax(0,.75fr) minmax(0,1.75fr);align-items:end}.route-summary-composition,.route-summary__dashboard{grid-template-columns:repeat(2,minmax(0,1fr))}.route-summary__section--events{grid-column:1 / -1}.route-summary__metrics--distance{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(prefers-reduced-motion:reduce){.route-summary *,.route-summary-state *{animation:none;scroll-behavior:auto;transition:none}}
+.page--logs{height:calc(var(--app-vv-height, 100dvh) - var(--app-nav-bottom-gap, var(--nav-bar-height)));min-height:0;background:var(--md-surface);overflow:hidden}@media(min-aspect-ratio:13/10),(horizontal-viewport-segments:2),(vertical-viewport-segments:2),(min-width:640px)and (min-height:650px){.page--logs{height:var(--page-content-height, calc(var(--app-vv-height, 100dvh) - 24px))}}.logs-shell{--logs-content-inline: var(--logs-shell-inline, clamp(12px, 3.5vw, 24px));--logs-route-media-column: minmax(0, 1fr);--logs-scrollbar-gutter: 0px;--logs-tab-track-radius: var(--r-sm);--logs-tab-inset: var(--sp-xs);--logs-tab-min-height: 36px;--logs-tab-font-size: var(--fs-label-sm);--logs-tab-font-weight: 700;height:100%;min-height:0;display:flex;flex-direction:column;max-width:1480px;margin:0 auto;overflow:hidden}.logs-tabbar{position:sticky;top:0;z-index:12;display:flex;align-items:center;justify-content:center;gap:var(--sp-md);padding:8px calc(var(--logs-content-inline) + var(--logs-scrollbar-gutter)) 6px var(--logs-content-inline);background:color-mix(in srgb,var(--md-surface) 96%,transparent);border-bottom:0;backdrop-filter:blur(18px)}.logs-tabs{--segmented-control-gap: var(--logs-tab-inset);--segmented-control-padding: var(--logs-tab-inset);--segmented-control-item-min-height: var(--logs-tab-min-height);--segmented-control-item-padding: 0 var(--sp-sm);--segmented-control-item-font-size: var(--logs-tab-font-size);--segmented-control-item-font-weight: var(--logs-tab-font-weight);--segmented-control-item-radius: calc(var(--logs-tab-track-radius) - var(--logs-tab-inset));--segmented-control-item-border: transparent;--segmented-control-item-bg: transparent;--segmented-control-hover-border: transparent;--segmented-control-hover-fg: var(--md-on-surface);--segmented-control-selected-border: transparent;--segmented-control-selected-bg: var(--md-primary);--segmented-control-selected-fg: var(--md-on-primary);width:auto;flex:1 1 auto;border-radius:var(--logs-tab-track-radius);background:var(--md-surface-cont);border:1px solid color-mix(in srgb,var(--md-outline-var) 36%,transparent)}.logs-menu{flex:0 0 auto;align-self:center}.logs-menu__button{width:var(--menu-trigger-size);height:var(--menu-trigger-size);min-height:var(--menu-trigger-size);padding:0}.logs-menu__button .ui-menu-icon{width:var(--menu-icon-size);height:var(--menu-icon-size)}.logs-panel{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden}.logs-panel[hidden]{display:none}.logs-subheader{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:12px clamp(14px,3.5vw,26px) 8px;background:color-mix(in srgb,var(--md-surface-cont) 55%,transparent);border-bottom:1px solid color-mix(in srgb,var(--md-outline-var) 45%,transparent)}.logs-subheader__title{font-size:15px;line-height:1.25;font-weight:800;color:var(--md-on-surface)}.logs-subheader__desc{margin-top:4px;font-size:13px;line-height:1.35;color:var(--md-on-surface-var);opacity:.82}.dashcam-expand-btn,.dashcam-menu-btn{appearance:none;border:0;padding:0;background:transparent;color:var(--md-on-surface-var);display:inline-flex;align-items:center;justify-content:center;line-height:0;cursor:pointer;box-sizing:border-box}.dashcam-expand-btn:hover,.dashcam-menu-btn:hover{background:color-mix(in srgb,var(--md-primary) 10%,transparent);color:var(--md-on-surface)}.dashcam-expand-btn svg,.dashcam-menu-btn svg{width:22px;height:22px}.dashcam-status{flex:0 0 auto;padding:22px clamp(14px,3.5vw,26px);color:var(--md-on-surface-var);text-align:center;font-size:14px}.dashcam-status.is-error{color:#ffb4ab}.logs-empty-state{flex:0 0 auto;display:grid;justify-items:center;width:fit-content;max-width:min(100%,360px);margin:0 auto;padding:14px 18px;border:1px solid color-mix(in srgb,var(--md-outline-var) 28%,transparent);border-radius:8px;background:color-mix(in srgb,var(--md-surface-cont) 46%,transparent);color:var(--md-on-surface);text-align:center}.logs-empty-state__title{color:var(--md-on-surface);font-size:14px;font-weight:850;line-height:1.25}.dashcam-routes{flex:1 1 auto;min-height:0;overflow:auto;overscroll-behavior:contain;overflow-anchor:none;scrollbar-gutter:stable;padding:6px var(--logs-content-inline) calc(var(--app-nav-bottom-gap, var(--nav-bar-height)) + 16px + env(safe-area-inset-bottom,0px));display:flex;flex-direction:column;gap:10px}.dashcam-routes:has(.logs-empty-state),.screenrecord-list:has(.logs-empty-state){align-items:center;justify-content:center}.dashcam-virtual-spacer{flex:0 0 auto;width:100%;min-height:0;pointer-events:none}.dashcam-route-card{flex:0 0 auto;overflow:hidden;border-radius:8px;border:1px solid color-mix(in srgb,var(--md-outline-var) 50%,transparent);background:var(--md-surface-cont-h);color:var(--md-on-surface)}.dashcam-route-media{min-width:0}.dashcam-route-preview{position:relative;aspect-ratio:16 / 9;align-self:start;overflow:hidden;cursor:pointer;background:linear-gradient(180deg,var(--md-surface-cont-hh),var(--md-surface-cont))}.dashcam-route-media-info{display:none;padding:10px 12px 12px;border-top:1px solid color-mix(in srgb,var(--md-outline-var) 30%,transparent);background:color-mix(in srgb,var(--md-surface-cont-h) 92%,var(--md-surface));cursor:pointer}.dashcam-route-preview img,.dashcam-segment-thumb img{width:100%;height:100%;object-fit:cover;display:block}.logs-lazy-img{opacity:1;transition:opacity .16s ease}.logs-lazy-img[data-src]{opacity:0}.dashcam-segment-thumb.is-thumb-loading:after{content:"";position:absolute;inset:0;z-index:2;pointer-events:none;background:linear-gradient(100deg,var(--md-surface-cont-h) 0%,color-mix(in srgb,var(--md-on-surface) 10%,var(--md-surface-cont-h)) 50%,var(--md-surface-cont-h) 100%);background-size:220% 100%;animation:logsThumbSweep 1.25s ease-in-out infinite}@keyframes logsThumbSweep{0%{background-position:220% 0}to{background-position:-120% 0}}@media(prefers-reduced-motion:reduce){.dashcam-segment-thumb.is-thumb-loading:after{animation:none}}.dashcam-route-preview__shade{position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,.08),transparent 45%,rgba(0,0,0,.36));pointer-events:none}.dashcam-route-preview__chips{position:absolute;left:12px;right:12px;bottom:12px;display:flex;flex-wrap:wrap;gap:8px}.dashcam-route-preview__timestamp{position:absolute;top:12px;left:12px;z-index:1;padding:5px 8px;border-radius:7px;background:#0000007a;border:1px solid rgba(255,255,255,.16);color:#fff;font-size:12px;font-weight:700;line-height:1.35;font-variant-numeric:tabular-nums;text-shadow:0 1px 2px rgba(0,0,0,.45);backdrop-filter:blur(8px);pointer-events:none}.dashcam-route-preview .dashcam-route-preview__menu{position:absolute;top:10px;right:10px;z-index:3;width:36px;height:36px;color:#fff;background:#0000007a;border:1px solid rgba(255,255,255,.16);box-shadow:0 3px 12px #0003;backdrop-filter:blur(8px)}.dashcam-route-preview .dashcam-route-preview__menu:hover{background:#000000a3}.dashcam-chip{display:inline-flex;align-items:center;min-height:26px;padding:0 10px;border-radius:999px;background:#0000006b;border:1px solid rgba(255,255,255,.16);color:#fff;font-size:12px;font-weight:750;backdrop-filter:blur(10px)}.dashcam-play-mark{position:absolute;inset:0;display:grid;place-items:center}.dashcam-play-mark svg{width:54px;height:54px;padding:11px;border-radius:999px;color:#fff;background:#0000005c;border:1px solid rgba(255,255,255,.18);box-shadow:0 8px 24px #00000042}.dashcam-route-main{min-width:0;padding:12px 14px}.dashcam-route-head{display:flex;align-items:flex-start;gap:10px;cursor:pointer;border-radius:6px}.dashcam-route-head:hover{background:color-mix(in srgb,var(--md-primary) 7%,transparent)}.dashcam-route-titleblock{min-width:0;flex:1}.dashcam-route-title{font-size:16px;line-height:1.25;font-weight:850;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dashcam-route-subtitle{margin-top:4px;font-size:12.5px;color:var(--md-on-surface-var);opacity:.78;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dashcam-expand-btn{width:34px;height:34px;border-radius:999px;flex:0 0 auto;margin-right:8px}.dashcam-segments{margin-top:12px}.dashcam-segments.is-collapsed{display:none}.dashcam-selection-row{display:flex;align-items:center;gap:8px;overflow-x:auto;padding-bottom:8px;color:var(--md-on-surface-var);font-size:13px;font-weight:750}.dashcam-selection-count{flex:0 0 auto;color:var(--md-on-surface-var)}.dashcam-selection-row .smallBtn{white-space:nowrap;min-height:34px;padding-inline:12px;border-radius:var(--r-sm);font-size:12.5px;font-weight:800}.dashcam-segment-list{max-height:min(40vh,330px);overflow:auto;overscroll-behavior:contain;overflow-anchor:none;-webkit-overflow-scrolling:touch;touch-action:pan-y;scrollbar-width:none;display:flex;flex-direction:column;gap:8px}.dashcam-segment-list::-webkit-scrollbar{display:none}.dashcam-segment-loader{flex:0 0 auto;width:100%;height:22px;position:relative;overflow:hidden;overflow-anchor:none;border-radius:999px;background:transparent;transition:background-color .16s ease-out}.dashcam-segment-loader.is-loading{background:color-mix(in srgb,var(--md-outline-var) 18%,transparent)}.dashcam-segment-loader:before{content:"";position:absolute;inset:8px 20%;border-radius:inherit;background:var(--md-primary);opacity:0;transition:opacity .16s ease-out}.dashcam-segment-loader.is-loading:before{opacity:1;animation:dashcam-segment-loader .82s ease-in-out infinite}.dashcam-segment-tile{flex:0 0 auto;min-height:70px;display:flex;align-items:center;gap:10px;padding:8px;border-radius:8px;border:1px solid color-mix(in srgb,var(--md-outline-var) 42%,transparent);background:var(--md-surface-cont);cursor:pointer;content-visibility:auto;contain-intrinsic-size:70px 100%}@media(hover:hover){.dashcam-segment-tile:hover{border-color:color-mix(in srgb,var(--md-primary) 44%,var(--md-outline-var));background:color-mix(in srgb,var(--md-surface-cont-h) 84%,var(--md-primary))}}.dashcam-segment-tile--append{animation:dashcam-segment-append .18s cubic-bezier(.2,0,0,1) both}@keyframes dashcam-segment-append{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}@keyframes dashcam-segment-loader{0%{transform:translate(-160%)}to{transform:translate(160%)}}.dashcam-segment-thumb{position:relative;width:78px;height:54px;flex:0 0 78px;overflow:hidden;border-radius:6px;background:var(--md-surface-cont-hh)}.dashcam-segment-check{position:absolute;inset:0;display:grid;place-items:center;background:#0000001f}.dashcam-segment-check input{width:20px;height:20px;accent-color:var(--md-primary)}.dashcam-segment-inline-check{width:28px;height:28px;flex:0 0 28px;display:grid;place-items:center;border-radius:6px}.dashcam-segment-inline-check input{width:18px;height:18px;accent-color:var(--md-primary)}.dashcam-segment-body{min-width:0;flex:1}.dashcam-segment-topline{min-width:0;display:flex;align-items:center;gap:var(--sp-xs)}.dashcam-segment-badge{min-width:0;display:inline-flex;align-items:center;min-height:22px;padding:0 8px;border-radius:999px;border:1px solid color-mix(in srgb,var(--md-outline-var) 40%,transparent);background:var(--md-surface-cont-h);color:var(--md-on-surface-var);font-size:11px;font-weight:850;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dashcam-segment-read-status{min-width:0;display:inline-flex;flex:0 0 auto;margin-left:auto}.dashcam-segment-read-status[hidden]{display:none}.dashcam-segment-name{margin-top:6px;font-size:13px;font-weight:650;color:var(--md-on-surface);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dashcam-menu-btn{width:30px;height:30px;border-radius:999px;flex:0 0 auto}.dashcam-menu-btn svg{display:block;transform:translateZ(0)}.dashcam-group-menu-btn{margin-left:auto;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;padding-inline:12px}.dashcam-group-menu-btn svg{width:18px;height:18px}.dashcam-group-menu-btn--row{display:inline-flex;margin-left:auto}.logs-loading-list{display:flex;flex-direction:column;gap:10px;pointer-events:none}.logs-loading-card,.logs-loading-row{position:relative;overflow:hidden;border-radius:8px;border:1px solid color-mix(in srgb,var(--md-outline-var) 36%,transparent);background:var(--md-surface-cont-h);opacity:0;transform:translateY(12px);animation:uiStaggerIn .38s cubic-bezier(.2,0,0,1) both;animation-delay:min(calc(var(--i, 0) * 36ms),.42s)}.logs-loading-card{min-height:184px}.logs-loading-row{min-height:64px}.logs-loading-card:before,.logs-loading-row:before{content:"";position:absolute;inset:0;background:linear-gradient(100deg,transparent 0%,color-mix(in srgb,var(--md-on-surface) 7%,transparent) 48%,transparent 72%);transform:translate(-100%);animation:logsSkeletonSweep 1.35s ease-in-out infinite;animation-delay:calc(var(--i, 0) * 90ms);z-index:1}.logs-loading-card:after,.logs-loading-row:after{content:"";position:absolute;inset:12px;border-radius:6px;background:linear-gradient(var(--md-surface-cont),var(--md-surface-cont)) 0 0 / 100% 104px no-repeat,linear-gradient(var(--md-surface-cont),var(--md-surface-cont)) 0 122px / 62% 14px no-repeat,linear-gradient(var(--md-surface-cont),var(--md-surface-cont)) 0 146px / 38% 12px no-repeat;z-index:0}.logs-loading-row:after{inset:8px 10px;background:linear-gradient(var(--md-surface-cont),var(--md-surface-cont)) 0 0 / 76px 48px no-repeat,linear-gradient(var(--md-surface-cont),var(--md-surface-cont)) 90px 7px / 52% 14px no-repeat,linear-gradient(var(--md-surface-cont),var(--md-surface-cont)) 90px 31px / 34% 11px no-repeat}@keyframes logsSkeletonSweep{to{transform:translate(100%)}}@media(prefers-reduced-motion:reduce){.logs-loading-card,.logs-loading-row{animation:none;opacity:1;transform:none}.dashcam-segment-tile--append{animation:none}.dashcam-segment-loader.is-loading:before{animation:none}.logs-loading-card:before,.logs-loading-row:before{animation:none;opacity:0}}.dashcam-player-overlay{position:fixed;inset:0;z-index:calc(var(--z-toast) - 1);display:grid;place-items:center;padding:16px;background:#000000ad;opacity:0;transition:opacity .16s ease}.dashcam-player-overlay.is-open{opacity:1}.dashcam-player-overlay.is-menu-open,.dashcam-player-overlay.is-action-open{z-index:calc(var(--z-modal) - 1)}.dashcam-player-dialog{width:auto;max-width:min(92vw,960px);max-height:min(78vh,680px)}.dashcam-player-frame{position:relative;overflow:hidden;border-radius:8px;border:1px solid rgba(255,255,255,.16);background:#000;box-shadow:0 20px 60px #00000070}.dashcam-player-video{width:auto;height:auto;max-width:min(92vw,960px);max-height:min(78vh,680px);display:block;background:#000;object-fit:contain}.dashcam-player-frame .plyr{width:auto;max-width:min(92vw,960px);max-height:min(78vh,680px);background:#000;--plyr-color-main: var(--md-primary)}.dashcam-player-frame .plyr video{max-width:min(92vw,960px);max-height:min(78vh,680px);object-fit:contain;background:#000}.dashcam-player-frame .plyr__controls{z-index:5}.dashcam-player-frame .plyr__control--overlaid{display:none!important}.dashcam-player-transport{z-index:4;pointer-events:auto}.dashcam-player-frame .plyr:-webkit-full-screen,.dashcam-player-frame .plyr:fullscreen,.dashcam-player-frame .plyr.plyr--fullscreen-active{width:100vw!important;height:100vh!important;max-width:none;max-height:none;display:grid;place-items:center;background:#000}.dashcam-player-frame .plyr:-webkit-full-screen video,.dashcam-player-frame .plyr:fullscreen video,.dashcam-player-frame .plyr.plyr--fullscreen-active video{width:100vw!important;height:100vh!important;max-width:none;max-height:none;object-fit:contain;object-position:center center}.dashcam-player-frame .plyr__video-wrapper{display:grid;place-items:center;background:#000}.dashcam-player-frame .plyr:-webkit-full-screen .plyr__video-wrapper,.dashcam-player-frame .plyr:fullscreen .plyr__video-wrapper,.dashcam-player-frame .plyr.plyr--fullscreen-active .plyr__video-wrapper{width:100vw;height:100vh}.dashcam-player-frame .plyr__control svg{display:block;margin:auto}.dashcam-player-overlay--dashcam .dashcam-player-dialog:not(.dashcam-player-dialog--browsable){width:min(84vw,760px);max-width:min(96vw,920px);max-height:min(84vh,760px)}.dashcam-player-overlay--dashcam .dashcam-player-video{width:100%;height:auto;max-width:none;max-height:min(84vh,760px)}.dashcam-player-overlay--dashcam .dashcam-player-frame .plyr,.dashcam-player-overlay--dashcam .dashcam-player-frame .plyr video{width:100%;max-width:none;max-height:min(84vh,760px)}.dashcam-player-top{position:absolute;left:0;right:0;top:0;z-index:3;display:flex;align-items:center;gap:8px;padding:10px;background:linear-gradient(180deg,rgba(0,0,0,.62),transparent);pointer-events:none;transition:opacity .2s ease,transform .2s ease}.dashcam-player-toast{display:none;position:absolute;top:50%;left:50%;z-index:5;padding:12px 22px;border-radius:999px;background:#000000bd;color:#fff;font-weight:700;font-size:18px;letter-spacing:.02em;white-space:nowrap;pointer-events:none;opacity:0;transform:translate(-50%,-50%) scale(.92);transition:opacity .14s ease,transform .14s ease;backdrop-filter:blur(8px);box-shadow:0 10px 28px #0000005c}.dashcam-player-toast.is-visible{opacity:1;transform:translate(-50%,-50%) scale(1)}.dashcam-player-title{min-width:0;flex:1;color:#fff;font-weight:800;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dashcam-player-heading{min-width:0;flex:1}.dashcam-player-subtitle{margin-top:2px;color:#fffc;font-size:11px;font-weight:650;line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-variant-numeric:tabular-nums}.dashcam-player-close{pointer-events:auto}.dashcam-player-action{width:var(--close-size, 32px);height:var(--close-size, 32px);border:1px solid color-mix(in srgb,var(--md-outline-var) 40%,transparent);border-radius:var(--close-radius, 10px);color:#fff;background:color-mix(in srgb,var(--md-surface-cont) 82%,#000);pointer-events:auto;transition:background var(--motion-quick) var(--ease-standard),border-color var(--motion-quick) var(--ease-standard),color var(--motion-quick) var(--ease-standard)}.dashcam-player-action svg{width:var(--close-icon, 16px);height:var(--close-icon, 16px)}.dashcam-player-action:hover{background:color-mix(in srgb,var(--md-primary) 14%,var(--md-surface-cont));border-color:color-mix(in srgb,var(--md-primary) 32%,var(--md-outline-var));color:#fff}.dashcam-player-action:disabled{opacity:.55;cursor:default}.screenrecord-list{flex:1 1 auto;min-height:0;overflow:auto;overscroll-behavior:contain;scrollbar-gutter:stable;padding:10px var(--logs-content-inline) calc(var(--app-nav-bottom-gap, var(--nav-bar-height)) + 16px + env(safe-area-inset-bottom,0px));display:flex;flex-direction:column;gap:8px}.dashcam-routes.is-loading-more:after,.screenrecord-list.is-loading-more:after{content:"";flex:0 0 auto;width:22px;height:22px;margin:10px auto 2px;border:2px solid color-mix(in srgb,var(--md-outline-var) 48%,transparent);border-top-color:var(--md-primary);border-radius:999px;animation:screenrecord-loading-spin .78s linear infinite}@keyframes screenrecord-loading-spin{to{transform:rotate(360deg)}}.screenrecord-row{flex:0 0 auto;min-height:64px;display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;border:1px solid color-mix(in srgb,var(--md-outline-var) 44%,transparent);background:var(--md-surface-cont-h);color:var(--md-on-surface);cursor:pointer;content-visibility:auto;contain-intrinsic-size:auto 64px}.screenrecord-list .screenrecord-row.ui-stagger-item{animation:none}.screenrecord-virtual-spacer{flex:0 0 auto;width:100%;min-height:0;pointer-events:none}.screenrecord-row:hover{border-color:color-mix(in srgb,var(--md-primary) 38%,var(--md-outline-var));background:color-mix(in srgb,var(--md-surface-cont-h) 88%,var(--md-primary))}.screenrecord-row__icon{width:38px;height:38px;flex:0 0 38px;display:grid;place-items:center;border-radius:8px;background:color-mix(in srgb,var(--md-primary) 12%,var(--md-surface-cont));color:var(--md-primary);overflow:hidden}.screenrecord-row__thumb{width:76px;height:48px;flex:0 0 76px;display:block;border-radius:6px;background:var(--md-surface-cont);overflow:hidden}.screenrecord-row__thumb img{width:100%;height:100%;object-fit:cover;display:block}.screenrecord-row__icon svg,.screenrecord-download svg{width:21px;height:21px}.screenrecord-row__main{min-width:0;flex:1 1 auto}.screenrecord-row__name{font-size:14px;line-height:1.25;font-weight:750;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.screenrecord-row__meta{margin-top:6px;display:flex;flex-wrap:wrap;gap:6px 10px;color:var(--md-on-surface-var);font-size:12px;line-height:1.25}.screenrecord-download{appearance:none;border:0;width:36px;height:36px;flex:0 0 36px;display:grid;place-items:center;border-radius:8px;color:var(--md-on-surface-var);background:transparent;cursor:pointer}.screenrecord-download:hover{color:var(--md-primary);background:color-mix(in srgb,var(--md-primary) 10%,transparent)}@media(min-width:900px){.logs-shell{--logs-content-inline: clamp(18px, 3vw, 34px);--logs-route-media-column: minmax(260px, 34%)}.dashcam-route-card{display:grid;grid-template-columns:var(--logs-route-media-column) minmax(0,1fr);align-items:start}.dashcam-route-preview{width:100%}.dashcam-route-media-info{display:block}.dashcam-route-main{padding:10px 14px 12px}.dashcam-route-head{justify-content:flex-end;min-height:34px}.dashcam-route-head .dashcam-route-titleblock{display:none}}@media(min-aspect-ratio:13/10),(horizontal-viewport-segments:2),(vertical-viewport-segments:2),(min-width:640px)and (min-height:650px){.logs-shell{--logs-route-media-column: minmax(230px, 32%)}.dashcam-route-card{display:grid;grid-template-columns:var(--logs-route-media-column) minmax(0,1fr);align-items:stretch;height:clamp(178px,45vh,236px)}.dashcam-route-media{min-height:0;height:100%;display:flex;flex-direction:column}.dashcam-route-preview{flex:1 1 auto;min-height:0;width:100%;aspect-ratio:auto}.dashcam-route-media-info{display:block;flex:0 0 auto;min-height:0;overflow:hidden}.dashcam-route-main{min-height:0;height:100%;display:flex;flex-direction:column;padding-top:8px;padding-right:0}.dashcam-route-head{display:none}.dashcam-segments,.dashcam-segments.is-collapsed{min-height:0;flex:1 1 auto;display:grid;grid-template-columns:minmax(0,1fr) 96px;gap:8px;margin-top:0}.dashcam-selection-row{grid-column:2;grid-row:1;min-height:0;height:100%;flex-direction:column;justify-content:center;align-items:center;gap:6px;box-sizing:border-box;overflow-y:auto;padding:8px;border-left:1px solid color-mix(in srgb,var(--md-outline-var) 38%,transparent);scrollbar-width:none;text-align:center}.dashcam-selection-row::-webkit-scrollbar{display:none}.dashcam-selection-row .dashcam-group-menu-btn--row{margin-left:0}.dashcam-segment-list{grid-column:1;grid-row:1;flex:1 1 auto;min-height:0;max-height:none}.dashcam-selection-count{text-align:center;font-size:12px}.dashcam-selection-row .smallBtn{width:100%;min-height:30px;padding-inline:8px;font-size:12px;white-space:normal;line-height:1.15;overflow-wrap:anywhere}.dashcam-selection-row .btn--filled{padding-inline:8px}.dashcam-segment-tile--compact{min-height:46px;gap:8px;padding:5px 8px 5px 5px;contain-intrinsic-size:46px 100%}.dashcam-segment-thumb--compact{width:56px;height:36px;flex:0 0 56px}.dashcam-segment-check--compact input{width:16px;height:16px}.dashcam-segment-tile--compact .dashcam-segment-body{min-width:0;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:8px}.dashcam-segment-tile--compact .dashcam-segment-topline{display:contents}.dashcam-segment-tile--compact .dashcam-segment-badge{grid-column:1;grid-row:1;min-height:20px;padding-inline:7px;font-size:10.5px}.dashcam-segment-tile--compact .dashcam-segment-name{min-width:0;grid-column:2;grid-row:1;margin-top:0;font-size:12.5px}.dashcam-segment-tile--compact .dashcam-segment-read-status{grid-column:3;grid-row:1;margin-left:0}.dashcam-segment-tile--compact .dashcam-menu-btn{width:26px;height:26px}}@media(max-height:620px)and (min-aspect-ratio:13/10),(max-height:620px)and (horizontal-viewport-segments:2),(max-height:620px)and (vertical-viewport-segments:2),(min-width:640px)and (min-height:650px)and (max-height:620px){.logs-tabbar{padding-top:6px;padding-bottom:4px;--logs-tab-min-height: 32px;--logs-tab-font-size: 12.5px}.logs-menu__button{min-height:38px}.dashcam-routes,.screenrecord-list{gap:8px;padding-top:4px}.dashcam-route-card{display:grid;grid-template-columns:var(--logs-route-media-column) minmax(0,1fr);align-items:stretch;height:clamp(168px,46vh,196px)}.dashcam-route-media-info{display:block;padding:8px 10px 10px}.dashcam-route-title{font-size:14px}.dashcam-route-subtitle{font-size:11.5px}.dashcam-selection-row{padding:6px 6px 6px 8px}.dashcam-segments,.dashcam-segments.is-collapsed{grid-template-columns:minmax(0,1fr) 86px}.dashcam-segment-list{max-height:none}.dashcam-player-dialog{width:auto;max-width:min(76vw,760px);max-height:68vh}.dashcam-player-video,.dashcam-player-frame .plyr,.dashcam-player-frame .plyr video{max-width:min(76vw,760px);max-height:68vh}.dashcam-player-title{font-size:12.5px}}.dashcam-player-dialog,.dashcam-segment-read-status{--logs-player-status-reading-surface: var(--md-primary);--logs-player-status-reading-ink: var(--md-on-primary);--logs-player-status-recent-surface: var(--md-primary-state-soft);--logs-player-status-recent-ink: var(--md-primary);--logs-player-status-recent-border: color-mix(in srgb, var(--md-primary) 36%, transparent)}.dashcam-player-dialog{--logs-player-dialog-radius: var(--r-md);--logs-player-dialog-border: color-mix(in srgb, var(--md-outline-var) 42%, transparent);--logs-player-dialog-surface: var(--md-surface-cont);--logs-player-dialog-wide: 88rem;--logs-player-dialog-max-height: 40rem;--logs-player-dialog-landscape-aspect: 12 / 5;--logs-player-dialog-portrait-wide: 42rem;--logs-player-dialog-portrait-max-height: 42rem;--logs-player-dialog-portrait-viewport-height: 80dvh;--logs-player-browser-pane-width: clamp(15.5rem, 22dvw, 19.5rem);--logs-player-browser-pane-compact-width: clamp(15rem, 31dvw, 16.5rem);--logs-player-browser-divider: color-mix(in srgb, var(--md-outline-var) 34%, transparent);--logs-player-browser-surface: var(--md-surface-cont);--logs-player-section-padding-inline: var(--sp-md);--logs-player-section-padding-block: var(--sp-sm);--logs-player-summary-gap: var(--sp-sm);--logs-player-summary-count-surface: var(--md-primary-state-soft);--logs-player-summary-count-ink: var(--md-primary);--logs-player-summary-muted: color-mix(in srgb, var(--md-on-surface-var) 72%, transparent);--logs-player-segment-list-gap: var(--sp-xs);--logs-player-segment-list-padding: var(--sp-sm);--logs-player-segment-item-min-height: 4.5rem;--logs-player-segment-item-padding: var(--sp-xs);--logs-player-segment-item-radius: var(--r-sm);--logs-player-segment-item-surface: color-mix(in srgb, var(--md-on-surface) 3%, transparent);--logs-player-segment-item-hover: color-mix(in srgb, var(--md-on-surface) 7%, transparent);--logs-player-segment-item-active: var(--md-primary-state-soft);--logs-player-segment-item-border: color-mix(in srgb, var(--md-outline-var) 28%, transparent);--logs-player-segment-item-active-border: color-mix(in srgb, var(--md-primary) 62%, var(--md-outline-var));--logs-player-segment-thumb-width: 5.5rem;--logs-player-segment-thumb-radius: var(--r-xs);--logs-player-segment-thumb-surface: var(--md-surface-cont-h);--logs-player-segment-muted: color-mix(in srgb, var(--md-on-surface-var) 70%, transparent)}.dashcam-player-layout{width:100%;min-width:0;min-height:0}.dashcam-player-dialog--browsable{--logs-player-browser-current-width: var(--logs-player-browser-pane-width);width:min(calc(var(--app-vv-width, 100dvw) - var(--sp-lg) - var(--sp-lg) - env(safe-area-inset-left,0px) - env(safe-area-inset-right,0px)),var(--logs-player-dialog-wide));height:auto;max-height:min(calc(var(--app-vv-height, 100dvh) - var(--sp-lg) - var(--sp-lg) - env(safe-area-inset-top,0px) - env(safe-area-inset-bottom,0px)),var(--logs-player-dialog-max-height));max-width:none;aspect-ratio:var(--logs-player-dialog-landscape-aspect);overflow:hidden;border:1px solid var(--logs-player-dialog-border);border-radius:var(--logs-player-dialog-radius);background:var(--logs-player-dialog-surface);container-name:logs-player-dialog;container-type:inline-size}.dashcam-player-dialog--browsable .dashcam-player-layout{height:100%;display:grid;grid-template-columns:minmax(0,1fr) var(--logs-player-browser-current-width)}.dashcam-player-dialog--browsable .dashcam-player-frame{min-width:0;min-height:0;height:100%;border:0;border-radius:0;box-shadow:none}.dashcam-player-dialog--browsable .dashcam-player-video,.dashcam-player-dialog--browsable .dashcam-player-frame .plyr,.dashcam-player-dialog--browsable .dashcam-player-frame .plyr video{width:100%;height:100%;max-width:none;max-height:none;object-fit:contain}.dashcam-player-browser{min-width:0;min-height:0;display:grid;grid-template-rows:auto minmax(0,1fr);overflow:hidden;border-left:1px solid var(--logs-player-browser-divider);background:var(--logs-player-browser-surface)}.dashcam-player-group-summary{min-width:0;display:grid;gap:var(--logs-player-summary-gap);padding:var(--logs-player-section-padding-block) var(--logs-player-section-padding-inline) var(--sp-md)}.dashcam-player-section-title{margin:0;color:var(--md-on-surface);font-size:var(--fs-title-xs);font-weight:850;line-height:1.3}.dashcam-player-group-summary__body{min-width:0;display:grid;gap:var(--sp-xs)}.dashcam-player-group-summary__main{min-width:0;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:var(--sp-sm)}.dashcam-player-group-summary__title{min-width:0;overflow:hidden;color:var(--md-on-surface);font-size:var(--fs-body-sm);font-weight:850;line-height:1.3;text-overflow:ellipsis;white-space:nowrap}.dashcam-player-group-summary__count{flex:0 0 auto;padding:var(--sp-xs) var(--sp-sm);border-radius:var(--r-round);background:var(--logs-player-summary-count-surface);color:var(--logs-player-summary-count-ink);font-size:var(--fs-label-sm);font-weight:850;line-height:1.2;white-space:nowrap}.dashcam-player-group-summary__time{min-width:0;overflow:hidden;color:var(--logs-player-summary-muted);font-size:var(--fs-label-sm);font-variant-numeric:tabular-nums;font-weight:700;line-height:1.35;text-overflow:ellipsis;white-space:nowrap}.dashcam-player-segment-region[hidden]{display:none!important}.dashcam-player-segment-region{min-width:0;min-height:0;overflow:hidden;border-top:1px solid var(--logs-player-browser-divider)}.dashcam-player-segment-browser{min-width:0;min-height:0;height:100%;display:grid;grid-template-rows:auto minmax(0,1fr)}.dashcam-player-segment-browser>.dashcam-player-section-title{padding:var(--logs-player-section-padding-block) var(--logs-player-section-padding-inline)}.dashcam-player-segment-list{min-width:0;min-height:0;display:grid;grid-auto-rows:max-content;align-content:start;gap:var(--logs-player-segment-list-gap);overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;padding:0 var(--logs-player-segment-list-padding) var(--logs-player-segment-list-padding);scrollbar-width:thin}.dashcam-player-segment-item{width:100%;min-width:0;min-height:var(--logs-player-segment-item-min-height);display:grid;grid-template-columns:var(--logs-player-segment-thumb-width) minmax(0,1fr);align-items:center;gap:var(--sp-sm);padding:var(--logs-player-segment-item-padding);border:1px solid var(--logs-player-segment-item-border);border-radius:var(--logs-player-segment-item-radius);background:var(--logs-player-segment-item-surface);color:var(--md-on-surface);font:inherit;text-align:left;cursor:pointer;transition:border-color var(--motion-quick) var(--ease-standard),background var(--motion-quick) var(--ease-standard)}.dashcam-player-segment-item:hover{background:var(--logs-player-segment-item-hover)}.dashcam-player-segment-item:focus-visible{border-color:var(--logs-player-segment-item-active-border);outline:var(--focus-ring-width) solid var(--focus-ring-color);outline-offset:var(--focus-ring-offset)}.dashcam-player-segment-item.is-active{border-color:var(--logs-player-segment-item-active-border);background:var(--logs-player-segment-item-active)}.dashcam-player-segment-thumb{position:relative;width:100%;overflow:hidden;border-radius:var(--logs-player-segment-thumb-radius);aspect-ratio:16 / 9;background:var(--logs-player-segment-thumb-surface)}.dashcam-player-segment-thumb img{width:100%;height:100%;display:block;object-fit:cover}.dashcam-player-segment-copy{min-width:0;display:grid;gap:var(--sp-xs)}.dashcam-player-segment-meta{min-width:0;display:flex;align-items:center;justify-content:space-between;gap:var(--sp-xs)}.dashcam-player-segment-status{min-width:0;display:inline-flex;flex:0 0 auto}.dashcam-player-segment-status[hidden]{display:none}.dashcam-player-segment-time,.dashcam-player-segment-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dashcam-player-segment-time{color:var(--md-on-surface);font-size:var(--fs-body-sm);font-variant-numeric:tabular-nums;font-weight:850;line-height:1.3}.dashcam-player-segment-name{color:var(--logs-player-segment-muted);font-size:var(--fs-label-sm);font-weight:700;line-height:1.3}.dashcam-player-status-tag{min-width:0;display:inline-flex;align-items:center;justify-content:center;padding:var(--sp-xs) var(--sp-sm);border:1px solid transparent;border-radius:var(--r-round);font-size:var(--fs-label-sm);font-weight:850;line-height:1.2;white-space:nowrap}.dashcam-player-status-tag[data-state=reading]{background:var(--logs-player-status-reading-surface);color:var(--logs-player-status-reading-ink)}.dashcam-player-status-tag[data-state=recent]{border-color:var(--logs-player-status-recent-border);background:var(--logs-player-status-recent-surface);color:var(--logs-player-status-recent-ink)}@container logs-player-dialog (max-width: 46rem){.dashcam-player-layout{--logs-player-segment-thumb-width: 4.75rem}}@media(orientation:portrait){.dashcam-player-dialog--browsable{width:min(calc(var(--app-vv-width, 100dvw) - var(--sp-md) - var(--sp-md) - env(safe-area-inset-left,0px) - env(safe-area-inset-right,0px)),var(--logs-player-dialog-portrait-wide));height:min(calc(var(--app-vv-height, 100dvh) - var(--sp-lg) - var(--sp-lg) - env(safe-area-inset-top,0px) - env(safe-area-inset-bottom,0px)),var(--logs-player-dialog-portrait-viewport-height),var(--logs-player-dialog-portrait-max-height));max-height:none;aspect-ratio:auto}.dashcam-player-dialog--browsable .dashcam-player-layout{grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr)}.dashcam-player-dialog--browsable .dashcam-player-frame{height:auto;aspect-ratio:16 / 9}.dashcam-player-browser{border-top:1px solid var(--logs-player-browser-divider);border-left:0}}@media(orientation:landscape)and (max-height:30rem){.dashcam-player-dialog--browsable{--logs-player-browser-current-width: var(--logs-player-browser-pane-compact-width);width:min(calc(var(--app-vv-width, 100dvw) - var(--sp-md) - var(--sp-md) - env(safe-area-inset-left,0px) - env(safe-area-inset-right,0px)),var(--logs-player-dialog-wide));max-height:calc(var(--app-vv-height, 100dvh) - var(--sp-md) - var(--sp-md) - env(safe-area-inset-top,0px) - env(safe-area-inset-bottom,0px))}}@media(prefers-reduced-motion:reduce){.dashcam-player-dialog--browsable,.dashcam-player-dialog--browsable *{transition:none!important}}.route-summary,.route-summary-state{--route-summary-gap: var(--sp-md);--route-summary-section-gap: var(--sp-sm);--route-summary-radius: var(--r-md);--route-summary-radius-small: var(--r-sm);--route-summary-border: color-mix(in srgb, var(--md-outline-var) 46%, transparent);--route-summary-divider: color-mix(in srgb, var(--md-outline-var) 30%, transparent);--route-summary-surface: var(--md-surface-cont-l);--route-summary-surface-raised: var(--md-surface-cont-h);--route-summary-text: var(--md-on-surface);--route-summary-muted: color-mix(in srgb, var(--md-on-surface-var) 76%, transparent);--route-summary-accent: var(--md-primary);--route-summary-accent-soft: var(--md-primary-state-soft);--route-summary-auto: var(--md-info);--route-summary-manual: var(--md-error);--route-summary-manual-soft: color-mix(in srgb, var(--md-error) 35%, transparent);--route-summary-gas: var(--md-warning);--route-summary-brake: var(--md-primary);--route-summary-event-over: var(--md-warning);--route-summary-event-over-soft: var(--md-warning-cont);--route-summary-event-hard: var(--md-error);--route-summary-event-hard-soft: color-mix(in srgb, var(--md-error-cont) 24%, var(--md-surface-cont-h));--route-summary-danger: var(--md-error);--route-summary-danger-soft: color-mix(in srgb, var(--md-error-cont) 24%, var(--md-surface-cont-h));--route-summary-progress-track: var(--md-surface-cont-h);--route-summary-progress-fill: var(--md-primary);--route-summary-progress-height: var(--sp-xs);--route-summary-progress-motion: var(--motion-medium);--route-summary-progress-cycle: calc(var(--motion-long) + var(--motion-long) + var(--motion-long));--route-summary-distribution-height: var(--sp-lg)}.route-summary,.route-summary-state{box-sizing:border-box;width:100%;color:var(--route-summary-text);font-family:var(--font-sans);text-align:left;white-space:normal}.route-summary{display:grid;gap:var(--route-summary-gap);container-name:route-summary;container-type:inline-size}#appDialog:has(.route-summary) .app-dialog__sheet{width:min(100%,var(--content-max-width))}.route-summary-hero__subtitle{display:grid;justify-items:start;gap:1px;margin:0;color:var(--route-summary-muted);font:700 var(--fs-label-sm)/1.45 var(--font-sans);font-variant-numeric:tabular-nums}.route-summary-hero__date,.route-summary-hero__range{white-space:nowrap}.route-summary__section{min-width:0;display:grid;align-content:start;gap:var(--route-summary-section-gap);padding:var(--sp-md);border:1px solid var(--route-summary-border);border-radius:var(--route-summary-radius);background:var(--route-summary-surface)}.route-summary__section-title{margin:0;color:var(--route-summary-text);font-size:var(--fs-title-xs);font-weight:800;line-height:1.35}.route-summary__dashboard{min-width:0;display:grid;align-items:start;gap:var(--route-summary-gap)}.route-summary-hero{min-width:0;display:grid;gap:var(--sp-md)}.route-summary-hero__time{min-width:0;display:grid;align-content:start;gap:var(--sp-xs)}.route-summary__metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start;gap:var(--sp-md);margin:0}.route-summary__metrics--distance{padding-top:var(--sp-xs)}.route-summary-metric{min-width:0;display:grid;gap:var(--sp-xs)}.route-summary-metric__label,.route-summary-detail__label{color:var(--route-summary-muted);font-size:var(--fs-label-sm);font-weight:700;line-height:1.35}.route-summary-metric__value{min-width:0;display:flex;align-items:baseline;gap:var(--sp-xs);margin:0;font-variant-numeric:tabular-nums}.route-summary-metric__number{overflow:hidden;font-size:var(--fs-title-md);font-weight:850;line-height:1.2;text-overflow:ellipsis;white-space:nowrap}.route-summary-metric[data-variant=hero] .route-summary-metric__number{color:var(--route-summary-accent);font-size:clamp(var(--fs-title-lg),5cqi,var(--fs-headline))}.route-summary-metric[data-variant=compact] .route-summary-metric__number{font-size:var(--fs-title-sm)}.route-summary-metric__unit{color:var(--route-summary-muted);font-size:var(--fs-label-sm);font-weight:700}.route-summary-events,.route-summary-details{display:grid;gap:0;overflow:hidden;border:0;border-radius:var(--route-summary-radius-small);background:transparent}.route-summary-event,.route-summary-detail{min-width:0;border-top:1px solid var(--route-summary-divider);background:transparent}.route-summary-event:first-child,.route-summary-detail:first-child{border-top:0}.route-summary-composition{display:grid;gap:var(--sp-xs) var(--sp-md)}.route-summary-composition__stat{min-width:0;display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:var(--sp-xs);align-items:baseline}.route-summary-composition__label,.route-summary-event__label{min-width:0;overflow:hidden;font-size:var(--fs-body-sm);font-weight:750;line-height:1.4;text-overflow:ellipsis;white-space:nowrap}.route-summary-composition__label{display:inline-flex;align-items:center;gap:var(--sp-xs)}.route-summary-composition__label:before{width:.55em;height:.55em;flex:0 0 auto;border-radius:var(--r-round);background:currentColor;content:""}.route-summary-composition__duration,.route-summary-composition__ratio{color:var(--route-summary-muted);font:750 var(--fs-label-sm)/1.35 var(--font-mono);font-variant-numeric:tabular-nums}.route-summary-composition__stat[data-kind=auto]{color:var(--route-summary-auto)}.route-summary-composition__stat[data-kind=manual]{color:var(--route-summary-manual)}.route-summary-composition__stat[data-kind=gas]{color:var(--route-summary-gas)}.route-summary-composition__stat[data-kind=brake]{color:var(--route-summary-brake)}.route-summary-composition__stat[data-kind] .route-summary-composition__duration,.route-summary-composition__stat[data-kind] .route-summary-composition__ratio{color:inherit}.route-summary-distribution{display:flex;width:100%;height:var(--route-summary-distribution-height);overflow:hidden;border:1px solid var(--route-summary-border);border-radius:var(--r-sm);background:var(--route-summary-surface-raised)}.route-summary-distribution__segment,.route-summary-distribution__manual{display:block;flex:var(--route-summary-weight, 0) 1 0%;height:100%;box-sizing:border-box}.route-summary-distribution__manual{min-width:0;display:flex;overflow:hidden;border:2px solid var(--route-summary-manual);background:var(--route-summary-manual-soft)}.route-summary-distribution__manual>*+*{border-left:1px solid var(--route-summary-surface)}.route-summary-distribution__segment[data-kind=auto]{background:var(--route-summary-auto)}.route-summary-distribution__segment[data-kind=manual]{background:var(--route-summary-manual)}.route-summary-distribution__segment[data-kind=gas]{background:var(--route-summary-gas)}.route-summary-distribution__segment[data-kind=brake]{background:var(--route-summary-brake)}.route-summary-distribution__segment[data-kind=other]{background:var(--route-summary-manual-soft)}.route-summary-event{display:grid;gap:var(--sp-sm);padding:var(--sp-sm) var(--sp-md)}.route-summary-event[data-tone=danger]{background:var(--route-summary-danger-soft)}.route-summary-event__head{display:flex;align-items:center;gap:var(--sp-sm)}.route-summary-event__count{min-width:2.25em;margin-left:auto;padding:2px var(--sp-sm);border-radius:var(--r-round);background:var(--route-summary-surface-raised);color:var(--route-summary-text);font:800 var(--fs-label-sm)/1.4 var(--font-mono);text-align:center}.route-summary-event[data-tone=danger] .route-summary-event__count{background:var(--route-summary-danger);color:var(--md-on-error)}.route-summary-event__times{display:flex;flex-wrap:wrap;gap:var(--sp-xs);margin:0;padding:0;list-style:none}.route-summary-event__time{display:inline-flex;align-items:baseline;gap:var(--sp-xs);padding:3px var(--sp-sm);border:1px solid color-mix(in srgb,var(--route-summary-event-over) 42%,transparent);border-radius:var(--r-sm);background:var(--route-summary-event-over-soft);color:var(--route-summary-event-over);font:700 var(--fs-label-sm)/1.35 var(--font-mono);font-variant-numeric:tabular-nums}.route-summary-event[data-tone=danger] .route-summary-event__time{border-color:color-mix(in srgb,var(--route-summary-event-hard) 46%,transparent);background:var(--route-summary-event-hard-soft);color:var(--route-summary-event-hard)}.route-summary-event__peak{color:inherit;font-weight:850}.route-summary-detail{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:var(--sp-md);align-items:baseline;padding:var(--sp-sm) var(--sp-md)}.route-summary-detail__value{margin:0;font:800 var(--fs-body-sm)/1.4 var(--font-mono);font-variant-numeric:tabular-nums;text-align:right}.route-summary__warning{margin:0;padding:var(--sp-sm) var(--sp-md);border-radius:var(--route-summary-radius-small);background:var(--route-summary-danger-soft);color:var(--route-summary-text);font-size:var(--fs-label-md);font-weight:700;line-height:1.45}.route-summary-metadata{overflow:hidden;border:1px solid var(--route-summary-border);border-radius:var(--route-summary-radius);background:var(--route-summary-surface)}.route-summary-metadata__summary{min-height:var(--control-min-height);display:flex;align-items:center;padding-inline:var(--sp-md);color:var(--route-summary-muted);font-size:var(--fs-label-md);font-weight:750;cursor:pointer}.route-summary-metadata__summary:focus-visible{outline:var(--focus-ring-width) solid var(--focus-ring-color);outline-offset:calc(0px - var(--focus-ring-width))}.route-summary-metadata__list{display:grid;gap:1px;margin:0;border-top:1px solid var(--route-summary-divider);background:var(--route-summary-divider)}.route-summary-metadata__row{min-width:0;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.5fr);gap:var(--sp-md);padding:var(--sp-sm) var(--sp-md);background:var(--route-summary-surface);font-size:var(--fs-label-sm);line-height:1.4}.route-summary-metadata__row dt{color:var(--route-summary-muted)}.route-summary-metadata__row dd{min-width:0;margin:0;overflow-wrap:anywhere;text-align:right}.route-summary-state{display:grid;place-content:center;gap:var(--sp-md);padding-block:var(--sp-xl);color:var(--route-summary-muted);font-size:var(--fs-body-sm);line-height:1.45;text-align:center}.route-summary__empty{min-height:calc(var(--control-min-height) + var(--control-min-height) + var(--sp-xl));display:grid;place-content:center;padding:var(--sp-lg);color:var(--route-summary-muted);font-size:var(--fs-body-sm);line-height:1.45;text-align:center}.route-summary-state__title{color:var(--route-summary-text);font-size:var(--fs-body-md);font-weight:800;line-height:1.4}.route-summary-state__description{color:var(--route-summary-muted);font-size:var(--fs-label-md);font-variant-numeric:tabular-nums;line-height:1.4}.route-summary-state__progress{position:relative;width:100%;height:var(--route-summary-progress-height);overflow:hidden;border-radius:var(--r-round);background:var(--route-summary-progress-track)}.route-summary-state__progress-fill{display:block;width:calc(var(--route-summary-progress, 0) * 1%);height:100%;border-radius:inherit;background:var(--route-summary-progress-fill);transition:width var(--route-summary-progress-motion) var(--ease-standard)}.route-summary-state__progress[data-mode=indeterminate] .route-summary-state__progress-fill{width:32%;animation:route-summary-progress-indeterminate var(--route-summary-progress-cycle) var(--ease-standard) infinite}@keyframes route-summary-progress-indeterminate{0%{transform:translate(-110%)}to{transform:translate(330%)}}@container route-summary (min-width: 540px){.route-summary-hero{grid-template-columns:minmax(0,.75fr) minmax(0,1.75fr);align-items:end}.route-summary-composition,.route-summary__dashboard{grid-template-columns:repeat(2,minmax(0,1fr))}.route-summary__section--events{grid-column:1 / -1}.route-summary__metrics--distance{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(prefers-reduced-motion:reduce){.route-summary *,.route-summary-state *{animation:none;scroll-behavior:auto;transition:none}}
diff --git a/openpilot/selfdrive/carrot/web/generated/asset-manifest.json b/openpilot/selfdrive/carrot/web/generated/asset-manifest.json
index 5c2ce00bbd..e20a662720 100644
--- a/openpilot/selfdrive/carrot/web/generated/asset-manifest.json
+++ b/openpilot/selfdrive/carrot/web/generated/asset-manifest.json
@@ -6,7 +6,7 @@
"kind": "bundle",
"source": "src/entries/app.js",
"path": "js/generated/app.js",
- "hash": "1af713de15b55281ee1a3e3a15dab05129aac5f7f22f65eaf14ea8cf7846f9bb"
+ "hash": "d55ee0caca57fd432655cda0d5eb381bb49d1bcf9da9a4b80b053d657eb37487"
},
{
"id": "drive.runtime",
@@ -41,7 +41,7 @@
"kind": "bundle",
"source": "src/entries/logs.js",
"path": "js/generated/logs.js",
- "hash": "bf9b37460a74c44188c14565701fad39a93f08683ea9c47ca5223a24700ab3b4"
+ "hash": "367bacf74535b6ad535b7b554491a2b7c7f150ba7ec9fb7054f4ad5b459c58ee"
},
{
"id": "terminal.runtime",
@@ -90,7 +90,7 @@
"kind": "bundle",
"source": "src/entries/design_system.css",
"path": "css/generated/design-system.css",
- "hash": "2a4cb7be71efb614c8d087124d664b6895e4a97351c98c646783e16bee65c0e6"
+ "hash": "68169a78e3fa9e18be4d2ca3f4d9c97a4281df52e27d6fe1396c8413bd737211"
},
{
"id": "drive.styles",
@@ -125,7 +125,7 @@
"kind": "bundle",
"source": "src/entries/logs.css",
"path": "css/generated/logs.css",
- "hash": "62e3527995bea1be410774a30955e04ca2662d439dbe70fbb9c9d809f7e3bb16"
+ "hash": "999280fcd3d4c792b4f688457b5fd4006fbee38bd1dc75b22927b64eaa73da9d"
},
{
"id": "terminal.styles",
diff --git a/openpilot/selfdrive/carrot/web/js/generated/app.js b/openpilot/selfdrive/carrot/web/js/generated/app.js
index d2c47e04ba..bb54c03662 100644
--- a/openpilot/selfdrive/carrot/web/js/generated/app.js
+++ b/openpilot/selfdrive/carrot/web/js/generated/app.js
@@ -1,2 +1,2 @@
/* @generated by web/build.mjs; edit web/src/entries/app.js instead. */
-(()=>{var Mt=/^[0-9a-f]{64}$/,Rt=/^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*\\).+$/,J=class extends Error{constructor(t,n){super(t,n),this.name="AssetManifestError"}};function zt(e){if(e?.schemaVersion!==1||!Array.isArray(e.assets))throw new J("Invalid asset manifest schema");let t=new Map,n=new Set;for(let r of e.assets){if(typeof r?.id!="string"||typeof r.path!="string"||!Rt.test(r.path)||typeof r.hash!="string"||!Mt.test(r.hash)||t.has(r.id)||n.has(r.path))throw new J("Invalid asset manifest entry");t.set(r.id,r),n.add(r.path)}return t}function Pt(e){let t=zt(e);return Object.freeze({resolve(n){let r=t.get(n);if(!r)throw new J(`Unknown asset id: ${n}`);return`/${r.path}?v=${r.hash}`}})}function Be(e=document,t=globalThis){let n=e.getElementById("carrotAssetManifest");if(!n)throw new J("Asset manifest element is missing");let r;try{r=JSON.parse(n.textContent||"")}catch(o){throw new J("Asset manifest JSON is invalid",{cause:o})}let i=Pt(r);return t.CarrotAssetUrl=i,i}var ie=Object.freeze({webUi:"web_ui",profile:"profile",restore:"restore",resetDefaults:"reset_defaults",intro:"intro",undo:"undo",device:"device"});function Ut(e){let t=String(e||"").trim();return Object.values(ie).includes(t)?t:ie.webUi}function qe(e={}){let t=e.postJson;if(typeof t!="function")throw new TypeError("postJson must be a function");let n=e.endpoint||"/api/param_set",r=typeof e.onCommitted=="function"?e.onCommitted:null,i=typeof e.dispatchEvent=="function"?e.dispatchEvent:null;async function o(a,f,A={}){let E=String(a||"").trim();if(!E)throw new TypeError("A parameter name is required");let b=Ut(A.source),l=await t(n,{name:E,value:f,source:b}),d=l&&Object.prototype.hasOwnProperty.call(l,"value")?l.value:f,s=String(d)!==String(f),g=Object.freeze({name:E,value:d,requested:f,source:b,adjusted:s});return r?.(g),i?.(g),g}return Object.freeze({commit:o,sources:ie})}function Bt(e){return typeof e=="object"&&e!==null||typeof e=="function"}function $e(e=globalThis,t={}){if(!Bt(e))throw new TypeError("A facade target is required");let n=t.createSettingCommitter||qe;if(typeof n!="function")throw new TypeError("A committer factory is required");let r=Object.freeze({create:n,sources:ie});return e.CarrotParamCommit=r,r}var qt=["a[href]","area[href]","button:not([disabled])",'input:not([disabled]):not([type="hidden"])',"select:not([disabled])","textarea:not([disabled])",'[tabindex]:not([tabindex="-1"])','[contenteditable]:not([contenteditable="false"])',"audio[controls]","video[controls]","iframe","object","embed","summary"].join(",");function Ve(e,t){let n=t.document??e?.ownerDocument??globalThis.document,r=t.window??n?.defaultView??globalThis.window??globalThis,i=t.requestAnimationFrame??r?.requestAnimationFrame?.bind(r)??(f=>globalThis.setTimeout(f,0)),o=t.cancelAnimationFrame??r?.cancelAnimationFrame?.bind(r)??(f=>globalThis.clearTimeout(f)),a=t.getComputedStyle??r?.getComputedStyle?.bind(r)??(()=>({}));return{documentRoot:n,requestFrame:i,cancelFrame:o,getComputedStyle:a}}function Ge(e,t={}){if(!e?.querySelectorAll)return[];let{getComputedStyle:n}=Ve(e,t);return Array.from(e.querySelectorAll(qt)).filter(r=>{if(r.disabled||r.hidden||r.hasAttribute?.("disabled"))return!1;let i=r.getAttribute?.("tabindex");if(i!=null&&Number(i)<0)return!1;let o=r;for(;o&&o!==e;){if(o.hasAttribute?.("inert")||o.getAttribute?.("aria-hidden")==="true")return!1;o=o.parentElement??o.parentNode}let a=r.getBoundingClientRect?.();if(a&&a.width===0&&a.height===0)return!1;let f=n(r)||{};return f.visibility!=="hidden"&&f.display!=="none"})}function $t(e,t){let n=typeof t=="function"?t():t;return n?typeof n=="string"?e.querySelector?.(n)??null:typeof n.focus=="function"?n:null:null}function le(e,t={},n={}){if(!e)return Object.freeze({activate(){},deactivate(){},isActive:()=>!1});let{documentRoot:r,requestFrame:i,cancelFrame:o}=Ve(e,n),a=!1,f=0,A=null,E=null;function b(d){if(!a)return;if(d.key==="Escape"&&typeof t.escape=="function"){t.escape(d);return}if(d.key!=="Tab")return;let s=Ge(e,n);if(!s.length){d.preventDefault(),e.focus?.();return}let g=s[0],x=s[s.length-1],p=r?.activeElement;d.shiftKey?(p===g||!e.contains?.(p))&&(d.preventDefault(),x.focus()):(p===x||!e.contains?.(p))&&(d.preventDefault(),g.focus())}function l(){E!=null&&(o(E),E=null)}return Object.freeze({activate(){if(a)return;a=!0;let d=++f;if(A=t.returnFocus||r?.activeElement||null,e.hasAttribute?.("tabindex")||e.setAttribute?.("tabindex","-1"),r?.addEventListener?.("keydown",b,!0),t.focusInitial===!1)return;let s=$t(e,t.initialFocus),x=(!s||s===e||typeof e.contains!="function"||e.contains(s)?s:null)||Ge(e,n)[0]||e;E=i(()=>{E=null,!(!a||d!==f)&&x?.focus?.({preventScroll:t.initialFocusPreventScroll!==!1})})},deactivate(d={}){if(!a){f+=1,l();return}a=!1,f+=1,l(),r?.removeEventListener?.("keydown",b,!0);let s=A;if(A=null,d.restoreFocus===!1||typeof s?.focus!="function")return;(typeof r?.contains=="function"?r.contains(s):s.isConnected!==!1)&&s.focus({preventScroll:!0})},isActive(){return a}})}var Gt=180,Vt=Object.freeze(["app-dialog--choice","app-dialog--choice-list","app-dialog--choice-grid","app-dialog--choice-value-grid","app-dialog--form","app-dialog--input"]),Ht=Object.freeze(["appDialog","appBranchPicker","appCarPicker","settingSearchPanel"]);function Kt(e){return!e||e.labelHtml?"":String(e.label??"").trim()}function He(e){return!e||typeof e!="object"?"":String(e.heading??"").trim()}function Yt(e){return!!e&&!He(e)&&(e.label!=null||!!e.labelHtml)}function Wt(e){let t=[];for(let n of Array.isArray(e)?e:[]){let r=He(n);if(r){t.push({heading:r,items:[]});continue}Yt(n)&&(t.length||t.push({heading:"",items:[]}),t[t.length-1].items.push(n))}return t.filter(n=>n.items.length>0)}function Xt(e,t={}){let n=String(t.choiceLayout||t.choiceKind||"").trim();return n==="grid"||n==="value-grid"||n==="values"?"value-grid":n==="list"||n==="action-list"||n==="actions"?"list":e.length>4&&e.every(i=>{let o=Kt(i);return o&&o.length<=5&&!i.danger&&/^[+-]?(?:\d+|\d+\.\d+|[A-Za-z]{1,4})$/.test(o)})?"value-grid":"list"}function Jt(e,t={}){let n=Array.isArray(e)?e.flatMap(r=>r.items||[]):[];return n.length?e.some(r=>!!r?.heading)?"list":Xt(n,t):""}function Zt(e,t={}){let n=Number(t.choiceColumns||t.columns);return Number.isInteger(n)&&n>=2&&n<=6?n:e<=4?Math.max(2,e):e<=25?5:4}function Qt(e){let t=e.target??globalThis,n=e.document??t.document,r=e.setTimeout??t.setTimeout?.bind(t)??globalThis.setTimeout.bind(globalThis),i=e.clearTimeout??t.clearTimeout?.bind(t)??globalThis.clearTimeout.bind(globalThis),o=e.requestAnimationFrame??t.requestAnimationFrame?.bind(t)??(f=>r(f,0)),a=e.cancelAnimationFrame??t.cancelAnimationFrame?.bind(t)??i;return{target:t,documentRoot:n,setTimer:r,requestFrame:o,cancelFrame:a}}function Ke(e={}){let{target:t,documentRoot:n,setTimer:r,requestFrame:i,cancelFrame:o}=Qt(e),a=c=>n?.getElementById?.(c)??null,f=a("appDialog"),A=a("appDialogBackdrop"),E=a("appDialogTitle"),b=a("appDialogBody"),l=a("appDialogChoices"),d=a("appDialogInputWrap"),s=a("appDialogInput"),g=a("appDialogInputError"),x=a("appDialogCopy"),p=a("appDialogDefault"),C=a("appDialogCancel"),L=a("appDialogConfirm"),_=null,N=0,D=null,I=e.createFocusTrap??((c,w)=>le(c,w,{document:n,window:t,requestAnimationFrame:i,cancelAnimationFrame:o}));function j(c,w){let M=t.getUIText;return typeof M=="function"?M.call(t,c,w):w}function h(){let c=Ht.some(w=>{let M=a(w);return!!(M&&!M.hidden)});n?.body?.classList?.toggle("dialog-open",c)}function v(){f?.classList?.remove(...Vt),l&&(l.className="app-dialog__choices",l.style.removeProperty("--app-dialog-choice-columns"))}function T(c){return c.mode==="prompt"||c.mode==="form"||c.mode==="choice"?null:!1}function y(c){c.lastFocus&&typeof c.lastFocus.focus=="function"&&c.lastFocus.focus()}function m(c,w){if(c.serial!==N){c.resolve(w);return}f.hidden=!0,h(),v(),l&&(l.hidden=!0,l.innerHTML=""),p&&(p.hidden=!0,p.onclick=null),d&&(d.hidden=!0),g&&(g.hidden=!0,g.textContent=""),s&&(s.disabled=!1,s.value="",s.placeholder="",s.type="text",s.removeAttribute("autocomplete"),s.removeAttribute("autocapitalize"),s.removeAttribute("inputmode"),s.removeAttribute("aria-labelledby"),s.spellcheck=!0),b&&(b.hidden=!1),L&&(L.disabled=!1),C&&(C.disabled=!1),y(c),D=null,c.resolve(w)}function u(c,w){!c||_!==c||c.closing||(c.closing=!0,_=null,D=c.lastFocus||D,c.openFrame!=null&&(o(c.openFrame),c.openFrame=null),c.focusTrap?.deactivate?.({restoreFocus:!1}),f?.classList?.remove("is-open"),c.closeTimer=r(()=>{c.closeTimer=null,m(c,w)},Gt))}function F(c){u(_,c)}function k(){!_||_.submitting||u(_,T(_))}async function S(){if(!_||_.submitting)return;let c=_,M=c.mode==="prompt"||c.mode==="form"?s?s.value:"":!0;if(c.mode!=="form"||typeof c.onSubmit!="function"){u(c,M);return}c.submitting=!0,g&&(g.hidden=!0,g.textContent=""),s&&(s.disabled=!0),L&&(L.disabled=!0,L.textContent=c.submittingLabel),C&&(C.disabled=!0);try{await c.onSubmit(M),_===c&&!c.closing&&u(c,M)}catch(B){if(_!==c||c.closing)return;c.submitting=!1,s&&(s.disabled=!1,s.focus()),L&&(L.disabled=!1,L.textContent=c.confirmLabel),C&&(C.disabled=!1),g&&(g.textContent=B?.message||String(B),g.hidden=!1)}}function R(c={}){if(!f||!E||!b||!L||!C)return c.mode==="prompt"||c.mode==="form"?Promise.resolve(null):Promise.resolve(c.mode==="alert");let w=D;_&&(w=_.lastFocus||w,u(_,T(_))),D=null;let M=c.mode||"alert",B=M==="form",Dt=c.title||(M==="confirm"?j("confirm_title","Confirm"):M==="prompt"||M==="form"?j("input_title","Input"):j("notice","Notice")),Ae=c.message||"",je=c.messageHtml||"",jt=!!c.html,ke=c.confirmLabel||j("ok","OK"),kt=c.cancelLabel||j("cancel","Cancel"),Me=c.defaultActionLabel||"",Re=M==="prompt"&&!!Me,Te=Wt(c.choices),ze=Te.flatMap(K=>K.items),ne=ze.length>0,Pe=M==="choice"||ne,re=Jt(Te,c),Ue=M!=="alert"&&c.showCancel!==!1;v(),B&&f.classList.add("app-dialog--form"),(M==="prompt"||B)&&f.classList.add("app-dialog--input"),ne&&(f.classList.add("app-dialog--choice"),f.classList.add(re==="value-grid"?"app-dialog--choice-grid":"app-dialog--choice-list"),re==="value-grid"&&f.classList.add("app-dialog--choice-value-grid")),E.textContent=Dt,jt?b.innerHTML=String(je||Ae):b.textContent=String(Ae),b.hidden=B&&!String(je||Ae).trim(),b.style.flex=ne?"0 0 auto":"1 1 auto",L.textContent=ke,C.textContent=kt,L.disabled=!1,C.disabled=!1,C.hidden=!Ue,C.setAttribute("aria-hidden",Ue?"false":"true"),L.hidden=Pe,L.setAttribute("aria-hidden",Pe?"true":"false"),p&&(p.hidden=!Re,p.textContent=Me,p.disabled=!1,p.onclick=Re?()=>F(c.defaultActionValue??""):null);let Ce=c.copyText||"";if(x&&(x.hidden=!Ce,x.textContent=c.copyLabel||j("copy","Copy"),x.onclick=Ce?()=>{let K=t.copyToClipboard;typeof K=="function"&&K.call(t,Ce);let O=t.alert;typeof O=="function"&&O.call(t,j("copied","Copied"))}:null),l){l.innerHTML="",l.hidden=!ne,l.className=`app-dialog__choices app-dialog__choices--${re||"list"}`,re==="value-grid"?l.style.setProperty("--app-dialog-choice-columns",String(Zt(ze.length,c))):l.style.removeProperty("--app-dialog-choice-columns");let K=O=>{let U=n.createElement("button");U.type="button";let z=O.danger?"btn btn--danger app-dialog__choiceBtn":"btn app-dialog__choiceBtn";return z+=re==="value-grid"?" app-dialog__choiceBtn--value":" app-dialog__choiceBtn--action",(O.current||O.selected)&&(z+=" is-current"),O.className&&(z+=` ${O.className}`),U.className=z,O.labelHtml?U.innerHTML=O.labelHtml:U.textContent=String(O.label),U.addEventListener("click",()=>F(O.value)),U};Te.forEach((O,U)=>{if(!O.heading){for(let Se of O.items)l.appendChild(K(Se));return}let z=n.createElement("div");z.className="app-dialog__choiceGroup",z.setAttribute("role","group");let Q=`appDialogChoiceHeading${U}`;z.setAttribute("aria-labelledby",Q);let ce=n.createElement("div");ce.id=Q,ce.className="app-dialog__choiceHeading",ce.textContent=O.heading,z.appendChild(ce);for(let Se of O.items)z.appendChild(K(Se));l.appendChild(z)})}if(d&&s){let K=M==="prompt"||B;d.hidden=!K,s.disabled=!1,s.value=c.defaultValue??"",s.placeholder=c.placeholder||"",s.type=c.inputType==="password"?"password":"text";let O=c.autocomplete??(B?"off":""),U=c.autocapitalize??(B?"none":"");O?s.autocomplete=O:s.removeAttribute("autocomplete"),U?s.autocapitalize=U:s.removeAttribute("autocapitalize"),s.spellcheck=c.spellcheck==null?!B:c.spellcheck===!0,B?s.setAttribute("aria-labelledby","appDialogTitle"):s.removeAttribute("aria-labelledby"),c.inputMode?s.inputMode=c.inputMode:s.removeAttribute("inputmode"),g&&(g.hidden=!0,g.textContent="")}return new Promise(K=>{let O={resolve:K,mode:M,serial:++N,onSubmit:c.onSubmit,submitting:!1,closing:!1,confirmLabel:ke,submittingLabel:c.submittingLabel||j("saving","Saving..."),lastFocus:w||n.activeElement||null,focusTrap:null,openFrame:null,closeTimer:null};_=O,f.hidden=!1,h();let U=f.querySelector?.(".app-dialog__sheet")||f;O.focusTrap=I(U,{focusInitial:!1,returnFocus:O.lastFocus}),O.focusTrap?.activate?.(),O.openFrame=i(()=>{if(O.openFrame=null,!(_!==O||O.serial!==N||O.closing))if(f.classList.add("is-open"),(M==="prompt"||B)&&s)s.focus(),s.select();else if(ne&&l){let z=l.querySelector(".is-current"),Q=z||l.querySelector("button");Q&&typeof Q.focus=="function"&&(Q.focus({preventScroll:!!z}),z&&typeof z.scrollIntoView=="function"&&z.scrollIntoView({block:"center",inline:"nearest"}))}else M==="choice"?C.focus():L.focus()})})}function V(c,w={}){return R({mode:"alert",title:w.title,message:c,messageHtml:w.messageHtml,html:w.html,confirmLabel:w.confirmLabel,copyText:w.copyText})}function Y(c,w={}){return R({mode:"confirm",title:w.title,message:c,confirmLabel:w.confirmLabel,cancelLabel:w.cancelLabel})}function H(c,w={}){return R({mode:"prompt",title:w.title,message:c,confirmLabel:w.confirmLabel,cancelLabel:w.cancelLabel,defaultValue:w.defaultValue,defaultActionLabel:w.defaultActionLabel,defaultActionValue:w.defaultActionValue,showCancel:w.showCancel,placeholder:w.placeholder})}function Nt(c,w={}){return R({mode:"form",title:w.title,message:c,defaultValue:w.defaultValue,placeholder:w.placeholder,inputType:w.inputType,autocomplete:w.autocomplete,autocapitalize:w.autocapitalize,inputMode:w.inputMode,spellcheck:w.spellcheck,confirmLabel:w.confirmLabel,cancelLabel:w.cancelLabel,submittingLabel:w.submittingLabel,onSubmit:w.onSubmit})}A&&(A.onclick=k),C&&(C.onclick=k),L&&(L.onclick=S);function Ft(c){if(!_)return;if(c.key==="Escape"){c.preventDefault(),_.mode==="alert"?u(_,!0):k();return}if(c.key!=="Enter"||c.shiftKey||c.isComposing||c.defaultPrevented)return;let w=c.target?.tagName;w==="BUTTON"||w==="SELECT"||w==="TEXTAREA"||w==="A"||w==="INPUT"&&c.target!==s||c.target?.isContentEditable||(c.preventDefault(),S())}return n?.addEventListener?.("keydown",Ft),Object.freeze({openAppDialog:R,appAlert:V,appConfirm:Y,appPrompt:H,appForm:Nt,syncModalBodyLock:h,cancelAppDialog:k,confirmAppDialog:S})}var en=Object.freeze(["openAppDialog","appAlert","appConfirm","appPrompt","appForm","syncModalBodyLock","createFocusTrap"]);function Ye(e=globalThis,t=e.document,n={}){let r=(A,E={})=>le(A,E,{document:t,window:e,requestAnimationFrame:n.requestAnimationFrame,cancelAnimationFrame:n.cancelAnimationFrame,getComputedStyle:n.getComputedStyle}),i=Ke({...n,target:e,document:t,createFocusTrap:r}),o=Object.freeze({openAppDialog:i.openAppDialog,appAlert:i.appAlert,appConfirm:i.appConfirm,appPrompt:i.appPrompt,appForm:i.appForm,syncModalBodyLock:i.syncModalBodyLock,createFocusTrap:r}),a=e.CarrotUI&&typeof e.CarrotUI=="object"?e.CarrotUI:null,f=a&&Object.isExtensible(a)?a:{...a||{}};f.dialog=o,e.CarrotUI=f;for(let A of en)e[A]=o[A];return o}function We(e,t,n){let r=String(t??"");if(n)for(let[i,o]of Object.entries(n))r=r.replaceAll(`{${i}}`,String(o));return r}function Xe(e){return String(e??"")}function Je(e){let t=Number(e?.summary?.selected||0);return Number.isFinite(t)&&t>0?t:(Array.isArray(e?.entries)?e.entries:[]).filter(r=>r?.apply).length}function Ee(e,t={}){let n=t.escape||Xe,r=t.text||We;return'
'+[["changed","settings_diff_changed","changed"],["same","settings_diff_same","same"],["skipped","settings_diff_skipped","skipped"],["invalid","settings_diff_invalid","invalid"]].map(([o,a,f])=>`
${n(r(a,f))}${n(e?.[o]??0)}
`).join("")+"
"}function tn(e,t,n){let r=e.filter(o=>o?.status==="invalid");if(!r.length)return"";let i=r.map(o=>`${t(o.key)}`+(o.reason?`${t(o.reason)}`:"")+"").join("");return`${t(n("settings_diff_invalid_detail","{count} could not be applied",{count:r.length}))}
`}function Ze(e,t={}){let n=t.escape||Xe,r=t.text||We,i=Array.isArray(e?.entries)?e.entries:[],o=i.filter(p=>p?.apply||p?.status==="changed"),a=Number.isFinite(t.limit)?Math.max(1,t.limit):80,f=o.slice(0,a),A=Math.max(0,o.length-f.length),E=t.currentLabel||r("settings_diff_current","Current"),b=t.nextLabel||r("settings_diff_apply","Apply"),l=t.changedLabel||r("settings_diff_changed_status","Changed"),d=tn(i,n,r),s=Ee(e?.summary,{escape:n,text:r});if(!o.length)return`${s}${n(r("settings_diff_no_changes","No changes to apply."))}
`+d;let g=f.map(p=>`${n(E)}${n(p.current)}
>
${n(b)}${n(p.value)}
`).join(""),x=A?`${n(r("settings_diff_more","{count} more changes hidden",{count:A}))}
`:"";return`${s}${g}
${x}${d}`}function Qe(e){return typeof e=="object"&&e!==null||typeof e=="function"}function et(e=globalThis,t={}){if(!Qe(e))throw new TypeError("A facade target is required");let n=Object.freeze({renderTable:t.renderDiffTable||Ze,renderSummary:t.renderDiffSummary||Ee,selectedCount:t.getDiffSelectedCount||Je}),r=Qe(e.CarrotUI)?e.CarrotUI:null,i=r&&Object.isExtensible(r)?r:Object.assign({},r||{});return i.diffTable=n,e.CarrotUI=i,n}var rt=Object.freeze({default:"i",info:"i",success:"✓",error:"✕",warn:"!",offline:"!",hint:"i"}),nn=Object.freeze({default:"",info:"is-info",success:"is-success",error:"is-error",warn:"is-warn",offline:"is-warn",hint:"is-hint"}),rn=2600,on=220;function an(e){let t=typeof e=="string"?e:"default";return Object.hasOwn(rt,t)?t:"default"}function ve(e,t){if(e==null)return t;let n=Number(e);return Number.isFinite(n)&&n>=0?n:t}function tt(e,t){return!e||!t?!1:typeof t.contains=="function"?t.contains(e):e.parentNode===t}function nt(e){e&&(typeof e.remove=="function"?e.remove():e.parentNode?.removeChild?.(e))}function ue(e={},t={}){let n=e.environment||t,r=e.host||null,i=e.documentRoot||n.document||r?.ownerDocument||globalThis.document,o=n.setTimeout||e.setTimeout||globalThis.setTimeout?.bind(globalThis),a=n.clearTimeout||e.clearTimeout||globalThis.clearTimeout?.bind(globalThis),f=n.requestAnimationFrame||e.requestAnimationFrame||(y=>o(y,0)),A=n.cancelAnimationFrame||e.cancelAnimationFrame||a,E=ve(e.defaultDuration,rn),b=ve(e.removeDelay,on),l=0,d=null,s=null,g=null,x=null,p=null,C=!1;function L(){s!=null&&a?.(s),g!=null&&a?.(g),x!=null&&A?.(x),s=null,g=null,x=null}function _(y,m){C||y!==l||m!==d||(nt(m),d=null,g=null,p=null)}function N(y,m){if(b===0){_(y,m);return}g=o?.(()=>_(y,m),b)??null}function D(y,m){C||y!==l||m!==d||(m.classList?.remove?.("is-visible"),s=null,N(y,m))}function I(){let y=!!(d&&tt(d,r));return Object.freeze({active:y,visible:y&&!!d.classList?.contains?.("is-visible"),message:y?p?.message??"":"",tone:y?p?.tone??"default":null,duration:y?p?.duration??null:null,glyph:y?p?.glyph??"":"",className:y&&d.className||"",serial:l,destroyed:C})}function j(){if(d&&tt(d,r))return d;if(!r||typeof i?.createElement!="function")return null;let y=i.createElement("div");if(typeof r.replaceChildren=="function")r.replaceChildren(y);else{for(;r.firstChild;)r.removeChild(r.firstChild);r.appendChild(y)}return d=y,y}function h(y,m={}){if(C||y==null||String(y).length===0)return I();let u=j();if(!u)return I();L();let F=++l,k=an(m.tone),S=ve(m.duration,E),R=nn[k],V=rt[k];u.className=R?`app-toast ${R}`:"app-toast";let Y=i.createElement("span");Y.className="app-toast__icon",Y.setAttribute?.("aria-hidden","true"),Y.textContent=V;let H=i.createElement("div");return H.className="app-toast__msg",H.textContent=String(y),u.replaceChildren(Y,H),p={message:String(y),tone:k,duration:S,glyph:V},x=f?.(()=>{C||F!==l||u!==d||(x=null,u.classList?.add?.("is-visible"))})??null,s=o?.(()=>D(F,u),S)??null,I()}function v(y={}){if(C||!d)return I();let m=d;L();let u=++l;return m.classList?.remove?.("is-visible"),y.immediate===!0?_(u,m):N(u,m),I()}function T(){C||(L(),l+=1,nt(d),d=null,p=null,C=!0)}return Object.freeze({show:h,dismiss:v,destroy:T,snapshot:I})}var it=new WeakMap;function ot(e){return typeof e=="object"&&e!==null||typeof e=="function"}function sn(e,t,n){let r=ot(e.CarrotUI)?e.CarrotUI:null,i=r&&Object.isExtensible(r)?r:Object.assign({},r||{});i[t]=n,e.CarrotUI=i}function at(e=globalThis,t={}){if(!ot(e))throw new TypeError("A facade target is required");let n=it.get(e),r=t.documentRoot||e.document,i=t.host||r?.getElementById?.("appToastHost")||null,o={document:r,setTimeout:e.setTimeout?.bind?.(e),clearTimeout:e.clearTimeout?.bind?.(e),requestAnimationFrame:e.requestAnimationFrame?.bind?.(e),cancelAnimationFrame:e.cancelAnimationFrame?.bind?.(e),...t.environment||{}},a=t.controller||ue({host:i,documentRoot:r,defaultDuration:t.defaultDuration,removeDelay:t.removeDelay,environment:o});if(!a||["show","dismiss","destroy","snapshot"].some(E=>typeof a[E]!="function"))throw new TypeError("A toast controller is required");let f=Object.freeze({create:ue,createController:ue,show:a.show,dismiss:a.dismiss,destroy:a.destroy,snapshot:a.snapshot});function A(E,b,l){if(b&&typeof b=="object")return a.show(E,b);let d={};return b!=null&&b!==""&&(d.tone=b),l!=null&&(d.duration=l),a.show(E,d)}return sn(e,"toast",f),e.showAppToast=A,n&&n!==a&&n.destroy(),it.set(e,a),f}var cn='[role^="menuitem"]',fe=new WeakMap;function de(e,t){if(!e||typeof e.addEventListener!="function"||typeof e.removeEventListener!="function")throw new TypeError(`${t} must be an event target`);return e}function st(e){return!!(e?.disabled||e?.hidden||e?.getAttribute?.("disabled")!=null||e?.getAttribute?.("aria-disabled")==="true"||e?.getAttribute?.("aria-hidden")==="true"||e?.classList?.contains?.("is-disabled"))}function ln(e,t,n){let r=typeof e?.closest=="function"?e.closest(t):null;if(!r){for(let i=e;i&&i!==n;i=i.parentElement||i.parentNode)if(typeof i.matches=="function"&&i.matches(t)){r=i;break}}return r&&n.contains(r)?r:null}function we(e){typeof e?.focus=="function"&&e.focus()}function ee(e={},t={}){let{root:n,trigger:r,panel:i,itemSelector:o=cn,beforeOpen:a,onSelect:f}=e,A=e.environment||t,E=e.documentRoot||A.document||n?.ownerDocument||globalThis.document;if(de(n,"root"),de(r,"trigger"),de(i,"panel"),de(E,"documentRoot"),typeof n.contains!="function"||typeof i.contains!="function")throw new TypeError("root and panel must support contains()");if(typeof i.querySelectorAll!="function")throw new TypeError("panel must support querySelectorAll()");if(typeof o!="string"||!o.trim())throw new TypeError("itemSelector must be a non-empty string");let b=fe.get(n);b&&b.destroy();let l=r.getAttribute?.("aria-expanded")==="true"||!i.hidden&&n.classList?.contains?.("is-open"),d=!1,s;function g(){return Array.from(i.querySelectorAll(o)).filter(m=>!st(m))}function x(){return d?!1:(n.classList?.toggle?.("is-open",l),i.hidden=!l,r.setAttribute?.("aria-expanded",l?"true":"false"),i.setAttribute?.("aria-hidden",l?"false":"true"),l)}function p(m){let u=g();if(!u.length)return null;let F=m==="last"?u[u.length-1]:u[0];return we(F),F}function C(m={}){if(d)return!1;if(l)x();else{if(typeof a=="function"&&a(s)===!1)return!1;l=!0,x()}let u=m===!0||m.focusFirst===!0?"first":m.focusLast===!0?"last":m.focus;return(u==="first"||u==="last")&&p(u),!0}function L(m={}){if(d)return!1;let u=l;return l=!1,x(),m.restoreFocus===!0&&we(r),u}function _(m={}){return d?!1:l?(L({restoreFocus:m.restoreFocus===!0}),!1):(C(m),l)}function N(m){let u=g();if(!u.length)return!1;let F=A.getActiveElement?.()||E.activeElement,k=u.indexOf(F),S;if(m.key==="ArrowDown")S=k<0?0:(k+1)%u.length;else if(m.key==="ArrowUp")S=k<0?u.length-1:(k-1+u.length)%u.length;else if(m.key==="Home")S=0;else if(m.key==="End")S=u.length-1;else return!1;return m.preventDefault?.(),we(u[S]),!0}function D(){_()}function I(m){m.key!=="ArrowDown"&&m.key!=="ArrowUp"||(m.preventDefault?.(),C({focus:m.key==="ArrowUp"?"last":"first"}))}function j(m){if(!l)return;let u=ln(m.target,o,i);!u||st(u)||(L({restoreFocus:!0}),typeof f=="function"&&f(u,m,s))}function h(m){if(l){if(m.key==="Tab"){L();return}if(m.key==="Escape"){m.preventDefault?.(),m.stopPropagation?.(),L({restoreFocus:!0});return}N(m)}}function v(m){if(!l)return;let u=m.target;n.contains(u)||r.contains?.(u)||i.contains(u)||L()}function T(m){!l||m.key!=="Escape"||(m.preventDefault?.(),L({restoreFocus:!0}))}function y(){d||(l&&(l=!1,x()),r.removeEventListener("click",D),r.removeEventListener("keydown",I),i.removeEventListener("click",j),i.removeEventListener("keydown",h),E.removeEventListener("pointerdown",v),E.removeEventListener("keydown",T),d=!0,fe.get(n)===s&&fe.delete(n))}return s=Object.freeze({open:C,close:L,toggle:_,sync:x,destroy:y,get isOpen(){return!d&&l},get destroyed(){return d}}),r.setAttribute?.("aria-haspopup","menu"),i.id&&r.setAttribute?.("aria-controls",i.id),i.getAttribute?.("role")||i.setAttribute?.("role","menu"),x(),r.addEventListener("click",D),r.addEventListener("keydown",I),i.addEventListener("click",j),i.addEventListener("keydown",h),E.addEventListener("pointerdown",v),E.addEventListener("keydown",T),fe.set(n,s),s}function ct(e){return typeof e=="object"&&e!==null||typeof e=="function"}var un=Object.freeze({create:ee,createController:ee,mount:ee});function lt(e=globalThis,t={}){if(!ct(e))throw new TypeError("A facade target is required");let n=t.createMenuController||ee;if(typeof n!="function")throw new TypeError("A menu controller factory is required");let r=n===ee?un:Object.freeze({create:n,createController:n,mount:n}),i=ct(e.CarrotUI)?e.CarrotUI:null,o=i&&Object.isExtensible(i)?i:Object.assign({},i||{});return o.menu=r,e.CarrotUI=o,t.compatibility!==!1&&(e.CarrotDropdownMenu=r),r}function fn(e){if(!e||typeof e.addEventListener!="function")throw new TypeError("A commit gesture target element is required");return e}function pe(e,t={}){let n=fn(e),r=t.onCommit;if(typeof r!="function")throw new TypeError("onCommit must be a function");let i=typeof t.onHoldStep=="function"?t.onHoldStep:r,o=Number.isFinite(t.holdDelayMs)?t.holdDelayMs:0,a=Number.isFinite(t.holdIntervalMs)?t.holdIntervalMs:160,f=Number.isFinite(t.moveTolerance)?t.moveTolerance:10,A=Number.isFinite(t.clickSuppressMs)?t.clickSuppressMs:700,E=typeof t.pressedClass=="string"?t.pressedClass:"is-holding",b=typeof t.now=="function"?t.now:()=>Date.now(),l=t.scheduler||globalThis,d=l.setTimeout.bind(l),s=l.clearTimeout.bind(l),g=null,x=null,p=null,C=0;function L(){x!==null&&(s(x),x=null),p!==null&&(s(p),p=null)}function _(){if(!(!g||g.pointerId===void 0||g.pointerId===null)&&typeof n.releasePointerCapture=="function")try{n.releasePointerCapture(g.pointerId)}catch{}}function N(){L(),_(),n.classList?.remove?.(E),g=null}function D(){g&&(g.holdFired=!0,i(),p=d(D,a))}function I(){x=null,g&&D()}function j(u){if(!(u.button!==void 0&&u.button!==0)){if(N(),g={pointerId:u.pointerId,startX:u.clientX,startY:u.clientY,holdFired:!1,cancelled:!1},n.classList?.add?.(E),typeof n.setPointerCapture=="function"&&u.pointerId!==void 0)try{n.setPointerCapture(u.pointerId)}catch{}o>0&&(x=d(I,o))}}function h(u){if(!g||u.pointerId!==void 0&&u.pointerId!==g.pointerId)return;let F=Math.abs(u.clientX-g.startX),k=Math.abs(u.clientY-g.startY);F<=f&&k<=f||(g.cancelled=!0,C=b()+A,N())}function v(u){if(!g||u.pointerId!==void 0&&u.pointerId!==g.pointerId)return;let F=!g.cancelled&&!g.holdFired;C=b()+A,N(),F&&r()}function T(){g&&(g.cancelled=!0,C=b()+A,N())}function y(u){if(b()n.addEventListener(u,F)),Object.freeze({destroy(){N(),C=0,m.forEach(([u,F])=>n.removeEventListener?.(u,F))},get pressed(){return g!==null}})}function mn(e,t,n){let r=e;return Number.isFinite(t)&&(r=Math.max(r,t)),Number.isFinite(n)&&(r=Math.min(r,n)),r}function _e(e={}){let t=Number(e.step),n=Number(e.sign),r=Number(e.min),i=Number(e.max),o=Number(e.current);if(Number.isFinite(o)||(o=Number(e.fallback)),Number.isFinite(o)||(o=Number.isFinite(r)?r:0),!Number.isFinite(t)||!Number.isFinite(n))return o;let a=mn(o+n*t,r,i);return Number.isInteger(r)&&Number.isInteger(i)&&Number.isInteger(t)&&(a=Math.round(a)),a}function ut(e={}){let t=e.commit;if(typeof t!="function")throw new TypeError("commit must be a function");let n=typeof e.getValue=="function"?e.getValue:()=>NaN,r=typeof e.getStep=="function"?e.getStep:()=>1,i=typeof e.getRange=="function"?e.getRange:()=>({}),o=typeof e.getFallback=="function"?e.getFallback:()=>NaN,a=Number.isFinite(e.holdDelayMs)?e.holdDelayMs:900,f=Number.isFinite(e.holdIntervalMs)?e.holdIntervalMs:160,A=!1;async function E(d){if(!A){A=!0;try{let s=i()||{},g=_e({current:n(),fallback:o(),sign:d,step:r(),min:s.min,max:s.max});await t(g)}finally{A=!1}}}let b=[];function l(d,s){d&&b.push(pe(d,{onCommit:()=>{E(s)},holdDelayMs:a,holdIntervalMs:f,pressedClass:e.pressedClass,now:e.now,scheduler:e.scheduler}))}return l(e.minusButton,-1),l(e.plusButton,1),Object.freeze({applyDelta:E,destroy(){b.forEach(d=>d.destroy()),b.length=0}})}function ft(e){return typeof e=="object"&&e!==null||typeof e=="function"}function dt(e=globalThis,t={}){if(!ft(e))throw new TypeError("A facade target is required");let n=t.createNumericStepper||ut,r=t.createCommitGesture||pe;if(typeof n!="function")throw new TypeError("A numeric stepper factory is required");if(typeof r!="function")throw new TypeError("A commit gesture factory is required");let i=Object.freeze({create:n,createGesture:r,nextStepValue:t.nextStepValue||_e}),o=ft(e.CarrotUI)?e.CarrotUI:null,a=o&&Object.isExtensible(o)?o:Object.assign({},o||{});return a.numericStepper=i,e.CarrotUI=a,e.CarrotNumericStepper=i,i}var me=new WeakMap;function be(e,t){if(!e||typeof e.addEventListener!="function"||typeof e.removeEventListener!="function")throw new TypeError(`${t} must be an event target`);return e}function te(e={},t={}){let{root:n,trigger:r,panel:i,beforeOpen:o,onOpenChange:a}=e,f=e.environment||t,A=e.documentRoot||f.document||n?.ownerDocument||globalThis.document;if(be(n,"root"),be(r,"trigger"),be(i,"panel"),be(A,"documentRoot"),typeof n.contains!="function"||typeof i.contains!="function")throw new TypeError("root and panel must support contains()");let E=me.get(n);E&&E.destroy();let b=r.getAttribute?.("aria-expanded")==="true"&&i.hidden!==!0,l=!1,d;function s(){return l?!1:(n.classList?.toggle?.("is-open",b),i.hidden=!b,r.setAttribute?.("aria-expanded",b?"true":"false"),i.setAttribute?.("aria-hidden",b?"false":"true"),typeof a=="function"&&a(b,d),b)}function g(){return l||!b&&typeof o=="function"&&o(d)===!1?!1:(b=!0,s())}function x(I={}){if(l)return!1;let j=b;return b=!1,s(),I.restoreFocus===!0&&r.focus?.(),j}function p(){return b?(x(),!1):g()}function C(){p()}function L(I){I.key!=="Escape"||!b||(I.preventDefault?.(),x({restoreFocus:!0}))}function _(I){!b||n.contains(I.target)||i.contains(I.target)||x()}function N(I){!b||I.key!=="Escape"||(I.preventDefault?.(),x({restoreFocus:!0}))}function D(){l||(b&&(b=!1,s()),r.removeEventListener("click",C),r.removeEventListener("keydown",L),A.removeEventListener("pointerdown",_),A.removeEventListener("keydown",N),l=!0,me.get(n)===d&&me.delete(n))}return d=Object.freeze({open:g,close:x,toggle:p,sync:s,destroy:D,get isOpen(){return!l&&b}}),i.id&&(r.setAttribute?.("aria-controls",i.id),r.setAttribute?.("aria-describedby",i.id)),s(),r.addEventListener("click",C),r.addEventListener("keydown",L),A.addEventListener("pointerdown",_),A.addEventListener("keydown",N),me.set(n,d),d}function pt(e){return typeof e=="object"&&e!==null||typeof e=="function"}var bn=Object.freeze({create:te,createController:te,mount:te});function mt(e=globalThis,t={}){if(!pt(e))throw new TypeError("A facade target is required");let n=t.createPopoverController||te;if(typeof n!="function")throw new TypeError("A popover controller factory is required");let r=n===te?bn:Object.freeze({create:n,createController:n,mount:n}),i=pt(e.CarrotUI)?e.CarrotUI:null,o=i&&Object.isExtensible(i)?i:Object.assign({},i||{});return o.popover=r,e.CarrotUI=o,r}var ge=new WeakMap;function oe(e,t){return typeof e?.getAttribute=="function"?e.getAttribute(t):null}function he(e){return e?.disabled===!0||typeof e?.hasAttribute=="function"&&e.hasAttribute("disabled")?!0:String(oe(e,"aria-disabled")||"").toLowerCase()==="true"}function gn(e,t){let n=oe(e,t);return n!==null&&String(n).toLowerCase()!=="false"}function hn(e){if(e?.dataset&&Object.prototype.hasOwnProperty.call(e.dataset,"value"))return String(e.dataset.value);let t=oe(e,"data-value");if(t!==null)return t;let n=oe(e,"value");return n!==null?n:e?.value!==void 0&&e.value!==null?String(e.value):null}function yn(e,t){return e===t?!0:typeof e?.contains=="function"&&e.contains(t)}function An(e){let t=String(e?.itemSelector||"").trim();if(!t)throw new TypeError("A segmented control itemSelector is required");return Object.freeze({itemSelector:t,selectedAttribute:String(e?.selectedAttribute||"aria-selected"),orientation:e?.orientation==="vertical"?"vertical":"horizontal",activation:e?.activation==="manual"?"manual":"automatic",onActivate:typeof e?.onActivate=="function"?e.onActivate:null})}function Tn(e){if(!e||typeof e!="object"||typeof e.querySelectorAll!="function"||typeof e.addEventListener!="function"||typeof e.removeEventListener!="function")throw new TypeError("A segmented control root element is required")}function bt(e,t={}){Tn(e);let n=ge.get(e);if(n)return n.sync(),n;let r=An(t),i=new Map,o=[],a=null,f=!1,A=null;function E(){return Array.from(e.querySelectorAll(r.itemSelector))}function b(h){i.has(h)||i.set(h,oe(h,"tabindex"))}function l(h,v){b(h),h.setAttribute("tabindex",String(v))}function d(h=o){return h.find(v=>gn(v,r.selectedAttribute))||null}function s({preferSelected:h=!0}={}){o=E();let v=o.filter(m=>!he(m)),T=d(o),y=T&&v.includes(T)?T:null;h&&y?a=y:v.includes(a)||(a=y||v[0]||null);for(let m of o)l(m,m===a?0:-1);return o}function g(){let h=E(),v=d(h),T=v?h.indexOf(v):-1;return Object.freeze({itemCount:h.length,selectedIndex:T,selectedValue:v?hn(v):null,destroyed:f})}function x(){return f||s({preferSelected:!0}),g()}function p(h){return o.find(v=>yn(v,h?.target))||null}function C(h){a=h;for(let v of o)l(v,v===h?0:-1)}function L(h,v,T){if(!r.onActivate)return;let y=o.indexOf(h);try{r.onActivate(h,y,Object.freeze({source:T,event:v,controller:A}))}finally{f||s({preferSelected:!1})}}function _(h,v){h&&(v?.preventDefault?.(),C(h),typeof h.focus=="function"&&h.focus(),r.activation==="automatic"&&L(h,v,"keyboard"))}function N(h,v){s({preferSelected:!1});let T=o.filter(k=>!he(k));if(!T.length)return null;let y=p(h),m=T.includes(y)?y:T.includes(a)?a:T[0],u=T.indexOf(m);return v==="first"?T[0]:v==="last"?T[T.length-1]:T[(u+(v==="previous"?-1:1)+T.length)%T.length]}function D(h){if(f||h?.isComposing||h?.altKey||h?.ctrlKey||h?.metaKey)return;let v=null;if(h.key==="Home"?v="first":h.key==="End"?v="last":r.orientation==="horizontal"&&h.key==="ArrowLeft"?v="previous":r.orientation==="horizontal"&&h.key==="ArrowRight"?v="next":r.orientation==="vertical"&&h.key==="ArrowUp"?v="previous":r.orientation==="vertical"&&h.key==="ArrowDown"&&(v="next"),v){_(N(h,v),h);return}if(r.activation!=="manual"||h.key!=="Enter"&&h.key!==" "&&h.key!=="Spacebar")return;s({preferSelected:!1});let T=p(h);!T||he(T)||(h.preventDefault?.(),C(T),L(T,h,"keyboard"))}function I(h){if(f)return;s({preferSelected:!1});let v=p(h);v&&!he(v)&&C(v)}function j(){if(f)return!1;f=!0,e.removeEventListener("keydown",D),e.removeEventListener("focusin",I);for(let[h,v]of i)v===null?h.removeAttribute("tabindex"):h.setAttribute("tabindex",v);return i.clear(),o=[],a=null,ge.get(e)===A&&ge.delete(e),!0}return A=Object.freeze({sync:x,destroy:j,snapshot:g}),ge.set(e,A),e.addEventListener("keydown",D),e.addEventListener("focusin",I),x(),A}function gt(e){return typeof e=="object"&&e!==null||typeof e=="function"}function ht(e=globalThis,t={}){if(!gt(e))throw new TypeError("A facade target is required");let n=t.createSegmentedControl||bt;if(typeof n!="function")throw new TypeError("A segmented control factory is required");let r=Object.freeze({create:n}),i=gt(e.CarrotUI)?e.CarrotUI:null,o=i&&Object.isExtensible(i)?i:Object.assign({},i||{});return o.segmentedControl=r,e.CarrotUI=o,e.CarrotSegmentedControl=r,r}var Le=Object.freeze(["toggle","segmented","select","stepper","slider"]);function X(e,t,n){let r=e.createElement(t);return n&&(r.className=n),r}function W(e,t,{text:n,label:r}={}){let i=X(e,"button",t);return i.type="button",n!==void 0&&(i.textContent=n),r&&i.setAttribute("aria-label",r),i}function Cn(e,t,n,r){let i=X(e,"label","c-switch"),o=X(e,"input","c-switch__input");o.type="checkbox",o.setAttribute("aria-label",r.label),i.appendChild(o),i.appendChild(X(e,"span","c-switch__track")),t.appendChild(i),n.toggleInput=o}function Sn(e,t,n,r){let i=X(e,"div","setting-segments");r.optionValues.forEach(o=>{let a=W(e,"setting-segment",{text:r.optionLabel(o)});a.dataset.value=String(o),a.setAttribute("aria-pressed","false"),n.segmentButtons.push(a),i.appendChild(a)}),t.appendChild(i),n.segmentGroup=i}function En(e,t,n,r){let i=W(e,"setting-select setting-select--button",{label:r.label});i.setAttribute("aria-haspopup","dialog"),t.appendChild(i),n.selectInput=i}function vn(e,t,n,r){n.minusButton=W(e,"smallBtn setting-value-arrow setting-value-arrow--prev",{text:"-",label:r.previousLabel}),n.plusButton=W(e,"smallBtn setting-value-arrow setting-value-arrow--next",{text:"+",label:r.nextLabel}),n.unitButton=W(e,"setting-unit-cycle"),t.appendChild(n.minusButton),t.appendChild(n.value),t.appendChild(n.plusButton)}function wn(e,t,n,r){let i=X(e,"div","setting-slider"),o=X(e,"input","setting-slider__input");o.type="range",o.min=String(r.min),o.max=String(r.max),o.step=String(r.step),o.setAttribute("aria-label",r.label),i.appendChild(o),n.sliderInput=o,n.minusButton=W(e,"smallBtn setting-step setting-step--minus",{text:"-"}),n.plusButton=W(e,"smallBtn setting-step setting-step--plus",{text:"+"}),n.unitButton=W(e,"setting-unit-cycle"),t.appendChild(i),t.appendChild(n.minusButton),t.appendChild(n.value),t.appendChild(n.plusButton)}function yt(e={}){let t=e.document;if(!t||typeof t.createElement!="function")throw new TypeError("A document is required to build a setting control");let n=String(e.kind||"");if(!Le.includes(n))throw new TypeError(`Unknown setting control kind: ${n}`);let r=n==="stepper",i=X(t,"div",`ctrl ctrl--${r?"value":n}`),o=W(t,r?"value-surface val setting-value-compact":"value-surface val",{label:e.valueLabel}),a={ctrl:i,value:o,toggleInput:null,segmentGroup:null,segmentButtons:[],selectInput:null,sliderInput:null,minusButton:null,plusButton:null,unitButton:null},f={label:e.label||"",valueLabel:e.valueLabel||"",previousLabel:e.previousLabel||"",nextLabel:e.nextLabel||"",optionValues:Array.isArray(e.optionValues)?e.optionValues:[],optionLabel:typeof e.optionLabel=="function"?e.optionLabel:A=>String(A),min:e.min,max:e.max,step:e.step};return n==="toggle"?Cn(t,i,a,f):n==="segmented"?Sn(t,i,a,f):n==="select"?En(t,i,a,f):r?vn(t,i,a,f):wn(t,i,a,f),!r&&n!=="slider"&&i.appendChild(o),a}function At(e){return typeof e=="object"&&e!==null||typeof e=="function"}function Tt(e=globalThis,t={}){if(!At(e))throw new TypeError("A facade target is required");let n=t.createSettingControl||yt;if(typeof n!="function")throw new TypeError("A setting control factory is required");let r=Object.freeze({createControl:n,kinds:Le}),i=At(e.CarrotUI)?e.CarrotUI:null,o=i&&Object.isExtensible(i)?i:Object.assign({},i||{});return o.settingRow=r,e.CarrotUI=o,r}var _n=new Set(["primary","secondary","danger"]);function q(e){return String(e??"").trim()}function Ln(e){if(typeof e!="function")return q(e);try{return q(e())}catch{return""}}function xn(e,t){let n=t?.document??e.environment?.document??e.document??globalThis.document;if(!n||typeof n.createElement!="function")throw new TypeError("createActionGroup requires a document environment");return n}function In(e){let t=q(e);return _n.has(t)?t:"secondary"}function xe(e){if(!Array.isArray(e))return Object.freeze([]);if(e.length>2)throw new RangeError("State surface action groups support at most 2 actions");let t=new Set,n=[];for(let r of e){let i=r&&typeof r=="object"?r:{},o=q(i.id),a=Ln(i.label),f=i.onActivate??i.invoke;!o||t.has(o)||!a||typeof f!="function"||(t.add(o),n.push(Object.freeze({id:o,label:a,tone:In(i.tone??i.kind),disabled:!!i.disabled,onActivate:f})))}return Object.freeze(n)}function Ct(e){return Array.from(new Set(e.flatMap(t=>q(t).split(/\s+/)).filter(Boolean))).join(" ")}function St(e={},t={}){let n=e.host;if(!n||typeof n.append!="function")return null;let r=xn(e,t),i=r.createElement("div");i.setAttribute("role","group"),i.setAttribute("aria-busy","false"),i.dataset.layout="single",i.dataset.sizing="full",i.dataset.pending="false",i.hidden=!0,n.append(i);let o=new Map,a=Object.freeze([]),f=new Map,A=null,E=0,b=!1,l=q(e.ariaLabel)||"Status actions",d=q(e.className),s=q(e.actionClassName),g=q(e.actionToneClassPrefix),x=typeof e.onPendingChange=="function"?e.onPendingChange:null;function p(){return A?.id||""}function C(T){return Ct(["btn",T.tone==="primary"?"btn--filled":"",T.tone==="danger"?"btn--danger":"","c-action-group__action",`c-action-group__action--${T.tone}`,s,g?`${g}${T.tone}`:""])}function L(){let T=a.length<=1;return Ct(["c-action-group","c-action-group--nowrap",T?"c-action-group--single":"c-action-group--split",T?"c-action-group--full":"c-action-group--equal",d])}function _(){if(b)return;let T=!!A,y=a.length<=1;i.className=L(),i.hidden=a.length===0,i.dataset.layout=y?"single":"split",i.dataset.sizing=y?"full":"equal",i.dataset.pending=String(T),i.setAttribute("aria-label",l),i.setAttribute("aria-busy",String(T));for(let m of a){let u=o.get(m.id).button,F=T||m.disabled;u.className=C(m),u.dataset.actionId=m.id,u.dataset.tone=m.tone,u.textContent=m.label,u.disabled=F,u.tabIndex=F?-1:0,u.setAttribute("aria-disabled",String(F)),u.setAttribute("tabindex",String(u.tabIndex)),F?u.setAttribute("disabled",""):u.removeAttribute("disabled")}x?.(p())}async function N(T){let y=f.get(q(T));if(b||!y||y.disabled||A)return!1;let m=Object.freeze({id:y.id,generation:++E});A=m,_();try{await y.onActivate()}catch{}finally{if(b||A!==m)return!0;A=null,_()}return!0}function D(T){let y=o.get(T);y&&(y.button.removeEventListener("click",y.listener),y.button.remove(),o.delete(T))}function I(T,y={}){if(b)return!1;let m=xe(T),u=new Set(m.map(S=>S.id));A&&!u.has(A.id)&&(A=null,E+=1);for(let S of o.keys())u.has(S)||D(S);for(let S of m){if(o.has(S.id))continue;let R=r.createElement("button");R.type="button";let V=()=>{N(S.id)};R.addEventListener("click",V),o.set(S.id,{button:R,listener:V})}a=m,f=new Map(a.map(S=>[S.id,S])),y.ariaLabel!=null&&(l=q(y.ariaLabel)||"Status actions");let F=Array.from(i.children).map(S=>S.dataset.actionId),k=a.map(S=>S.id);if(F.join("\0")!==k.join("\0"))for(let S of a)i.append(o.get(S.id).button);return _(),!0}function j(T){return b?!1:(l=q(T)||"Status actions",_(),!0)}function h(){let T=a.map(y=>Object.freeze({id:y.id,label:y.label,tone:y.tone,disabled:y.disabled}));return Object.freeze({actions:Object.freeze(T),pendingActionId:p(),layout:a.length<=1?"single":"split",sizing:a.length<=1?"full":"equal"})}function v(){if(b)return!1;b=!0,A=null,E+=1;for(let T of Array.from(o.keys()))D(T);return a=Object.freeze([]),f=new Map,i.remove(),!0}return Object.freeze({element:i,setActions:I,setAriaLabel:j,activate:N,snapshot:h,destroy:v})}var P=Object.freeze({EMPTY:"empty",LOADING:"loading",DEGRADED:"degraded",ERROR:"error",OWNERSHIP:"ownership"}),$=Object.freeze({NEUTRAL:"neutral",INFO:"info",WARNING:"warning",ERROR:"error"}),ae=Object.freeze({PRIMARY:"primary",SECONDARY:"secondary",DANGER:"danger"}),ye=Object.freeze({COVER:"cover",COMPACT:"compact"}),On=new Set(Object.values(P)),Nn=new Set(Object.values($)),Fn=new Set(Object.values(ye)),Dn=Object.freeze({[P.EMPTY]:$.NEUTRAL,[P.LOADING]:$.INFO,[P.DEGRADED]:$.WARNING,[P.ERROR]:$.ERROR,[P.OWNERSHIP]:$.WARNING}),Ie=new WeakMap,Fe=new WeakSet,jn=0;function Z(e){return String(e??"").trim()}function Oe(e){if(typeof e!="function")return Z(e);try{return Z(e())}catch{return""}}function Ne(e){return Array.from(new Set(e.flatMap(t=>Z(t).split(/\s+/)).filter(Boolean))).join(" ")}function Et(e){let t=Z(e);return On.has(t)?t:P.EMPTY}function kn(e,t){let n=Z(e);return Nn.has(n)?n:Dn[t]}function vt(e){let t=Z(e);return Fn.has(t)?t:ye.COVER}function wt(e){return e&&typeof e=="object"?e.featureLabel??e.label??e.name:e}function Mn(e,t){let n=t?.document??e.environment?.document??e.document??globalThis.document;if(!n||typeof n.createElement!="function")throw new TypeError("createStateSurface requires a document environment");return n}function Rn(e,t){if(typeof e=="string")return{state:Et(e),descriptor:t&&typeof t=="object"?t:{}};let n=e&&typeof e=="object"?e:{};return{state:Et(n.state),descriptor:n}}function _t(e={},t={}){let n=e.host;if(!n||typeof n.append!="function")return null;let r=Ie.get(n);if(r){let S=e.featureLabel??e.feature;return S!=null&&r.setFeature(S),r}let i=Mn(e,t),o=`c-state-surface-${++jn}`,a=Z(e.className),f=vt(e.variant),A=wt(e.featureLabel??e.feature),E=null,b=null,l=P.EMPTY,d=$.NEUTRAL,s=f,g="",x=!1,p=i.createElement("section");p.className=Ne(["c-state-surface",`c-state-surface--${d}`,`c-state-surface--state-${l}`,`c-state-surface--${s}`,a]),p.hidden=!0,p.setAttribute("aria-hidden","true"),p.setAttribute("aria-busy","false"),p.dataset.state=l,p.dataset.tone=d,p.dataset.variant=s,p.dataset.pending="false";let C=i.createElement("div");C.className="c-state-surface__body";let L=i.createElement("div");L.className="c-state-surface__copy",L.setAttribute("role","status"),L.setAttribute("aria-live","polite"),L.setAttribute("aria-atomic","true");let _=i.createElement("h2");_.id=`${o}-feature`,_.className="c-state-surface__feature";let N=i.createElement("p");N.id=`${o}-title`,N.className="c-state-surface__title";let D=i.createElement("p");D.id=`${o}-description`,D.className="c-state-surface__description",L.append(_,N,D),C.append(L),p.append(C),n.append(p);function I(){let S=l===P.LOADING||!!g;p.dataset.pending=String(!!g),p.setAttribute("aria-busy",String(S))}let j=St({host:C,className:"c-state-surface__actions",actionClassName:"c-state-surface__action",actionToneClassPrefix:"c-state-surface__action--",onPendingChange(S){g=S,I()}},{document:i});function h(S,R){let V=R.featureLabel!=null?R.featureLabel:A;return Object.freeze({state:S,featureLabel:Oe(V),title:Oe(R.title),description:Oe(R.description),tone:kn(R.tone,S),variant:vt(R.variant??f),actions:xe(R.actions)})}function v(S){l=S.state,d=S.tone,s=S.variant,p.className=Ne(["c-state-surface",`c-state-surface--${d}`,`c-state-surface--state-${l}`,`c-state-surface--${s}`,a]),p.dataset.state=l,p.dataset.tone=d,p.dataset.variant=s,_.textContent=S.featureLabel,_.hidden=!S.featureLabel,N.textContent=S.title,N.hidden=!S.title,D.textContent=S.description,D.hidden=!S.description,S.title||S.featureLabel?(p.setAttribute("aria-labelledby",S.title?N.id:_.id),p.removeAttribute("aria-label")):(p.removeAttribute("aria-labelledby"),p.setAttribute("aria-label","Status")),S.description?p.setAttribute("aria-describedby",D.id):p.removeAttribute("aria-describedby")}function T(S={},R){if(x)return!1;let{state:V,descriptor:Y}=Rn(S,R),H=h(V,Y);return E={...Y},b=H,v(H),j.setActions(H.actions,{ariaLabel:`${H.featureLabel||H.title||"Status"} actions`}),p.hidden=!1,p.setAttribute("aria-hidden","false"),I(),!0}function y(){return x||p.hidden?!1:(E=null,b=null,l=P.EMPTY,d=$.NEUTRAL,s=f,j.setActions([]),g="",p.hidden=!0,p.setAttribute("aria-hidden","true"),p.dataset.state=l,p.dataset.tone=d,p.dataset.variant=s,p.className=Ne(["c-state-surface",`c-state-surface--${d}`,`c-state-surface--state-${l}`,`c-state-surface--${s}`,a]),I(),!0)}function m(S){return x?!1:(A=wt(S),p.hidden||!E?!0:T(l,E))}function u(){let S=j.snapshot();return Object.freeze({visible:!x&&!p.hidden,state:b?.state||P.EMPTY,featureLabel:b?.featureLabel||"",title:b?.title||"",description:b?.description||"",tone:b?.tone||$.NEUTRAL,variant:b?.variant||f,actions:S.actions,pendingActionId:S.pendingActionId})}function F(){return x?!1:(x=!0,E=null,b=null,g="",j.destroy(),Ie.delete(n),Fe.delete(k),p.remove(),!0)}let k=Object.freeze({show:T,hide:y,setFeature:m,snapshot:u,destroy:F});return Ie.set(n,k),Fe.add(k),k}function Lt(e){return!!(e&&typeof e=="object"&&Fe.has(e))}var xt=new WeakMap,zn=new Set(Object.values(ae));function De(e){return String(e??"").trim()}function Pn(e){if(typeof e!="function")return De(e);try{return De(e())}catch{return""}}function It(e){if(typeof e=="string"||typeof e=="function")return Object.freeze({id:"",label:e});let t=e&&typeof e=="object"?e:{};return Object.freeze({id:t.id,label:t.label??t.name})}function Un(e){let t=De(e);return zn.has(t)?t:ae.SECONDARY}function Bn(e){return Array.isArray(e)?e.map(t=>{let n=t&&typeof t=="object"?t:{};return{id:n.id,label:n.label,tone:Un(n.kind??n.tone),disabled:!!n.disabled,onActivate:n.invoke??n.onActivate}}):[]}function qn(e){let t=new WeakMap,n=new WeakSet;function r(o={}){let a=o.host;if(!a||typeof a.append!="function")return null;let f=t.get(a);if(f)return o.feature!=null&&f.setFeature(o.feature),f;let A=It(o.feature),E=e.create({host:a,className:o.className,featureLabel:A.label});if(!E)return null;let b=!1;function l(C={}){if(b)return!1;let L=C&&typeof C=="object"?C:{};return E.show(P.OWNERSHIP,{title:L.title,description:L.description,tone:$.NEUTRAL,actions:Bn(L.actions)})}function d(){return b?!1:E.hide()}function s(C){return b?!1:(A=It(C),E.setFeature(A.label))}function g(){let C=E.snapshot(),L=C.actions.map(_=>Object.freeze({id:_.id,label:_.label,kind:_.tone,disabled:_.disabled}));return Object.freeze({visible:C.visible,featureId:C.visible?Pn(A.id):"",featureLabel:C.featureLabel,title:C.title,description:C.description,actions:Object.freeze(L),pendingActionId:C.pendingActionId})}function x(){return b?!1:(b=!0,t.delete(a),n.delete(p),E.destroy(),!0)}let p=Object.freeze({show:l,hide:d,setFeature:s,snapshot:g,destroy:x});return t.set(a,p),n.add(p),p}function i(o){return!!(o&&typeof o=="object"&&n.has(o))}return Object.freeze({ACTION_KIND:ae,create:r,isNotice:i})}function $n(e={}){return Object.freeze({STATE:P,TONE:$,VARIANT:ye,ACTION_KIND:ae,create(t={}){return _t(t,e)},isSurface:Lt})}function Gn(e,t){let n=e.CarrotUI;if(n&&typeof n=="object"&&Object.isExtensible(n))return n.stateSurface=t,n;let r=n&&typeof n=="object"?{...n}:{};return r.stateSurface=t,e.CarrotUI=r,r}function Ot(e=globalThis,t={}){if(!e||typeof e!="object"&&typeof e!="function")throw new TypeError("installStateSurfaceFacade requires an object target");let n=xt.get(e);if(!n){let r={...t,document:t.document??e.document??globalThis.document},i=$n(r);n=Object.freeze({stateSurface:i,streamOwnershipNotice:qn(i)}),xt.set(e,n)}return Gn(e,n.stateSurface),e.StreamOwnershipNotice=n.streamOwnershipNotice,n.stateSurface}var G=globalThis,se=G.document;se.getElementById("carrotAssetManifest")&&Be(se,G);Ye(G,se);lt(G);mt(G);at(G,{documentRoot:se});Ot(G,{document:se});ht(G);dt(G);$e(G);Tt(G);et(G);})();
+(()=>{var Ut=/^[0-9a-f]{64}$/,Bt=/^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*\\).+$/,ee=class extends Error{constructor(t,n){super(t,n),this.name="AssetManifestError"}};function qt(e){if(e?.schemaVersion!==1||!Array.isArray(e.assets))throw new ee("Invalid asset manifest schema");let t=new Map,n=new Set;for(let r of e.assets){if(typeof r?.id!="string"||typeof r.path!="string"||!Bt.test(r.path)||typeof r.hash!="string"||!Ut.test(r.hash)||t.has(r.id)||n.has(r.path))throw new ee("Invalid asset manifest entry");t.set(r.id,r),n.add(r.path)}return t}function $t(e){let t=qt(e);return Object.freeze({resolve(n){let r=t.get(n);if(!r)throw new ee(`Unknown asset id: ${n}`);return`/${r.path}?v=${r.hash}`}})}function Ve(e=document,t=globalThis){let n=e.getElementById("carrotAssetManifest");if(!n)throw new ee("Asset manifest element is missing");let r;try{r=JSON.parse(n.textContent||"")}catch(o){throw new ee("Asset manifest JSON is invalid",{cause:o})}let i=$t(r);return t.CarrotAssetUrl=i,i}var ce=Object.freeze({webUi:"web_ui",profile:"profile",restore:"restore",resetDefaults:"reset_defaults",intro:"intro",undo:"undo",device:"device"});function Vt(e){let t=String(e||"").trim();return Object.values(ce).includes(t)?t:ce.webUi}function He(e={}){let t=e.postJson;if(typeof t!="function")throw new TypeError("postJson must be a function");let n=e.endpoint||"/api/param_set",r=typeof e.onCommitted=="function"?e.onCommitted:null,i=typeof e.dispatchEvent=="function"?e.dispatchEvent:null;async function o(s,u,h={}){let T=String(s||"").trim();if(!T)throw new TypeError("A parameter name is required");let d=Vt(h.source),l=await t(n,{name:T,value:u,source:d}),p=l&&Object.prototype.hasOwnProperty.call(l,"value")?l.value:u,c=String(p)!==String(u),y=Object.freeze({name:T,value:p,requested:u,source:d,adjusted:c});return r?.(y),i?.(y),y}return Object.freeze({commit:o,sources:ce})}function Ht(e){return typeof e=="object"&&e!==null||typeof e=="function"}function Ge(e=globalThis,t={}){if(!Ht(e))throw new TypeError("A facade target is required");let n=t.createSettingCommitter||He;if(typeof n!="function")throw new TypeError("A committer factory is required");let r=Object.freeze({create:n,sources:ce});return e.CarrotParamCommit=r,r}var Gt=["a[href]","area[href]","button:not([disabled])",'input:not([disabled]):not([type="hidden"])',"select:not([disabled])","textarea:not([disabled])",'[tabindex]:not([tabindex="-1"])','[contenteditable]:not([contenteditable="false"])',"audio[controls]","video[controls]","iframe","object","embed","summary"].join(",");function Ye(e,t){let n=t.document??e?.ownerDocument??globalThis.document,r=t.window??n?.defaultView??globalThis.window??globalThis,i=t.requestAnimationFrame??r?.requestAnimationFrame?.bind(r)??(u=>globalThis.setTimeout(u,0)),o=t.cancelAnimationFrame??r?.cancelAnimationFrame?.bind(r)??(u=>globalThis.clearTimeout(u)),s=t.getComputedStyle??r?.getComputedStyle?.bind(r)??(()=>({}));return{documentRoot:n,requestFrame:i,cancelFrame:o,getComputedStyle:s}}function Ke(e,t={}){if(!e?.querySelectorAll)return[];let{getComputedStyle:n}=Ye(e,t);return Array.from(e.querySelectorAll(Gt)).filter(r=>{if(r.disabled||r.hidden||r.hasAttribute?.("disabled"))return!1;let i=r.getAttribute?.("tabindex");if(i!=null&&Number(i)<0)return!1;let o=r;for(;o&&o!==e;){if(o.hasAttribute?.("inert")||o.getAttribute?.("aria-hidden")==="true")return!1;o=o.parentElement??o.parentNode}let s=r.getBoundingClientRect?.();if(s&&s.width===0&&s.height===0)return!1;let u=n(r)||{};return u.visibility!=="hidden"&&u.display!=="none"})}function Kt(e,t){let n=typeof t=="function"?t():t;return n?typeof n=="string"?e.querySelector?.(n)??null:typeof n.focus=="function"?n:null:null}function pe(e,t={},n={}){if(!e)return Object.freeze({activate(){},deactivate(){},isActive:()=>!1});let{documentRoot:r,requestFrame:i,cancelFrame:o}=Ye(e,n),s=!1,u=0,h=null,T=null;function d(p){if(!s)return;if(p.key==="Escape"&&typeof t.escape=="function"){t.escape(p);return}if(p.key!=="Tab")return;let c=Ke(e,n);if(!c.length){p.preventDefault(),e.focus?.();return}let y=c[0],x=c[c.length-1],m=r?.activeElement;p.shiftKey?(m===y||!e.contains?.(m))&&(p.preventDefault(),x.focus()):(m===x||!e.contains?.(m))&&(p.preventDefault(),y.focus())}function l(){T!=null&&(o(T),T=null)}return Object.freeze({activate(){if(s)return;s=!0;let p=++u;if(h=t.returnFocus||r?.activeElement||null,e.hasAttribute?.("tabindex")||e.setAttribute?.("tabindex","-1"),r?.addEventListener?.("keydown",d,!0),t.focusInitial===!1)return;let c=Kt(e,t.initialFocus),x=(!c||c===e||typeof e.contains!="function"||e.contains(c)?c:null)||Ke(e,n)[0]||e;T=i(()=>{T=null,!(!s||p!==u)&&x?.focus?.({preventScroll:t.initialFocusPreventScroll!==!1})})},deactivate(p={}){if(!s){u+=1,l();return}s=!1,u+=1,l(),r?.removeEventListener?.("keydown",d,!0);let c=h;if(h=null,p.restoreFocus===!1||typeof c?.focus!="function")return;(typeof r?.contains=="function"?r.contains(c):c.isConnected!==!1)&&c.focus({preventScroll:!0})},isActive(){return s}})}function We(e){if(e&&typeof e=="object"){let n=String(e.mode||"").trim().toLowerCase();if(n==="indeterminate")return{mode:n,value:null};if(n==="determinate"){let r=e.value==null?Number.NaN:Number(e.value);return Number.isFinite(r)?{mode:n,value:Math.max(0,Math.min(100,r))}:{mode:"indeterminate",value:null}}}let t=e==null?Number.NaN:Number(e);return Number.isFinite(t)?{mode:"determinate",value:Math.max(0,Math.min(100,t))}:{mode:"indeterminate",value:null}}function Yt(e,t,n){let r=We(n);if(!e)return r;let i=r.mode==="determinate";if(e.dataset.progressMode=r.mode,e.setAttribute("aria-busy",i?"false":"true"),!i)return e.removeAttribute("value"),e.removeAttribute("aria-valuenow"),e.removeAttribute("data-progress-value"),t&&(t.textContent="…",t.removeAttribute("data-progress-value")),r;let o=Math.round(r.value);return e.max=100,e.value=r.value,e.setAttribute("value",String(r.value)),e.setAttribute("aria-valuenow",String(o)),e.dataset.progressValue=String(r.value),t&&(t.textContent=`${o}%`,t.dataset.progressValue=String(r.value)),r}function re(e,t,n){let r=e.createElement(t);return r.className=n,r}function Xe(e,t={}){if(!e?.createElement)return null;let n=re(e,"div","app-progress"),r=re(e,"div","app-progress__header"),i=re(e,"div","app-progress__message"),o=re(e,"output","app-progress__value"),s=re(e,"progress","app-progress__track"),u=re(e,"div","app-progress__summary");i.setAttribute("aria-live","polite"),o.setAttribute("aria-live","polite"),s.max=100,s.setAttribute("max","100"),t.progressLabel&&s.setAttribute("aria-label",String(t.progressLabel)),r.append(i,o),n.append(r,s,u);let h={element:n,progressElement:s,valueElement:o,setMessage(T){i.textContent=String(T||"")},setProgressState(T){return Yt(s,o,T)},setSummary(T){u.textContent=String(T||"")}};return h.setMessage(t.message),h.setProgressState(t.progressState??t.progress),h.setSummary(t.summary),Object.freeze(h)}var Wt=180,Xt=Object.freeze(["app-dialog--choice","app-dialog--choice-list","app-dialog--choice-grid","app-dialog--choice-value-grid","app-dialog--form","app-dialog--input","app-dialog--progress"]),Jt=Object.freeze(["appDialog","appBranchPicker","appCarPicker","settingSearchPanel"]);function Zt(e){return!e||e.labelHtml?"":String(e.label??"").trim()}function Je(e){return!e||typeof e!="object"?"":String(e.heading??"").trim()}function Qt(e){return!!e&&!Je(e)&&(e.label!=null||!!e.labelHtml)}function en(e){let t=[];for(let n of Array.isArray(e)?e:[]){let r=Je(n);if(r){t.push({heading:r,items:[]});continue}Qt(n)&&(t.length||t.push({heading:"",items:[]}),t[t.length-1].items.push(n))}return t.filter(n=>n.items.length>0)}function tn(e,t={}){let n=String(t.choiceLayout||t.choiceKind||"").trim();return n==="grid"||n==="value-grid"||n==="values"?"value-grid":n==="list"||n==="action-list"||n==="actions"?"list":e.length>4&&e.every(i=>{let o=Zt(i);return o&&o.length<=5&&!i.danger&&/^[+-]?(?:\d+|\d+\.\d+|[A-Za-z]{1,4})$/.test(o)})?"value-grid":"list"}function nn(e,t={}){let n=Array.isArray(e)?e.flatMap(r=>r.items||[]):[];return n.length?e.some(r=>!!r?.heading)?"list":tn(n,t):""}function rn(e,t={}){let n=Number(t.choiceColumns||t.columns);return Number.isInteger(n)&&n>=2&&n<=6?n:e<=4?Math.max(2,e):e<=25?5:4}function on(e){let t=e.target??globalThis,n=e.document??t.document,r=e.setTimeout??t.setTimeout?.bind(t)??globalThis.setTimeout.bind(globalThis),i=e.clearTimeout??t.clearTimeout?.bind(t)??globalThis.clearTimeout.bind(globalThis),o=e.requestAnimationFrame??t.requestAnimationFrame?.bind(t)??(u=>r(u,0)),s=e.cancelAnimationFrame??t.cancelAnimationFrame?.bind(t)??i;return{target:t,documentRoot:n,setTimer:r,requestFrame:o,cancelFrame:s}}function Ze(e={}){let{target:t,documentRoot:n,setTimer:r,requestFrame:i,cancelFrame:o}=on(e),s=a=>n?.getElementById?.(a)??null,u=s("appDialog"),h=s("appDialogBackdrop"),T=s("appDialogTitle"),d=s("appDialogBody"),l=s("appDialogChoices"),p=s("appDialogInputWrap"),c=s("appDialogInput"),y=s("appDialogInputError"),x=s("appDialogCopy"),m=s("appDialogDefault"),g=s("appDialogCancel"),_=s("appDialogConfirm"),L=null,D=0,j=null,I=e.createFocusTrap??((a,v)=>pe(a,v,{document:n,window:t,requestAnimationFrame:i,cancelAnimationFrame:o}));function M(a,v){let O=t.getUIText;return typeof O=="function"?O.call(t,a,v):v}function A(){let a=Jt.some(v=>{let O=s(v);return!!(O&&!O.hidden)});n?.body?.classList?.toggle("dialog-open",a)}function w(){u?.classList?.remove(...Xt),l&&(l.className="app-dialog__choices",l.style.removeProperty("--app-dialog-choice-columns"))}function S(a){return a.mode==="prompt"||a.mode==="form"||a.mode==="choice"?null:!1}function C(a){a.lastFocus&&typeof a.lastFocus.focus=="function"&&a.lastFocus.focus()}function b(a,v){if(a.serial!==D){a.resolve(v);return}u.hidden=!0,A(),w(),l&&(l.hidden=!0,l.innerHTML=""),m&&(m.hidden=!0,m.onclick=null),p&&(p.hidden=!0),y&&(y.hidden=!0,y.textContent=""),c&&(c.disabled=!1,c.value="",c.placeholder="",c.type="text",c.removeAttribute("autocomplete"),c.removeAttribute("autocapitalize"),c.removeAttribute("inputmode"),c.removeAttribute("aria-labelledby"),c.spellcheck=!0),d&&(d.hidden=!1),_&&(_.disabled=!1),g&&(g.disabled=!1),C(a),j=null,a.resolve(v)}function f(a,v){!a||L!==a||a.closing||(a.closing=!0,L=null,j=a.lastFocus||j,a.openFrame!=null&&(o(a.openFrame),a.openFrame=null),a.focusTrap?.deactivate?.({restoreFocus:!1}),u?.classList?.remove("is-open"),a.closeTimer=r(()=>{a.closeTimer=null,b(a,v)},Wt))}function F(a){f(L,a)}async function k(){let a=L;if(!(!a||a.submitting||a.canceling)){if(typeof a.onCancel!="function"){f(a,S(a));return}a.canceling=!0,g&&(g.disabled=!0,g.textContent=a.cancelingLabel);try{await a.onCancel(),a.closeOnCancel&&L===a&&!a.closing&&f(a,S(a))}catch(v){if(L!==a||a.closing)return;a.canceling=!1,g&&(g.disabled=!1,g.textContent=a.cancelLabel),typeof t.showAppToast=="function"&&t.showAppToast(v?.message||String(v),{tone:"error",duration:3600})}}}async function E(){if(!L||L.submitting)return;let a=L,O=a.mode==="prompt"||a.mode==="form"?c?c.value:"":!0;if(a.mode!=="form"||typeof a.onSubmit!="function"){f(a,O);return}a.submitting=!0,y&&(y.hidden=!0,y.textContent=""),c&&(c.disabled=!0),_&&(_.disabled=!0,_.textContent=a.submittingLabel),g&&(g.disabled=!0);try{await a.onSubmit(O),L===a&&!a.closing&&f(a,O)}catch(R){if(L!==a||a.closing)return;a.submitting=!1,c&&(c.disabled=!1,c.focus()),_&&(_.disabled=!1,_.textContent=a.confirmLabel),g&&(g.disabled=!1),y&&(y.textContent=R?.message||String(R),y.hidden=!1)}}function P(a={}){if(!u||!T||!d||!_||!g)return a.mode==="prompt"||a.mode==="form"?Promise.resolve(null):Promise.resolve(a.mode==="alert");let v=j;L&&(v=L.lastFocus||v,f(L,S(L))),j=null;let O=a.mode||"alert",R=O==="form",Z=a.title||(O==="confirm"?M("confirm_title","Confirm"):O==="prompt"||O==="form"?M("input_title","Input"):M("notice","Notice")),W=a.message||"",U=a.messageHtml||"",zt=!!a.html,Pe=a.confirmLabel||M("ok","OK"),Re=a.cancelLabel||M("cancel","Cancel"),ze=a.defaultActionLabel||"",Ue=O==="prompt"&&!!ze,ve=en(a.choices),Be=ve.flatMap(Y=>Y.items),ae=Be.length>0,qe=O==="choice"||ae,se=nn(ve,a),$e=O!=="alert"&&a.showCancel!==!1;w(),R&&u.classList.add("app-dialog--form"),(O==="prompt"||R)&&u.classList.add("app-dialog--input"),ae&&(u.classList.add("app-dialog--choice"),u.classList.add(se==="value-grid"?"app-dialog--choice-grid":"app-dialog--choice-list"),se==="value-grid"&&u.classList.add("app-dialog--choice-value-grid")),T.textContent=Z,zt?d.innerHTML=String(U||W):d.textContent=String(W),d.hidden=R&&!String(U||W).trim(),d.style.flex=ae?"0 0 auto":"1 1 auto",_.textContent=Pe,g.textContent=Re,_.disabled=!1,g.disabled=!1,g.hidden=!$e,g.setAttribute("aria-hidden",$e?"false":"true"),_.hidden=qe,_.setAttribute("aria-hidden",qe?"true":"false"),m&&(m.hidden=!Ue,m.textContent=ze,m.disabled=!1,m.onclick=Ue?()=>F(a.defaultActionValue??""):null);let Ee=a.copyText||"";if(x&&(x.hidden=!Ee,x.textContent=a.copyLabel||M("copy","Copy"),x.onclick=Ee?()=>{let Y=t.copyToClipboard;typeof Y=="function"&&Y.call(t,Ee);let N=t.alert;typeof N=="function"&&N.call(t,M("copied","Copied"))}:null),l){l.innerHTML="",l.hidden=!ae,l.className=`app-dialog__choices app-dialog__choices--${se||"list"}`,se==="value-grid"?l.style.setProperty("--app-dialog-choice-columns",String(rn(Be.length,a))):l.style.removeProperty("--app-dialog-choice-columns");let Y=N=>{let q=n.createElement("button");q.type="button";let z=N.danger?"btn btn--danger app-dialog__choiceBtn":"btn app-dialog__choiceBtn";return z+=se==="value-grid"?" app-dialog__choiceBtn--value":" app-dialog__choiceBtn--action",(N.current||N.selected)&&(z+=" is-current"),N.className&&(z+=` ${N.className}`),q.className=z,N.labelHtml?q.innerHTML=N.labelHtml:q.textContent=String(N.label),q.addEventListener("click",()=>F(N.value)),q};ve.forEach((N,q)=>{if(!N.heading){for(let we of N.items)l.appendChild(Y(we));return}let z=n.createElement("div");z.className="app-dialog__choiceGroup",z.setAttribute("role","group");let ne=`appDialogChoiceHeading${q}`;z.setAttribute("aria-labelledby",ne);let de=n.createElement("div");de.id=ne,de.className="app-dialog__choiceHeading",de.textContent=N.heading,z.appendChild(de);for(let we of N.items)z.appendChild(Y(we));l.appendChild(z)})}if(p&&c){let Y=O==="prompt"||R;p.hidden=!Y,c.disabled=!1,c.value=a.defaultValue??"",c.placeholder=a.placeholder||"",c.type=a.inputType==="password"?"password":"text";let N=a.autocomplete??(R?"off":""),q=a.autocapitalize??(R?"none":"");N?c.autocomplete=N:c.removeAttribute("autocomplete"),q?c.autocapitalize=q:c.removeAttribute("autocapitalize"),c.spellcheck=a.spellcheck==null?!R:a.spellcheck===!0,R?c.setAttribute("aria-labelledby","appDialogTitle"):c.removeAttribute("aria-labelledby"),a.inputMode?c.inputMode=a.inputMode:c.removeAttribute("inputmode"),y&&(y.hidden=!0,y.textContent="")}return new Promise(Y=>{let N={resolve:Y,mode:O,serial:++D,onSubmit:a.onSubmit,onCancel:a.onCancel,closeOnCancel:a.closeOnCancel!==!1,submitting:!1,canceling:!1,closing:!1,confirmLabel:Pe,cancelLabel:Re,cancelingLabel:a.cancelingLabel||M("canceling","Canceling..."),submittingLabel:a.submittingLabel||M("saving","Saving..."),lastFocus:v||n.activeElement||null,focusTrap:null,openFrame:null,closeTimer:null};L=N,u.hidden=!1,A();let q=u.querySelector?.(".app-dialog__sheet")||u;N.focusTrap=I(q,{focusInitial:!1,returnFocus:N.lastFocus}),N.focusTrap?.activate?.(),N.openFrame=i(()=>{if(N.openFrame=null,!(L!==N||N.serial!==D||N.closing))if(u.classList.add("is-open"),(O==="prompt"||R)&&c)c.focus(),c.select();else if(ae&&l){let z=l.querySelector(".is-current"),ne=z||l.querySelector("button");ne&&typeof ne.focus=="function"&&(ne.focus({preventScroll:!!z}),z&&typeof z.scrollIntoView=="function"&&z.scrollIntoView({block:"center",inline:"nearest"}))}else O==="choice"?g.focus():_.focus()})})}function G(a,v={}){return P({mode:"alert",title:v.title,message:a,messageHtml:v.messageHtml,html:v.html,confirmLabel:v.confirmLabel,copyText:v.copyText})}function X(a={}){let v=P({mode:"choice",title:a.title,message:" ",cancelLabel:a.cancelLabel,cancelingLabel:a.cancelingLabel,onCancel:a.onCancel,closeOnCancel:!1}),O=L;u?.classList?.add("app-dialog--progress"),d&&(d.style.flex="0 0 auto");let R=Xe(n,a);d&&R&&d.replaceChildren(R.element);let Z=()=>!!(O&&L===O&&!O.closing),W={completion:v,setCancelHandler(U){Z()&&(O.onCancel=typeof U=="function"?U:null,g&&(g.hidden=!O.onCancel,g.setAttribute("aria-hidden",O.onCancel?"false":"true")))},setCanceling(U){!Z()||!g||(O.canceling=!!U,g.disabled=O.canceling,g.textContent=O.canceling?O.cancelingLabel:O.cancelLabel)},setMessage(U){Z()&&R?.setMessage(U)},setProgressState(U){Z()&&R?.setProgressState(U)},setProgress(U){W.setProgressState(U)},setSummary(U){Z()&&R?.setSummary(U)},close(){return Z()&&f(O,!0),v}};return W.setMessage(a.message),W.setProgressState(a.progressState??a.progress),W.setSummary(a.summary),W.setCancelHandler(a.onCancel),Object.freeze(W)}function K(a,v={}){return P({mode:"confirm",title:v.title,message:a,messageHtml:v.messageHtml,html:v.html,confirmLabel:v.confirmLabel,cancelLabel:v.cancelLabel})}function kt(a,v={}){return P({mode:"prompt",title:v.title,message:a,confirmLabel:v.confirmLabel,cancelLabel:v.cancelLabel,defaultValue:v.defaultValue,defaultActionLabel:v.defaultActionLabel,defaultActionValue:v.defaultActionValue,showCancel:v.showCancel,placeholder:v.placeholder})}function Pt(a,v={}){return P({mode:"form",title:v.title,message:a,defaultValue:v.defaultValue,placeholder:v.placeholder,inputType:v.inputType,autocomplete:v.autocomplete,autocapitalize:v.autocapitalize,inputMode:v.inputMode,spellcheck:v.spellcheck,confirmLabel:v.confirmLabel,cancelLabel:v.cancelLabel,submittingLabel:v.submittingLabel,onSubmit:v.onSubmit})}h&&(h.onclick=k),g&&(g.onclick=k),_&&(_.onclick=E);function Rt(a){if(!L)return;if(a.key==="Escape"){a.preventDefault(),L.mode==="alert"?f(L,!0):k();return}if(a.key!=="Enter"||a.shiftKey||a.isComposing||a.defaultPrevented)return;let v=a.target?.tagName;v==="BUTTON"||v==="SELECT"||v==="TEXTAREA"||v==="A"||v==="INPUT"&&a.target!==c||a.target?.isContentEditable||(a.preventDefault(),E())}return n?.addEventListener?.("keydown",Rt),Object.freeze({openAppDialog:P,openAppProgressDialog:X,appAlert:G,appConfirm:K,appPrompt:kt,appForm:Pt,syncModalBodyLock:A,cancelAppDialog:k,confirmAppDialog:E})}var an=Object.freeze(["openAppDialog","openAppProgressDialog","appAlert","appConfirm","appPrompt","appForm","syncModalBodyLock","createFocusTrap"]);function Qe(e=globalThis,t=e.document,n={}){let r=(h,T={})=>pe(h,T,{document:t,window:e,requestAnimationFrame:n.requestAnimationFrame,cancelAnimationFrame:n.cancelAnimationFrame,getComputedStyle:n.getComputedStyle}),i=Ze({...n,target:e,document:t,createFocusTrap:r}),o=Object.freeze({openAppDialog:i.openAppDialog,openAppProgressDialog:i.openAppProgressDialog,appAlert:i.appAlert,appConfirm:i.appConfirm,appPrompt:i.appPrompt,appForm:i.appForm,syncModalBodyLock:i.syncModalBodyLock,createFocusTrap:r}),s=e.CarrotUI&&typeof e.CarrotUI=="object"?e.CarrotUI:null,u=s&&Object.isExtensible(s)?s:{...s||{}};u.dialog=o,e.CarrotUI=u;for(let h of an)e[h]=o[h];return o}function et(e,t,n){let r=String(t??"");if(n)for(let[i,o]of Object.entries(n))r=r.replaceAll(`{${i}}`,String(o));return r}function tt(e){return String(e??"")}function nt(e){let t=Number(e?.summary?.selected||0);return Number.isFinite(t)&&t>0?t:(Array.isArray(e?.entries)?e.entries:[]).filter(r=>r?.apply).length}function Le(e,t={}){let n=t.escape||tt,r=t.text||et;return''+[["changed","settings_diff_changed","changed"],["same","settings_diff_same","same"],["skipped","settings_diff_skipped","skipped"],["invalid","settings_diff_invalid","invalid"]].map(([o,s,u])=>`
${n(r(s,u))}${n(e?.[o]??0)}
`).join("")+"
"}function sn(e,t,n){let r=e.filter(o=>o?.status==="invalid");if(!r.length)return"";let i=r.map(o=>`${t(o.key)}`+(o.reason?`${t(o.reason)}`:"")+"").join("");return`${t(n("settings_diff_invalid_detail","{count} could not be applied",{count:r.length}))}
`}function rt(e,t={}){let n=t.escape||tt,r=t.text||et,i=Array.isArray(e?.entries)?e.entries:[],o=i.filter(m=>m?.apply||m?.status==="changed"),s=Number.isFinite(t.limit)?Math.max(1,t.limit):80,u=o.slice(0,s),h=Math.max(0,o.length-u.length),T=t.currentLabel||r("settings_diff_current","Current"),d=t.nextLabel||r("settings_diff_apply","Apply"),l=t.changedLabel||r("settings_diff_changed_status","Changed"),p=sn(i,n,r),c=Le(e?.summary,{escape:n,text:r});if(!o.length)return`${c}${n(r("settings_diff_no_changes","No changes to apply."))}
`+p;let y=u.map(m=>`${n(T)}${n(m.current)}
>
${n(d)}${n(m.value)}
`).join(""),x=h?`${n(r("settings_diff_more","{count} more changes hidden",{count:h}))}
`:"";return`${c}${y}
${x}${p}`}function it(e){return typeof e=="object"&&e!==null||typeof e=="function"}function ot(e=globalThis,t={}){if(!it(e))throw new TypeError("A facade target is required");let n=Object.freeze({renderTable:t.renderDiffTable||rt,renderSummary:t.renderDiffSummary||Le,selectedCount:t.getDiffSelectedCount||nt}),r=it(e.CarrotUI)?e.CarrotUI:null,i=r&&Object.isExtensible(r)?r:Object.assign({},r||{});return i.diffTable=n,e.CarrotUI=i,n}var ct=Object.freeze({default:"i",info:"i",success:"✓",error:"✕",warn:"!",offline:"!",hint:"i"}),cn=Object.freeze({default:"",info:"is-info",success:"is-success",error:"is-error",warn:"is-warn",offline:"is-warn",hint:"is-hint"}),ln=2600,un=220;function fn(e){let t=typeof e=="string"?e:"default";return Object.hasOwn(ct,t)?t:"default"}function _e(e,t){if(e==null)return t;let n=Number(e);return Number.isFinite(n)&&n>=0?n:t}function at(e,t){return!e||!t?!1:typeof t.contains=="function"?t.contains(e):e.parentNode===t}function st(e){e&&(typeof e.remove=="function"?e.remove():e.parentNode?.removeChild?.(e))}function me(e={},t={}){let n=e.environment||t,r=e.host||null,i=e.documentRoot||n.document||r?.ownerDocument||globalThis.document,o=n.setTimeout||e.setTimeout||globalThis.setTimeout?.bind(globalThis),s=n.clearTimeout||e.clearTimeout||globalThis.clearTimeout?.bind(globalThis),u=n.requestAnimationFrame||e.requestAnimationFrame||(C=>o(C,0)),h=n.cancelAnimationFrame||e.cancelAnimationFrame||s,T=_e(e.defaultDuration,ln),d=_e(e.removeDelay,un),l=0,p=null,c=null,y=null,x=null,m=null,g=!1;function _(){c!=null&&s?.(c),y!=null&&s?.(y),x!=null&&h?.(x),c=null,y=null,x=null}function L(C,b){g||C!==l||b!==p||(st(b),p=null,y=null,m=null)}function D(C,b){if(d===0){L(C,b);return}y=o?.(()=>L(C,b),d)??null}function j(C,b){g||C!==l||b!==p||(b.classList?.remove?.("is-visible"),c=null,D(C,b))}function I(){let C=!!(p&&at(p,r));return Object.freeze({active:C,visible:C&&!!p.classList?.contains?.("is-visible"),message:C?m?.message??"":"",tone:C?m?.tone??"default":null,duration:C?m?.duration??null:null,glyph:C?m?.glyph??"":"",className:C&&p.className||"",serial:l,destroyed:g})}function M(){if(p&&at(p,r))return p;if(!r||typeof i?.createElement!="function")return null;let C=i.createElement("div");if(typeof r.replaceChildren=="function")r.replaceChildren(C);else{for(;r.firstChild;)r.removeChild(r.firstChild);r.appendChild(C)}return p=C,C}function A(C,b={}){if(g||C==null||String(C).length===0)return I();let f=M();if(!f)return I();_();let F=++l,k=fn(b.tone),E=_e(b.duration,T),P=cn[k],G=ct[k];f.className=P?`app-toast ${P}`:"app-toast";let X=i.createElement("span");X.className="app-toast__icon",X.setAttribute?.("aria-hidden","true"),X.textContent=G;let K=i.createElement("div");return K.className="app-toast__msg",K.textContent=String(C),f.replaceChildren(X,K),m={message:String(C),tone:k,duration:E,glyph:G},x=u?.(()=>{g||F!==l||f!==p||(x=null,f.classList?.add?.("is-visible"))})??null,c=o?.(()=>j(F,f),E)??null,I()}function w(C={}){if(g||!p)return I();let b=p;_();let f=++l;return b.classList?.remove?.("is-visible"),C.immediate===!0?L(f,b):D(f,b),I()}function S(){g||(_(),l+=1,st(p),p=null,m=null,g=!0)}return Object.freeze({show:A,dismiss:w,destroy:S,snapshot:I})}var lt=new WeakMap;function ut(e){return typeof e=="object"&&e!==null||typeof e=="function"}function dn(e,t,n){let r=ut(e.CarrotUI)?e.CarrotUI:null,i=r&&Object.isExtensible(r)?r:Object.assign({},r||{});i[t]=n,e.CarrotUI=i}function ft(e=globalThis,t={}){if(!ut(e))throw new TypeError("A facade target is required");let n=lt.get(e),r=t.documentRoot||e.document,i=t.host||r?.getElementById?.("appToastHost")||null,o={document:r,setTimeout:e.setTimeout?.bind?.(e),clearTimeout:e.clearTimeout?.bind?.(e),requestAnimationFrame:e.requestAnimationFrame?.bind?.(e),cancelAnimationFrame:e.cancelAnimationFrame?.bind?.(e),...t.environment||{}},s=t.controller||me({host:i,documentRoot:r,defaultDuration:t.defaultDuration,removeDelay:t.removeDelay,environment:o});if(!s||["show","dismiss","destroy","snapshot"].some(T=>typeof s[T]!="function"))throw new TypeError("A toast controller is required");let u=Object.freeze({create:me,createController:me,show:s.show,dismiss:s.dismiss,destroy:s.destroy,snapshot:s.snapshot});function h(T,d,l){if(d&&typeof d=="object")return s.show(T,d);let p={};return d!=null&&d!==""&&(p.tone=d),l!=null&&(p.duration=l),s.show(T,p)}return dn(e,"toast",u),e.showAppToast=h,n&&n!==s&&n.destroy(),lt.set(e,s),u}var pn='[role^="menuitem"]',be=new WeakMap;function ge(e,t){if(!e||typeof e.addEventListener!="function"||typeof e.removeEventListener!="function")throw new TypeError(`${t} must be an event target`);return e}function dt(e){return!!(e?.disabled||e?.hidden||e?.getAttribute?.("disabled")!=null||e?.getAttribute?.("aria-disabled")==="true"||e?.getAttribute?.("aria-hidden")==="true"||e?.classList?.contains?.("is-disabled"))}function mn(e,t,n){let r=typeof e?.closest=="function"?e.closest(t):null;if(!r){for(let i=e;i&&i!==n;i=i.parentElement||i.parentNode)if(typeof i.matches=="function"&&i.matches(t)){r=i;break}}return r&&n.contains(r)?r:null}function xe(e){typeof e?.focus=="function"&&e.focus()}function ie(e={},t={}){let{root:n,trigger:r,panel:i,itemSelector:o=pn,beforeOpen:s,onSelect:u}=e,h=e.environment||t,T=e.documentRoot||h.document||n?.ownerDocument||globalThis.document;if(ge(n,"root"),ge(r,"trigger"),ge(i,"panel"),ge(T,"documentRoot"),typeof n.contains!="function"||typeof i.contains!="function")throw new TypeError("root and panel must support contains()");if(typeof i.querySelectorAll!="function")throw new TypeError("panel must support querySelectorAll()");if(typeof o!="string"||!o.trim())throw new TypeError("itemSelector must be a non-empty string");let d=be.get(n);d&&d.destroy();let l=r.getAttribute?.("aria-expanded")==="true"||!i.hidden&&n.classList?.contains?.("is-open"),p=!1,c;function y(){return Array.from(i.querySelectorAll(o)).filter(b=>!dt(b))}function x(){return p?!1:(n.classList?.toggle?.("is-open",l),i.hidden=!l,r.setAttribute?.("aria-expanded",l?"true":"false"),i.setAttribute?.("aria-hidden",l?"false":"true"),l)}function m(b){let f=y();if(!f.length)return null;let F=b==="last"?f[f.length-1]:f[0];return xe(F),F}function g(b={}){if(p)return!1;if(l)x();else{if(typeof s=="function"&&s(c)===!1)return!1;l=!0,x()}let f=b===!0||b.focusFirst===!0?"first":b.focusLast===!0?"last":b.focus;return(f==="first"||f==="last")&&m(f),!0}function _(b={}){if(p)return!1;let f=l;return l=!1,x(),b.restoreFocus===!0&&xe(r),f}function L(b={}){return p?!1:l?(_({restoreFocus:b.restoreFocus===!0}),!1):(g(b),l)}function D(b){let f=y();if(!f.length)return!1;let F=h.getActiveElement?.()||T.activeElement,k=f.indexOf(F),E;if(b.key==="ArrowDown")E=k<0?0:(k+1)%f.length;else if(b.key==="ArrowUp")E=k<0?f.length-1:(k-1+f.length)%f.length;else if(b.key==="Home")E=0;else if(b.key==="End")E=f.length-1;else return!1;return b.preventDefault?.(),xe(f[E]),!0}function j(){L()}function I(b){b.key!=="ArrowDown"&&b.key!=="ArrowUp"||(b.preventDefault?.(),g({focus:b.key==="ArrowUp"?"last":"first"}))}function M(b){if(!l)return;let f=mn(b.target,o,i);!f||dt(f)||(_({restoreFocus:!0}),typeof u=="function"&&u(f,b,c))}function A(b){if(l){if(b.key==="Tab"){_();return}if(b.key==="Escape"){b.preventDefault?.(),b.stopPropagation?.(),_({restoreFocus:!0});return}D(b)}}function w(b){if(!l)return;let f=b.target;n.contains(f)||r.contains?.(f)||i.contains(f)||_()}function S(b){!l||b.key!=="Escape"||(b.preventDefault?.(),_({restoreFocus:!0}))}function C(){p||(l&&(l=!1,x()),r.removeEventListener("click",j),r.removeEventListener("keydown",I),i.removeEventListener("click",M),i.removeEventListener("keydown",A),T.removeEventListener("pointerdown",w),T.removeEventListener("keydown",S),p=!0,be.get(n)===c&&be.delete(n))}return c=Object.freeze({open:g,close:_,toggle:L,sync:x,destroy:C,get isOpen(){return!p&&l},get destroyed(){return p}}),r.setAttribute?.("aria-haspopup","menu"),i.id&&r.setAttribute?.("aria-controls",i.id),i.getAttribute?.("role")||i.setAttribute?.("role","menu"),x(),r.addEventListener("click",j),r.addEventListener("keydown",I),i.addEventListener("click",M),i.addEventListener("keydown",A),T.addEventListener("pointerdown",w),T.addEventListener("keydown",S),be.set(n,c),c}function pt(e){return typeof e=="object"&&e!==null||typeof e=="function"}var bn=Object.freeze({create:ie,createController:ie,mount:ie});function mt(e=globalThis,t={}){if(!pt(e))throw new TypeError("A facade target is required");let n=t.createMenuController||ie;if(typeof n!="function")throw new TypeError("A menu controller factory is required");let r=n===ie?bn:Object.freeze({create:n,createController:n,mount:n}),i=pt(e.CarrotUI)?e.CarrotUI:null,o=i&&Object.isExtensible(i)?i:Object.assign({},i||{});return o.menu=r,e.CarrotUI=o,t.compatibility!==!1&&(e.CarrotDropdownMenu=r),r}function gn(e){if(!e||typeof e.addEventListener!="function")throw new TypeError("A commit gesture target element is required");return e}function ye(e,t={}){let n=gn(e),r=t.onCommit;if(typeof r!="function")throw new TypeError("onCommit must be a function");let i=typeof t.onHoldStep=="function"?t.onHoldStep:r,o=Number.isFinite(t.holdDelayMs)?t.holdDelayMs:0,s=Number.isFinite(t.holdIntervalMs)?t.holdIntervalMs:160,u=Number.isFinite(t.moveTolerance)?t.moveTolerance:10,h=Number.isFinite(t.clickSuppressMs)?t.clickSuppressMs:700,T=typeof t.pressedClass=="string"?t.pressedClass:"is-holding",d=typeof t.now=="function"?t.now:()=>Date.now(),l=t.scheduler||globalThis,p=l.setTimeout.bind(l),c=l.clearTimeout.bind(l),y=null,x=null,m=null,g=0;function _(){x!==null&&(c(x),x=null),m!==null&&(c(m),m=null)}function L(){if(!(!y||y.pointerId===void 0||y.pointerId===null)&&typeof n.releasePointerCapture=="function")try{n.releasePointerCapture(y.pointerId)}catch{}}function D(){_(),L(),n.classList?.remove?.(T),y=null}function j(){y&&(y.holdFired=!0,i(),m=p(j,s))}function I(){x=null,y&&j()}function M(f){if(!(f.button!==void 0&&f.button!==0)){if(D(),y={pointerId:f.pointerId,startX:f.clientX,startY:f.clientY,holdFired:!1,cancelled:!1},n.classList?.add?.(T),typeof n.setPointerCapture=="function"&&f.pointerId!==void 0)try{n.setPointerCapture(f.pointerId)}catch{}o>0&&(x=p(I,o))}}function A(f){if(!y||f.pointerId!==void 0&&f.pointerId!==y.pointerId)return;let F=Math.abs(f.clientX-y.startX),k=Math.abs(f.clientY-y.startY);F<=u&&k<=u||(y.cancelled=!0,g=d()+h,D())}function w(f){if(!y||f.pointerId!==void 0&&f.pointerId!==y.pointerId)return;let F=!y.cancelled&&!y.holdFired;g=d()+h,D(),F&&r()}function S(){y&&(y.cancelled=!0,g=d()+h,D())}function C(f){if(d()n.addEventListener(f,F)),Object.freeze({destroy(){D(),g=0,b.forEach(([f,F])=>n.removeEventListener?.(f,F))},get pressed(){return y!==null}})}function An(e,t,n){let r=e;return Number.isFinite(t)&&(r=Math.max(r,t)),Number.isFinite(n)&&(r=Math.min(r,n)),r}function Oe(e={}){let t=Number(e.step),n=Number(e.sign),r=Number(e.min),i=Number(e.max),o=Number(e.current);if(Number.isFinite(o)||(o=Number(e.fallback)),Number.isFinite(o)||(o=Number.isFinite(r)?r:0),!Number.isFinite(t)||!Number.isFinite(n))return o;let s=An(o+n*t,r,i);return Number.isInteger(r)&&Number.isInteger(i)&&Number.isInteger(t)&&(s=Math.round(s)),s}function bt(e={}){let t=e.commit;if(typeof t!="function")throw new TypeError("commit must be a function");let n=typeof e.getValue=="function"?e.getValue:()=>NaN,r=typeof e.getStep=="function"?e.getStep:()=>1,i=typeof e.getRange=="function"?e.getRange:()=>({}),o=typeof e.getFallback=="function"?e.getFallback:()=>NaN,s=Number.isFinite(e.holdDelayMs)?e.holdDelayMs:900,u=Number.isFinite(e.holdIntervalMs)?e.holdIntervalMs:160,h=!1;async function T(p){if(!h){h=!0;try{let c=i()||{},y=Oe({current:n(),fallback:o(),sign:p,step:r(),min:c.min,max:c.max});await t(y)}finally{h=!1}}}let d=[];function l(p,c){p&&d.push(ye(p,{onCommit:()=>{T(c)},holdDelayMs:s,holdIntervalMs:u,pressedClass:e.pressedClass,now:e.now,scheduler:e.scheduler}))}return l(e.minusButton,-1),l(e.plusButton,1),Object.freeze({applyDelta:T,destroy(){d.forEach(p=>p.destroy()),d.length=0}})}function gt(e){return typeof e=="object"&&e!==null||typeof e=="function"}function yt(e=globalThis,t={}){if(!gt(e))throw new TypeError("A facade target is required");let n=t.createNumericStepper||bt,r=t.createCommitGesture||ye;if(typeof n!="function")throw new TypeError("A numeric stepper factory is required");if(typeof r!="function")throw new TypeError("A commit gesture factory is required");let i=Object.freeze({create:n,createGesture:r,nextStepValue:t.nextStepValue||Oe}),o=gt(e.CarrotUI)?e.CarrotUI:null,s=o&&Object.isExtensible(o)?o:Object.assign({},o||{});return s.numericStepper=i,e.CarrotUI=s,e.CarrotNumericStepper=i,i}var he=new WeakMap;function Ae(e,t){if(!e||typeof e.addEventListener!="function"||typeof e.removeEventListener!="function")throw new TypeError(`${t} must be an event target`);return e}function oe(e={},t={}){let{root:n,trigger:r,panel:i,beforeOpen:o,onOpenChange:s}=e,u=e.environment||t,h=e.documentRoot||u.document||n?.ownerDocument||globalThis.document;if(Ae(n,"root"),Ae(r,"trigger"),Ae(i,"panel"),Ae(h,"documentRoot"),typeof n.contains!="function"||typeof i.contains!="function")throw new TypeError("root and panel must support contains()");let T=he.get(n);T&&T.destroy();let d=r.getAttribute?.("aria-expanded")==="true"&&i.hidden!==!0,l=!1,p;function c(){return l?!1:(n.classList?.toggle?.("is-open",d),i.hidden=!d,r.setAttribute?.("aria-expanded",d?"true":"false"),i.setAttribute?.("aria-hidden",d?"false":"true"),typeof s=="function"&&s(d,p),d)}function y(){return l||!d&&typeof o=="function"&&o(p)===!1?!1:(d=!0,c())}function x(I={}){if(l)return!1;let M=d;return d=!1,c(),I.restoreFocus===!0&&r.focus?.(),M}function m(){return d?(x(),!1):y()}function g(){m()}function _(I){I.key!=="Escape"||!d||(I.preventDefault?.(),x({restoreFocus:!0}))}function L(I){!d||n.contains(I.target)||i.contains(I.target)||x()}function D(I){!d||I.key!=="Escape"||(I.preventDefault?.(),x({restoreFocus:!0}))}function j(){l||(d&&(d=!1,c()),r.removeEventListener("click",g),r.removeEventListener("keydown",_),h.removeEventListener("pointerdown",L),h.removeEventListener("keydown",D),l=!0,he.get(n)===p&&he.delete(n))}return p=Object.freeze({open:y,close:x,toggle:m,sync:c,destroy:j,get isOpen(){return!l&&d}}),i.id&&(r.setAttribute?.("aria-controls",i.id),r.setAttribute?.("aria-describedby",i.id)),c(),r.addEventListener("click",g),r.addEventListener("keydown",_),h.addEventListener("pointerdown",L),h.addEventListener("keydown",D),he.set(n,p),p}function ht(e){return typeof e=="object"&&e!==null||typeof e=="function"}var Cn=Object.freeze({create:oe,createController:oe,mount:oe});function At(e=globalThis,t={}){if(!ht(e))throw new TypeError("A facade target is required");let n=t.createPopoverController||oe;if(typeof n!="function")throw new TypeError("A popover controller factory is required");let r=n===oe?Cn:Object.freeze({create:n,createController:n,mount:n}),i=ht(e.CarrotUI)?e.CarrotUI:null,o=i&&Object.isExtensible(i)?i:Object.assign({},i||{});return o.popover=r,e.CarrotUI=o,r}var Ce=new WeakMap;function le(e,t){return typeof e?.getAttribute=="function"?e.getAttribute(t):null}function Te(e){return e?.disabled===!0||typeof e?.hasAttribute=="function"&&e.hasAttribute("disabled")?!0:String(le(e,"aria-disabled")||"").toLowerCase()==="true"}function Tn(e,t){let n=le(e,t);return n!==null&&String(n).toLowerCase()!=="false"}function Sn(e){if(e?.dataset&&Object.prototype.hasOwnProperty.call(e.dataset,"value"))return String(e.dataset.value);let t=le(e,"data-value");if(t!==null)return t;let n=le(e,"value");return n!==null?n:e?.value!==void 0&&e.value!==null?String(e.value):null}function vn(e,t){return e===t?!0:typeof e?.contains=="function"&&e.contains(t)}function En(e){let t=String(e?.itemSelector||"").trim();if(!t)throw new TypeError("A segmented control itemSelector is required");return Object.freeze({itemSelector:t,selectedAttribute:String(e?.selectedAttribute||"aria-selected"),orientation:e?.orientation==="vertical"?"vertical":"horizontal",activation:e?.activation==="manual"?"manual":"automatic",onActivate:typeof e?.onActivate=="function"?e.onActivate:null})}function wn(e){if(!e||typeof e!="object"||typeof e.querySelectorAll!="function"||typeof e.addEventListener!="function"||typeof e.removeEventListener!="function")throw new TypeError("A segmented control root element is required")}function Ct(e,t={}){wn(e);let n=Ce.get(e);if(n)return n.sync(),n;let r=En(t),i=new Map,o=[],s=null,u=!1,h=null;function T(){return Array.from(e.querySelectorAll(r.itemSelector))}function d(A){i.has(A)||i.set(A,le(A,"tabindex"))}function l(A,w){d(A),A.setAttribute("tabindex",String(w))}function p(A=o){return A.find(w=>Tn(w,r.selectedAttribute))||null}function c({preferSelected:A=!0}={}){o=T();let w=o.filter(b=>!Te(b)),S=p(o),C=S&&w.includes(S)?S:null;A&&C?s=C:w.includes(s)||(s=C||w[0]||null);for(let b of o)l(b,b===s?0:-1);return o}function y(){let A=T(),w=p(A),S=w?A.indexOf(w):-1;return Object.freeze({itemCount:A.length,selectedIndex:S,selectedValue:w?Sn(w):null,destroyed:u})}function x(){return u||c({preferSelected:!0}),y()}function m(A){return o.find(w=>vn(w,A?.target))||null}function g(A){s=A;for(let w of o)l(w,w===A?0:-1)}function _(A,w,S){if(!r.onActivate)return;let C=o.indexOf(A);try{r.onActivate(A,C,Object.freeze({source:S,event:w,controller:h}))}finally{u||c({preferSelected:!1})}}function L(A,w){A&&(w?.preventDefault?.(),g(A),typeof A.focus=="function"&&A.focus(),r.activation==="automatic"&&_(A,w,"keyboard"))}function D(A,w){c({preferSelected:!1});let S=o.filter(k=>!Te(k));if(!S.length)return null;let C=m(A),b=S.includes(C)?C:S.includes(s)?s:S[0],f=S.indexOf(b);return w==="first"?S[0]:w==="last"?S[S.length-1]:S[(f+(w==="previous"?-1:1)+S.length)%S.length]}function j(A){if(u||A?.isComposing||A?.altKey||A?.ctrlKey||A?.metaKey)return;let w=null;if(A.key==="Home"?w="first":A.key==="End"?w="last":r.orientation==="horizontal"&&A.key==="ArrowLeft"?w="previous":r.orientation==="horizontal"&&A.key==="ArrowRight"?w="next":r.orientation==="vertical"&&A.key==="ArrowUp"?w="previous":r.orientation==="vertical"&&A.key==="ArrowDown"&&(w="next"),w){L(D(A,w),A);return}if(r.activation!=="manual"||A.key!=="Enter"&&A.key!==" "&&A.key!=="Spacebar")return;c({preferSelected:!1});let S=m(A);!S||Te(S)||(A.preventDefault?.(),g(S),_(S,A,"keyboard"))}function I(A){if(u)return;c({preferSelected:!1});let w=m(A);w&&!Te(w)&&g(w)}function M(){if(u)return!1;u=!0,e.removeEventListener("keydown",j),e.removeEventListener("focusin",I);for(let[A,w]of i)w===null?A.removeAttribute("tabindex"):A.setAttribute("tabindex",w);return i.clear(),o=[],s=null,Ce.get(e)===h&&Ce.delete(e),!0}return h=Object.freeze({sync:x,destroy:M,snapshot:y}),Ce.set(e,h),e.addEventListener("keydown",j),e.addEventListener("focusin",I),x(),h}function Tt(e){return typeof e=="object"&&e!==null||typeof e=="function"}function St(e=globalThis,t={}){if(!Tt(e))throw new TypeError("A facade target is required");let n=t.createSegmentedControl||Ct;if(typeof n!="function")throw new TypeError("A segmented control factory is required");let r=Object.freeze({create:n}),i=Tt(e.CarrotUI)?e.CarrotUI:null,o=i&&Object.isExtensible(i)?i:Object.assign({},i||{});return o.segmentedControl=r,e.CarrotUI=o,e.CarrotSegmentedControl=r,r}var Ie=Object.freeze(["toggle","segmented","select","stepper","slider"]);function Q(e,t,n){let r=e.createElement(t);return n&&(r.className=n),r}function J(e,t,{text:n,label:r}={}){let i=Q(e,"button",t);return i.type="button",n!==void 0&&(i.textContent=n),r&&i.setAttribute("aria-label",r),i}function Ln(e,t,n,r){let i=Q(e,"label","c-switch"),o=Q(e,"input","c-switch__input");o.type="checkbox",o.setAttribute("aria-label",r.label),i.appendChild(o),i.appendChild(Q(e,"span","c-switch__track")),t.appendChild(i),n.toggleInput=o}function _n(e,t,n,r){let i=Q(e,"div","setting-segments");r.optionValues.forEach(o=>{let s=J(e,"setting-segment",{text:r.optionLabel(o)});s.dataset.value=String(o),s.setAttribute("aria-pressed","false"),n.segmentButtons.push(s),i.appendChild(s)}),t.appendChild(i),n.segmentGroup=i}function xn(e,t,n,r){let i=J(e,"setting-select setting-select--button",{label:r.label});i.setAttribute("aria-haspopup","dialog"),t.appendChild(i),n.selectInput=i}function On(e,t,n,r){n.minusButton=J(e,"smallBtn setting-value-arrow setting-value-arrow--prev",{text:"-",label:r.previousLabel}),n.plusButton=J(e,"smallBtn setting-value-arrow setting-value-arrow--next",{text:"+",label:r.nextLabel}),n.unitButton=J(e,"setting-unit-cycle"),t.appendChild(n.minusButton),t.appendChild(n.value),t.appendChild(n.plusButton)}function In(e,t,n,r){let i=Q(e,"div","setting-slider"),o=Q(e,"input","setting-slider__input");o.type="range",o.min=String(r.min),o.max=String(r.max),o.step=String(r.step),o.setAttribute("aria-label",r.label),i.appendChild(o),n.sliderInput=o,n.minusButton=J(e,"smallBtn setting-step setting-step--minus",{text:"-"}),n.plusButton=J(e,"smallBtn setting-step setting-step--plus",{text:"+"}),n.unitButton=J(e,"setting-unit-cycle"),t.appendChild(i),t.appendChild(n.minusButton),t.appendChild(n.value),t.appendChild(n.plusButton)}function vt(e={}){let t=e.document;if(!t||typeof t.createElement!="function")throw new TypeError("A document is required to build a setting control");let n=String(e.kind||"");if(!Ie.includes(n))throw new TypeError(`Unknown setting control kind: ${n}`);let r=n==="stepper",i=Q(t,"div",`ctrl ctrl--${r?"value":n}`),o=J(t,r?"value-surface val setting-value-compact":"value-surface val",{label:e.valueLabel}),s={ctrl:i,value:o,toggleInput:null,segmentGroup:null,segmentButtons:[],selectInput:null,sliderInput:null,minusButton:null,plusButton:null,unitButton:null},u={label:e.label||"",valueLabel:e.valueLabel||"",previousLabel:e.previousLabel||"",nextLabel:e.nextLabel||"",optionValues:Array.isArray(e.optionValues)?e.optionValues:[],optionLabel:typeof e.optionLabel=="function"?e.optionLabel:h=>String(h),min:e.min,max:e.max,step:e.step};return n==="toggle"?Ln(t,i,s,u):n==="segmented"?_n(t,i,s,u):n==="select"?xn(t,i,s,u):r?On(t,i,s,u):In(t,i,s,u),!r&&n!=="slider"&&i.appendChild(o),s}function Et(e){return typeof e=="object"&&e!==null||typeof e=="function"}function wt(e=globalThis,t={}){if(!Et(e))throw new TypeError("A facade target is required");let n=t.createSettingControl||vt;if(typeof n!="function")throw new TypeError("A setting control factory is required");let r=Object.freeze({createControl:n,kinds:Ie}),i=Et(e.CarrotUI)?e.CarrotUI:null,o=i&&Object.isExtensible(i)?i:Object.assign({},i||{});return o.settingRow=r,e.CarrotUI=o,r}var Nn=new Set(["primary","secondary","danger"]);function $(e){return String(e??"").trim()}function Dn(e){if(typeof e!="function")return $(e);try{return $(e())}catch{return""}}function Fn(e,t){let n=t?.document??e.environment?.document??e.document??globalThis.document;if(!n||typeof n.createElement!="function")throw new TypeError("createActionGroup requires a document environment");return n}function jn(e){let t=$(e);return Nn.has(t)?t:"secondary"}function Ne(e){if(!Array.isArray(e))return Object.freeze([]);if(e.length>2)throw new RangeError("State surface action groups support at most 2 actions");let t=new Set,n=[];for(let r of e){let i=r&&typeof r=="object"?r:{},o=$(i.id),s=Dn(i.label),u=i.onActivate??i.invoke;!o||t.has(o)||!s||typeof u!="function"||(t.add(o),n.push(Object.freeze({id:o,label:s,tone:jn(i.tone??i.kind),disabled:!!i.disabled,onActivate:u})))}return Object.freeze(n)}function Lt(e){return Array.from(new Set(e.flatMap(t=>$(t).split(/\s+/)).filter(Boolean))).join(" ")}function _t(e={},t={}){let n=e.host;if(!n||typeof n.append!="function")return null;let r=Fn(e,t),i=r.createElement("div");i.setAttribute("role","group"),i.setAttribute("aria-busy","false"),i.dataset.layout="single",i.dataset.sizing="full",i.dataset.pending="false",i.hidden=!0,n.append(i);let o=new Map,s=Object.freeze([]),u=new Map,h=null,T=0,d=!1,l=$(e.ariaLabel)||"Status actions",p=$(e.className),c=$(e.actionClassName),y=$(e.actionToneClassPrefix),x=typeof e.onPendingChange=="function"?e.onPendingChange:null;function m(){return h?.id||""}function g(S){return Lt(["btn",S.tone==="primary"?"btn--filled":"",S.tone==="danger"?"btn--danger":"","c-action-group__action",`c-action-group__action--${S.tone}`,c,y?`${y}${S.tone}`:""])}function _(){let S=s.length<=1;return Lt(["c-action-group","c-action-group--nowrap",S?"c-action-group--single":"c-action-group--split",S?"c-action-group--full":"c-action-group--equal",p])}function L(){if(d)return;let S=!!h,C=s.length<=1;i.className=_(),i.hidden=s.length===0,i.dataset.layout=C?"single":"split",i.dataset.sizing=C?"full":"equal",i.dataset.pending=String(S),i.setAttribute("aria-label",l),i.setAttribute("aria-busy",String(S));for(let b of s){let f=o.get(b.id).button,F=S||b.disabled;f.className=g(b),f.dataset.actionId=b.id,f.dataset.tone=b.tone,f.textContent=b.label,f.disabled=F,f.tabIndex=F?-1:0,f.setAttribute("aria-disabled",String(F)),f.setAttribute("tabindex",String(f.tabIndex)),F?f.setAttribute("disabled",""):f.removeAttribute("disabled")}x?.(m())}async function D(S){let C=u.get($(S));if(d||!C||C.disabled||h)return!1;let b=Object.freeze({id:C.id,generation:++T});h=b,L();try{await C.onActivate()}catch{}finally{if(d||h!==b)return!0;h=null,L()}return!0}function j(S){let C=o.get(S);C&&(C.button.removeEventListener("click",C.listener),C.button.remove(),o.delete(S))}function I(S,C={}){if(d)return!1;let b=Ne(S),f=new Set(b.map(E=>E.id));h&&!f.has(h.id)&&(h=null,T+=1);for(let E of o.keys())f.has(E)||j(E);for(let E of b){if(o.has(E.id))continue;let P=r.createElement("button");P.type="button";let G=()=>{D(E.id)};P.addEventListener("click",G),o.set(E.id,{button:P,listener:G})}s=b,u=new Map(s.map(E=>[E.id,E])),C.ariaLabel!=null&&(l=$(C.ariaLabel)||"Status actions");let F=Array.from(i.children).map(E=>E.dataset.actionId),k=s.map(E=>E.id);if(F.join("\0")!==k.join("\0"))for(let E of s)i.append(o.get(E.id).button);return L(),!0}function M(S){return d?!1:(l=$(S)||"Status actions",L(),!0)}function A(){let S=s.map(C=>Object.freeze({id:C.id,label:C.label,tone:C.tone,disabled:C.disabled}));return Object.freeze({actions:Object.freeze(S),pendingActionId:m(),layout:s.length<=1?"single":"split",sizing:s.length<=1?"full":"equal"})}function w(){if(d)return!1;d=!0,h=null,T+=1;for(let S of Array.from(o.keys()))j(S);return s=Object.freeze([]),u=new Map,i.remove(),!0}return Object.freeze({element:i,setActions:I,setAriaLabel:M,activate:D,snapshot:A,destroy:w})}var B=Object.freeze({EMPTY:"empty",LOADING:"loading",DEGRADED:"degraded",ERROR:"error",OWNERSHIP:"ownership"}),V=Object.freeze({NEUTRAL:"neutral",INFO:"info",WARNING:"warning",ERROR:"error"}),ue=Object.freeze({PRIMARY:"primary",SECONDARY:"secondary",DANGER:"danger"}),Se=Object.freeze({COVER:"cover",COMPACT:"compact"}),Mn=new Set(Object.values(B)),kn=new Set(Object.values(V)),Pn=new Set(Object.values(Se)),Rn=Object.freeze({[B.EMPTY]:V.NEUTRAL,[B.LOADING]:V.INFO,[B.DEGRADED]:V.WARNING,[B.ERROR]:V.ERROR,[B.OWNERSHIP]:V.WARNING}),De=new WeakMap,Me=new WeakSet,zn=0;function te(e){return String(e??"").trim()}function Fe(e){if(typeof e!="function")return te(e);try{return te(e())}catch{return""}}function je(e){return Array.from(new Set(e.flatMap(t=>te(t).split(/\s+/)).filter(Boolean))).join(" ")}function xt(e){let t=te(e);return Mn.has(t)?t:B.EMPTY}function Un(e,t){let n=te(e);return kn.has(n)?n:Rn[t]}function Ot(e){let t=te(e);return Pn.has(t)?t:Se.COVER}function It(e){return e&&typeof e=="object"?e.featureLabel??e.label??e.name:e}function Bn(e,t){let n=t?.document??e.environment?.document??e.document??globalThis.document;if(!n||typeof n.createElement!="function")throw new TypeError("createStateSurface requires a document environment");return n}function qn(e,t){if(typeof e=="string")return{state:xt(e),descriptor:t&&typeof t=="object"?t:{}};let n=e&&typeof e=="object"?e:{};return{state:xt(n.state),descriptor:n}}function Nt(e={},t={}){let n=e.host;if(!n||typeof n.append!="function")return null;let r=De.get(n);if(r){let E=e.featureLabel??e.feature;return E!=null&&r.setFeature(E),r}let i=Bn(e,t),o=`c-state-surface-${++zn}`,s=te(e.className),u=Ot(e.variant),h=It(e.featureLabel??e.feature),T=null,d=null,l=B.EMPTY,p=V.NEUTRAL,c=u,y="",x=!1,m=i.createElement("section");m.className=je(["c-state-surface",`c-state-surface--${p}`,`c-state-surface--state-${l}`,`c-state-surface--${c}`,s]),m.hidden=!0,m.setAttribute("aria-hidden","true"),m.setAttribute("aria-busy","false"),m.dataset.state=l,m.dataset.tone=p,m.dataset.variant=c,m.dataset.pending="false";let g=i.createElement("div");g.className="c-state-surface__body";let _=i.createElement("div");_.className="c-state-surface__copy",_.setAttribute("role","status"),_.setAttribute("aria-live","polite"),_.setAttribute("aria-atomic","true");let L=i.createElement("h2");L.id=`${o}-feature`,L.className="c-state-surface__feature";let D=i.createElement("p");D.id=`${o}-title`,D.className="c-state-surface__title";let j=i.createElement("p");j.id=`${o}-description`,j.className="c-state-surface__description",_.append(L,D,j),g.append(_),m.append(g),n.append(m);function I(){let E=l===B.LOADING||!!y;m.dataset.pending=String(!!y),m.setAttribute("aria-busy",String(E))}let M=_t({host:g,className:"c-state-surface__actions",actionClassName:"c-state-surface__action",actionToneClassPrefix:"c-state-surface__action--",onPendingChange(E){y=E,I()}},{document:i});function A(E,P){let G=P.featureLabel!=null?P.featureLabel:h;return Object.freeze({state:E,featureLabel:Fe(G),title:Fe(P.title),description:Fe(P.description),tone:Un(P.tone,E),variant:Ot(P.variant??u),actions:Ne(P.actions)})}function w(E){l=E.state,p=E.tone,c=E.variant,m.className=je(["c-state-surface",`c-state-surface--${p}`,`c-state-surface--state-${l}`,`c-state-surface--${c}`,s]),m.dataset.state=l,m.dataset.tone=p,m.dataset.variant=c,L.textContent=E.featureLabel,L.hidden=!E.featureLabel,D.textContent=E.title,D.hidden=!E.title,j.textContent=E.description,j.hidden=!E.description,E.title||E.featureLabel?(m.setAttribute("aria-labelledby",E.title?D.id:L.id),m.removeAttribute("aria-label")):(m.removeAttribute("aria-labelledby"),m.setAttribute("aria-label","Status")),E.description?m.setAttribute("aria-describedby",j.id):m.removeAttribute("aria-describedby")}function S(E={},P){if(x)return!1;let{state:G,descriptor:X}=qn(E,P),K=A(G,X);return T={...X},d=K,w(K),M.setActions(K.actions,{ariaLabel:`${K.featureLabel||K.title||"Status"} actions`}),m.hidden=!1,m.setAttribute("aria-hidden","false"),I(),!0}function C(){return x||m.hidden?!1:(T=null,d=null,l=B.EMPTY,p=V.NEUTRAL,c=u,M.setActions([]),y="",m.hidden=!0,m.setAttribute("aria-hidden","true"),m.dataset.state=l,m.dataset.tone=p,m.dataset.variant=c,m.className=je(["c-state-surface",`c-state-surface--${p}`,`c-state-surface--state-${l}`,`c-state-surface--${c}`,s]),I(),!0)}function b(E){return x?!1:(h=It(E),m.hidden||!T?!0:S(l,T))}function f(){let E=M.snapshot();return Object.freeze({visible:!x&&!m.hidden,state:d?.state||B.EMPTY,featureLabel:d?.featureLabel||"",title:d?.title||"",description:d?.description||"",tone:d?.tone||V.NEUTRAL,variant:d?.variant||u,actions:E.actions,pendingActionId:E.pendingActionId})}function F(){return x?!1:(x=!0,T=null,d=null,y="",M.destroy(),De.delete(n),Me.delete(k),m.remove(),!0)}let k=Object.freeze({show:S,hide:C,setFeature:b,snapshot:f,destroy:F});return De.set(n,k),Me.add(k),k}function Dt(e){return!!(e&&typeof e=="object"&&Me.has(e))}var Ft=new WeakMap,$n=new Set(Object.values(ue));function ke(e){return String(e??"").trim()}function Vn(e){if(typeof e!="function")return ke(e);try{return ke(e())}catch{return""}}function jt(e){if(typeof e=="string"||typeof e=="function")return Object.freeze({id:"",label:e});let t=e&&typeof e=="object"?e:{};return Object.freeze({id:t.id,label:t.label??t.name})}function Hn(e){let t=ke(e);return $n.has(t)?t:ue.SECONDARY}function Gn(e){return Array.isArray(e)?e.map(t=>{let n=t&&typeof t=="object"?t:{};return{id:n.id,label:n.label,tone:Hn(n.kind??n.tone),disabled:!!n.disabled,onActivate:n.invoke??n.onActivate}}):[]}function Kn(e){let t=new WeakMap,n=new WeakSet;function r(o={}){let s=o.host;if(!s||typeof s.append!="function")return null;let u=t.get(s);if(u)return o.feature!=null&&u.setFeature(o.feature),u;let h=jt(o.feature),T=e.create({host:s,className:o.className,featureLabel:h.label});if(!T)return null;let d=!1;function l(g={}){if(d)return!1;let _=g&&typeof g=="object"?g:{};return T.show(B.OWNERSHIP,{title:_.title,description:_.description,tone:V.NEUTRAL,actions:Gn(_.actions)})}function p(){return d?!1:T.hide()}function c(g){return d?!1:(h=jt(g),T.setFeature(h.label))}function y(){let g=T.snapshot(),_=g.actions.map(L=>Object.freeze({id:L.id,label:L.label,kind:L.tone,disabled:L.disabled}));return Object.freeze({visible:g.visible,featureId:g.visible?Vn(h.id):"",featureLabel:g.featureLabel,title:g.title,description:g.description,actions:Object.freeze(_),pendingActionId:g.pendingActionId})}function x(){return d?!1:(d=!0,t.delete(s),n.delete(m),T.destroy(),!0)}let m=Object.freeze({show:l,hide:p,setFeature:c,snapshot:y,destroy:x});return t.set(s,m),n.add(m),m}function i(o){return!!(o&&typeof o=="object"&&n.has(o))}return Object.freeze({ACTION_KIND:ue,create:r,isNotice:i})}function Yn(e={}){return Object.freeze({STATE:B,TONE:V,VARIANT:Se,ACTION_KIND:ue,create(t={}){return Nt(t,e)},isSurface:Dt})}function Wn(e,t){let n=e.CarrotUI;if(n&&typeof n=="object"&&Object.isExtensible(n))return n.stateSurface=t,n;let r=n&&typeof n=="object"?{...n}:{};return r.stateSurface=t,e.CarrotUI=r,r}function Mt(e=globalThis,t={}){if(!e||typeof e!="object"&&typeof e!="function")throw new TypeError("installStateSurfaceFacade requires an object target");let n=Ft.get(e);if(!n){let r={...t,document:t.document??e.document??globalThis.document},i=Yn(r);n=Object.freeze({stateSurface:i,streamOwnershipNotice:Kn(i)}),Ft.set(e,n)}return Wn(e,n.stateSurface),e.StreamOwnershipNotice=n.streamOwnershipNotice,n.stateSurface}var H=globalThis,fe=H.document;fe.getElementById("carrotAssetManifest")&&Ve(fe,H);Qe(H,fe);mt(H);At(H);ft(H,{documentRoot:fe});Mt(H,{document:fe});St(H);yt(H);Ge(H);wt(H);ot(H);})();
diff --git a/openpilot/selfdrive/carrot/web/js/generated/logs.js b/openpilot/selfdrive/carrot/web/js/generated/logs.js
index c7f11a4b18..b93008b99a 100644
--- a/openpilot/selfdrive/carrot/web/js/generated/logs.js
+++ b/openpilot/selfdrive/carrot/web/js/generated/logs.js
@@ -1,8 +1,8 @@
/* @generated by web/build.mjs; edit web/src/entries/logs.js instead. */
-(()=>{(()=>{let t=document.querySelector(".logs-shell"),e=document.getElementById("logsDashcamPanel"),a=document.getElementById("logsScreenPanel"),n=document.getElementById("dashcamRoutes"),s=document.getElementById("screenrecordVideos"),r=[n,s].filter(Boolean);if(!t||!r.length)return;let o=0;function i(){return e&&!e.hidden?n:a&&!a.hidden?s:r.find(d=>d.offsetWidth>0)||null}function l(){o=0;let d=i();if(!d||d.offsetWidth<=0)return;let m=window.getComputedStyle(d),g=(Number.parseFloat(m.borderLeftWidth)||0)+(Number.parseFloat(m.borderRightWidth)||0),f=Math.max(0,Math.round(d.offsetWidth-d.clientWidth-g));t.style.setProperty("--logs-scrollbar-gutter",`${f}px`)}function u(){o||(o=window.requestAnimationFrame(l))}if(typeof ResizeObserver=="function"){let d=new ResizeObserver(u);d.observe(t),r.forEach(m=>d.observe(m))}if(typeof MutationObserver=="function"){let d=new MutationObserver(u);e&&d.observe(e,{attributes:!0,attributeFilter:["hidden"]}),a&&d.observe(a,{attributes:!0,attributeFilter:["hidden"]})}window.addEventListener("resize",u,{passive:!0}),window.addEventListener("carrot:pagechange",u),u()})();var Kt=Object.freeze({READING:"reading",RECENT:"recent"});function P(t){return String(t||"").trim()}function Yt(t){let e=new Set,a=[];for(let n of Array.isArray(t)?t:[]){let s=P(n);!s||e.has(s)||(e.add(s),a.push(s))}return a}function on(t,e){let a=t&&typeof t=="object"?t:{},n=Number(a.segmentCount),s=Number.isFinite(n)?Math.max(0,Math.trunc(n)):e;return Object.freeze({route:String(a.route||""),dateLabel:String(a.dateLabel||""),timeRange:String(a.timeRange||""),segmentCount:s})}function Xt(t,e){let a=P(e);return a?a===t?.activeSegment?Kt.READING:a===t?.previousSegment?Kt.RECENT:"":""}function cn(t={}){let e=P(t.activeSegment),a=P(t.previousSegment),n=new Set;a===e&&(a="");let s=()=>Object.freeze({activeSegment:e,previousSegment:a}),r=g=>Object.freeze({changed:g,state:s()}),o=()=>{let g=s();n.forEach(f=>f(g))};return Object.freeze({finish:()=>e?(a=e,e="",o(),r(!0)):r(!1),restoreRecent:g=>{let f=P(g);return!f||f===e||f===a?r(!1):(a=f,o(),r(!0))},select:g=>{let f=P(g);return!f||f===e?r(!1):(e?a=e:a===f&&(a=""),e=f,o(),r(!0))},snapshot:s,statusFor:g=>Xt({activeSegment:e,previousSegment:a},g),subscribe:g=>typeof g!="function"?()=>{}:(n.add(g),()=>n.delete(g))})}var ee=cn();function Zt(t={}){let e=Yt(t.segments),a=on(t.group,e.length),n=e.includes(P(t.activeSegment))?P(t.activeSegment):e[0]||"",s=e.includes(P(t.previousSegment))?P(t.previousSegment):"";s===n&&(s="");let r=()=>{let m=e.indexOf(n);return Object.freeze({group:a,segments:Object.freeze([...e]),activeSegment:n,previousSegment:s,activeIndex:m,previousIndex:e.indexOf(s),canMovePrevious:m>0,canMoveNext:m>=0&&mObject.freeze({changed:m,state:r()}),i=m=>{let g=P(m);return!e.includes(g)||g===n?o(!1):(s=n,n=g,o(!0))};return Object.freeze({move:m=>{let g=Math.sign(Number(m)||0),f=e.indexOf(n);if(!g||f<0)return o(!1);let h=f+g;return h<0||h>=e.length?o(!1):i(e[h])},replaceSegments:(m,g={})=>{let f=Yt(m),h=P(g.activeSegment??n),p=P(g.previousSegment??s),x=f.includes(h)?h:f[0]||"",$=f.includes(p)&&p!==x?p:"",S=f.length!==e.length||f.some((T,k)=>T!==e[k])||x!==n||$!==s;return e=f,n=x,s=$,o(S)},select:i,snapshot:r,statusFor:m=>Xt({activeSegment:n,previousSegment:s},m)})}function I(t,e="",a=""){let n=document.createElement(t);return e&&(n.className=e),a!==""&&(n.textContent=String(a)),n}function ln(t,e={}){let a=String(t||"");for(let[n,s]of Object.entries(e))a=a.replaceAll(`{${n}}`,String(s));return a}var dn=new Set(["reading","recent"]);function un(t){if(!t||typeof t!="object")return null;let e=Number(t.segmentCount);return Object.freeze({route:String(t.route||"").trim(),title:String(t.title||t.route||"").trim(),dateLabel:String(t.dateLabel||"").trim(),timeRange:String(t.timeRange||"").trim(),segmentCount:Number.isFinite(e)?Math.max(0,Math.trunc(e)):0})}function Ge(t){let e=new Set,a=[];for(let n of Array.isArray(t)?t:[]){if(!n||typeof n!="object")continue;let s=String(n.id||"").trim();!s||e.has(s)||(e.add(s),a.push(Object.freeze({id:s,name:String(n.name||s).trim(),timeLabel:String(n.timeLabel||"").trim(),title:String(n.title||n.name||s).trim(),subtitle:String(n.subtitle||"").trim(),src:String(n.src||"").trim(),thumbnailSrc:String(n.thumbnailSrc||"").trim()})))}return Object.freeze(a)}function _t(t,e={}){let a=String(t||"").trim().toLowerCase();if(!dn.has(a))return null;let n=I("span","dashcam-player-status-tag",a==="reading"?e.reading:e.recent);return n.dataset.state=a,n}function mn(t,e={}){let a=un(t);if(!a)return null;let n=I("section","dashcam-player-group-summary"),s=I("h2","dashcam-player-section-title",e.currentGroup),r=I("div","dashcam-player-group-summary__body"),o=I("div","dashcam-player-group-summary__main"),i=I("strong","dashcam-player-group-summary__title",a.title||a.route),l=I("span","dashcam-player-group-summary__count",ln(e.segmentCount,{count:a.segmentCount})),u=I("span","dashcam-player-group-summary__time",[a.dateLabel,a.timeRange].filter(Boolean).join(" · "));return s.id="dashcamPlayerCurrentGroupTitle",n.setAttribute("aria-labelledby",s.id),o.append(i,l),r.append(o,u),n.append(s,r),Object.freeze({element:n,group:a})}function Qt(t,e={}){let a=I("div","dashcam-player-segment-browser"),n=I("h2","dashcam-player-section-title",e.labels?.segments),s=I("div","dashcam-player-segment-list"),r=Object.freeze([]),o=typeof e.statusFor=="function"?e.statusFor:null;n.id="dashcamPlayerSegmentListTitle",s.setAttribute("aria-labelledby",n.id),a.append(n,s);let i=()=>{s.querySelectorAll(".dashcam-player-segment-item").forEach(m=>{let g=m.querySelector(".dashcam-player-segment-status"),f=o?.(m.dataset.segmentId)||"",h=_t(f,e.labels);g?.replaceChildren(...h?[h]:[]),g&&(g.hidden=!h),m.setAttribute("aria-label",[m.dataset.segmentLabel,h?.textContent].filter(Boolean).join(" · "))})},l=(m,g=!1)=>{let f=String(m||""),h=null;return s.querySelectorAll(".dashcam-player-segment-item").forEach(p=>{let x=p.dataset.segmentId===f;p.classList.toggle("is-active",x),x?(p.setAttribute("aria-current","true"),h=p):p.removeAttribute("aria-current")}),i(),g&&h&&h.scrollIntoView({block:"nearest"}),h},u=m=>{o=typeof m=="function"?m:null,i()},d=(m,g="")=>{r=Ge(m);let f=document.createDocumentFragment();for(let h of r){let p=I("button","dashcam-player-segment-item"),x=I("span","dashcam-player-segment-thumb"),$=I("span","dashcam-player-segment-copy"),S=I("span","dashcam-player-segment-meta"),T=I("span","dashcam-player-segment-time",h.timeLabel),k=I("span","dashcam-player-segment-status"),w=I("span","dashcam-player-segment-name",h.name),H=[h.timeLabel,h.name].filter(Boolean).join(" · ");if(p.type="button",p.dataset.segmentId=h.id,p.dataset.segmentLabel=H,p.setAttribute("aria-label",H),h.thumbnailSrc){let O=I("img");O.src=h.thumbnailSrc,O.alt="",O.loading="lazy",O.decoding="async",x.append(O)}else x.classList.add("is-empty");k.hidden=!0,S.append(w,k),$.append(T,S),p.append(x,$),p.addEventListener("click",()=>e.onSelect?.(h.id)),f.append(p)}return s.replaceChildren(f),l(g),r};return d(t,e.activeSegmentId),Object.freeze({element:a,get segments(){return r},render:d,setActive:l,setStatusFor:u})}function ea(t={}){let e=I("div","dashcam-player-dialog");e.setAttribute("role","dialog"),e.setAttribute("aria-modal","true");let a=I("div","dashcam-player-layout"),n=I("div","dashcam-player-frame"),s=mn(t.currentGroup,t.labels),r=null,o=null;return a.append(n),s&&(e.classList.add("dashcam-player-dialog--browsable"),r=I("aside","dashcam-player-browser"),r.setAttribute("aria-label",String(t.labels?.browser||"")),o=I("section","dashcam-player-segment-region"),o.hidden=!0,r.append(s.element,o),a.append(r)),e.append(a),Object.freeze({browser:r,dialog:e,frame:n,group:s?.group||null,layout:a,segmentHost:o})}var ta=40,gn=720,aa=8,b={initialized:!1,loading:!1,loadingMore:!1,loadSeq:0,videos:[],rowHeight:80,windowStart:0,windowEnd:0,total:0,nextOffset:0,hasMore:!1,signature:"",renderFrame:0};function Ve(t,e=""){let a=document.getElementById("screenrecordStatus");a&&(a.textContent=t||"",a.hidden=!t,a.classList.toggle("is-error",e==="error"))}function Je(t,e){return`/api/screenrecord/${t}/${encodeURIComponent(e)}`}function sa(t){return(t||[]).map(e=>[e.id||"",e.name||"",e.modifiedLabel||"",e.size||0].join("|")).join(`
-`)+"|"+(typeof LANG<"u"?LANG:"")}function ra(t){return!t||!b.hasMore||b.loading||b.loadingMore?!1:t.scrollHeight-t.scrollTop-t.clientHeight<=gn}function pn(t,e){let a=Math.max(48,Number(b.rowHeight)||80),n=Math.max(1,t?.clientHeight||a*8),s=Math.max(0,t?.scrollTop||0),r=Math.ceil(n/a),o=Math.max(0,Math.floor(s/a)-aa),i=Math.min(e,o+r+aa*2);return{start:o,end:i,rowHeight:a}}function fn(t){let e=t?.querySelector?.(".screenrecord-row");if(!e)return;let a=window.getComputedStyle?.(t),n=Number.parseFloat(a?.rowGap||a?.gap||"0")||0,s=Math.max(48,e.getBoundingClientRect().height+n);Math.abs(s-b.rowHeight)<1||(b.rowHeight=s)}function na(t,e){if(t<=0)return null;let a=document.createElement("div");return a.className="screenrecord-virtual-spacer",a.dataset.spacer=e,a.style.height=`${Math.round(t)}px`,a}function hn(t,e,a){let n=String(t?.id||""),s=n?a.get(n):null;if(s)return s.style.setProperty("--i",String(e)),s.classList.remove("ui-stagger-item"),s;let r=document.createElement("template");return r.innerHTML=bn(t,e),r.content.firstElementChild}function yn(t,e,a){let n=new Map(Array.from(t.querySelectorAll(".screenrecord-row")).map(i=>[i.dataset.id||"",i]).filter(([i])=>!!i)),s=document.createDocumentFragment(),r=na(a.start*a.rowHeight,"top"),o=na((e.length-a.end)*a.rowHeight,"bottom");r&&s.appendChild(r),e.slice(a.start,a.end).forEach((i,l)=>{let u=hn(i,a.start+l,n);u&&s.appendChild(u)}),o&&s.appendChild(o),Qe(t),t.replaceChildren(s)}function ge(t){let e=document.getElementById("screenrecordVideos");e&&e.classList.toggle("is-loading-more",!!t)}function Ke(){b.renderFrame||(b.renderFrame=requestAnimationFrame(()=>{b.renderFrame=0,q({preserve:!0})}))}function We(t,e){if(!t)return;let a=b.videos||[],n=a.findIndex(o=>String(o?.id||"")===String(t)),s=n>0?a[n-1]:null,r=n>=0&&n{o?.(),We(s.id||"",s.name||"")}:null,onNext:r?({close:o}={})=>{o?.(),We(r.id||"",r.name||"")}:null})}function bn(t,e=0){let a=escapeHtml(t.id||""),n=escapeHtml(t.name||"-"),s=escapeHtml(Ye(t.modifiedEpoch)||Xe(t.modifiedLabel||t.relativeModifiedLabel)||"-"),r=escapeHtml(Ze(t.size)),o=escapeHtml((t.ext||"video").toUpperCase());return`
+(()=>{(()=>{let t=document.querySelector(".logs-shell"),e=document.getElementById("logsDashcamPanel"),a=document.getElementById("logsScreenPanel"),n=document.getElementById("dashcamRoutes"),s=document.getElementById("screenrecordVideos"),r=[n,s].filter(Boolean);if(!t||!r.length)return;let o=0;function c(){return e&&!e.hidden?n:a&&!a.hidden?s:r.find(u=>u.offsetWidth>0)||null}function l(){o=0;let u=c();if(!u||u.offsetWidth<=0)return;let g=window.getComputedStyle(u),m=(Number.parseFloat(g.borderLeftWidth)||0)+(Number.parseFloat(g.borderRightWidth)||0),f=Math.max(0,Math.round(u.offsetWidth-u.clientWidth-m));t.style.setProperty("--logs-scrollbar-gutter",`${f}px`)}function d(){o||(o=window.requestAnimationFrame(l))}if(typeof ResizeObserver=="function"){let u=new ResizeObserver(d);u.observe(t),r.forEach(g=>u.observe(g))}if(typeof MutationObserver=="function"){let u=new MutationObserver(d);e&&u.observe(e,{attributes:!0,attributeFilter:["hidden"]}),a&&u.observe(a,{attributes:!0,attributeFilter:["hidden"]})}window.addEventListener("resize",d,{passive:!0}),window.addEventListener("carrot:pagechange",d),d()})();var Zt=Object.freeze({READING:"reading",RECENT:"recent"});function P(t){return String(t||"").trim()}function Qt(t){let e=new Set,a=[];for(let n of Array.isArray(t)?t:[]){let s=P(n);!s||e.has(s)||(e.add(s),a.push(s))}return a}function hn(t,e){let a=t&&typeof t=="object"?t:{},n=Number(a.segmentCount),s=Number.isFinite(n)?Math.max(0,Math.trunc(n)):e;return Object.freeze({route:String(a.route||""),dateLabel:String(a.dateLabel||""),timeRange:String(a.timeRange||""),segmentCount:s})}function ea(t,e){let a=P(e);return a?a===t?.activeSegment?Zt.READING:a===t?.previousSegment?Zt.RECENT:"":""}function yn(t={}){let e=P(t.activeSegment),a=P(t.previousSegment),n=new Set;a===e&&(a="");let s=()=>Object.freeze({activeSegment:e,previousSegment:a}),r=m=>Object.freeze({changed:m,state:s()}),o=()=>{let m=s();n.forEach(f=>f(m))};return Object.freeze({finish:()=>e?(a=e,e="",o(),r(!0)):r(!1),restoreRecent:m=>{let f=P(m);return!f||f===e||f===a?r(!1):(a=f,o(),r(!0))},select:m=>{let f=P(m);return!f||f===e?r(!1):(e?a=e:a===f&&(a=""),e=f,o(),r(!0))},snapshot:s,statusFor:m=>ea({activeSegment:e,previousSegment:a},m),subscribe:m=>typeof m!="function"?()=>{}:(n.add(m),()=>n.delete(m))})}var ee=yn();function ta(t={}){let e=Qt(t.segments),a=hn(t.group,e.length),n=e.includes(P(t.activeSegment))?P(t.activeSegment):e[0]||"",s=e.includes(P(t.previousSegment))?P(t.previousSegment):"";s===n&&(s="");let r=()=>{let g=e.indexOf(n);return Object.freeze({group:a,segments:Object.freeze([...e]),activeSegment:n,previousSegment:s,activeIndex:g,previousIndex:e.indexOf(s),canMovePrevious:g>0,canMoveNext:g>=0&&gObject.freeze({changed:g,state:r()}),c=g=>{let m=P(g);return!e.includes(m)||m===n?o(!1):(s=n,n=m,o(!0))};return Object.freeze({move:g=>{let m=Math.sign(Number(g)||0),f=e.indexOf(n);if(!m||f<0)return o(!1);let h=f+m;return h<0||h>=e.length?o(!1):c(e[h])},replaceSegments:(g,m={})=>{let f=Qt(g),h=P(m.activeSegment??n),p=P(m.previousSegment??s),x=f.includes(h)?h:f[0]||"",$=f.includes(p)&&p!==x?p:"",b=f.length!==e.length||f.some((T,k)=>T!==e[k])||x!==n||$!==s;return e=f,n=x,s=$,o(b)},select:c,snapshot:r,statusFor:g=>ea({activeSegment:n,previousSegment:s},g)})}function I(t,e="",a=""){let n=document.createElement(t);return e&&(n.className=e),a!==""&&(n.textContent=String(a)),n}function Sn(t,e={}){let a=String(t||"");for(let[n,s]of Object.entries(e))a=a.replaceAll(`{${n}}`,String(s));return a}var bn=new Set(["reading","recent"]);function _n(t){if(!t||typeof t!="object")return null;let e=Number(t.segmentCount);return Object.freeze({route:String(t.route||"").trim(),title:String(t.title||t.route||"").trim(),dateLabel:String(t.dateLabel||"").trim(),timeRange:String(t.timeRange||"").trim(),segmentCount:Number.isFinite(e)?Math.max(0,Math.trunc(e)):0})}function Ve(t){let e=new Set,a=[];for(let n of Array.isArray(t)?t:[]){if(!n||typeof n!="object")continue;let s=String(n.id||"").trim();!s||e.has(s)||(e.add(s),a.push(Object.freeze({id:s,name:String(n.name||s).trim(),timeLabel:String(n.timeLabel||"").trim(),title:String(n.title||n.name||s).trim(),subtitle:String(n.subtitle||"").trim(),src:String(n.src||"").trim(),thumbnailSrc:String(n.thumbnailSrc||"").trim()})))}return Object.freeze(a)}function vt(t,e={}){let a=String(t||"").trim().toLowerCase();if(!bn.has(a))return null;let n=I("span","dashcam-player-status-tag",a==="reading"?e.reading:e.recent);return n.dataset.state=a,n}function vn(t,e={}){let a=_n(t);if(!a)return null;let n=I("section","dashcam-player-group-summary"),s=I("h2","dashcam-player-section-title",e.currentGroup),r=I("div","dashcam-player-group-summary__body"),o=I("div","dashcam-player-group-summary__main"),c=I("strong","dashcam-player-group-summary__title",a.title||a.route),l=I("span","dashcam-player-group-summary__count",Sn(e.segmentCount,{count:a.segmentCount})),d=I("span","dashcam-player-group-summary__time",[a.dateLabel,a.timeRange].filter(Boolean).join(" · "));return s.id="dashcamPlayerCurrentGroupTitle",n.setAttribute("aria-labelledby",s.id),o.append(c,l),r.append(o,d),n.append(s,r),Object.freeze({element:n,group:a})}function aa(t,e={}){let a=I("div","dashcam-player-segment-browser"),n=I("h2","dashcam-player-section-title",e.labels?.segments),s=I("div","dashcam-player-segment-list"),r=Object.freeze([]),o=typeof e.statusFor=="function"?e.statusFor:null;n.id="dashcamPlayerSegmentListTitle",s.setAttribute("aria-labelledby",n.id),a.append(n,s);let c=()=>{s.querySelectorAll(".dashcam-player-segment-item").forEach(g=>{let m=g.querySelector(".dashcam-player-segment-status"),f=o?.(g.dataset.segmentId)||"",h=vt(f,e.labels);m?.replaceChildren(...h?[h]:[]),m&&(m.hidden=!h),g.setAttribute("aria-label",[g.dataset.segmentLabel,h?.textContent].filter(Boolean).join(" · "))})},l=(g,m=!1)=>{let f=String(g||""),h=null;return s.querySelectorAll(".dashcam-player-segment-item").forEach(p=>{let x=p.dataset.segmentId===f;p.classList.toggle("is-active",x),x?(p.setAttribute("aria-current","true"),h=p):p.removeAttribute("aria-current")}),c(),m&&h&&h.scrollIntoView({block:"nearest"}),h},d=g=>{o=typeof g=="function"?g:null,c()},u=(g,m="")=>{r=Ve(g);let f=document.createDocumentFragment();for(let h of r){let p=I("button","dashcam-player-segment-item"),x=I("span","dashcam-player-segment-thumb"),$=I("span","dashcam-player-segment-copy"),b=I("span","dashcam-player-segment-meta"),T=I("span","dashcam-player-segment-time",h.timeLabel),k=I("span","dashcam-player-segment-status"),w=I("span","dashcam-player-segment-name",h.name),O=[h.timeLabel,h.name].filter(Boolean).join(" · ");if(p.type="button",p.dataset.segmentId=h.id,p.dataset.segmentLabel=O,p.setAttribute("aria-label",O),h.thumbnailSrc){let H=I("img");H.src=h.thumbnailSrc,H.alt="",H.loading="lazy",H.decoding="async",x.append(H)}else x.classList.add("is-empty");k.hidden=!0,b.append(w,k),$.append(T,b),p.append(x,$),p.addEventListener("click",()=>e.onSelect?.(h.id)),f.append(p)}return s.replaceChildren(f),l(m),r};return u(t,e.activeSegmentId),Object.freeze({element:a,get segments(){return r},render:u,setActive:l,setStatusFor:d})}function na(t={}){let e=I("div","dashcam-player-dialog");e.setAttribute("role","dialog"),e.setAttribute("aria-modal","true");let a=I("div","dashcam-player-layout"),n=I("div","dashcam-player-frame"),s=vn(t.currentGroup,t.labels),r=null,o=null;return a.append(n),s&&(e.classList.add("dashcam-player-dialog--browsable"),r=I("aside","dashcam-player-browser"),r.setAttribute("aria-label",String(t.labels?.browser||"")),o=I("section","dashcam-player-segment-region"),o.hidden=!0,r.append(s.element,o),a.append(r)),e.append(a),Object.freeze({browser:r,dialog:e,frame:n,group:s?.group||null,layout:a,segmentHost:o})}var sa=40,wn=720,ra=8,S={initialized:!1,loading:!1,loadingMore:!1,loadSeq:0,videos:[],rowHeight:80,windowStart:0,windowEnd:0,total:0,nextOffset:0,hasMore:!1,signature:"",renderFrame:0};function We(t,e=""){let a=document.getElementById("screenrecordStatus");a&&(a.textContent=t||"",a.hidden=!t,a.classList.toggle("is-error",e==="error"))}function Ke(t,e){return`/api/screenrecord/${t}/${encodeURIComponent(e)}`}function ca(t){return(t||[]).map(e=>[e.id||"",e.name||"",e.modifiedLabel||"",e.size||0].join("|")).join(`
+`)+"|"+(typeof LANG<"u"?LANG:"")}function ia(t){return!t||!S.hasMore||S.loading||S.loadingMore?!1:t.scrollHeight-t.scrollTop-t.clientHeight<=wn}function xn(t,e){let a=Math.max(48,Number(S.rowHeight)||80),n=Math.max(1,t?.clientHeight||a*8),s=Math.max(0,t?.scrollTop||0),r=Math.ceil(n/a),o=Math.max(0,Math.floor(s/a)-ra),c=Math.min(e,o+r+ra*2);return{start:o,end:c,rowHeight:a}}function Tn(t){let e=t?.querySelector?.(".screenrecord-row");if(!e)return;let a=window.getComputedStyle?.(t),n=Number.parseFloat(a?.rowGap||a?.gap||"0")||0,s=Math.max(48,e.getBoundingClientRect().height+n);Math.abs(s-S.rowHeight)<1||(S.rowHeight=s)}function oa(t,e){if(t<=0)return null;let a=document.createElement("div");return a.className="screenrecord-virtual-spacer",a.dataset.spacer=e,a.style.height=`${Math.round(t)}px`,a}function Mn(t,e,a){let n=String(t?.id||""),s=n?a.get(n):null;if(s)return s.style.setProperty("--i",String(e)),s.classList.remove("ui-stagger-item"),s;let r=document.createElement("template");return r.innerHTML=Ln(t,e),r.content.firstElementChild}function An(t,e,a){let n=new Map(Array.from(t.querySelectorAll(".screenrecord-row")).map(c=>[c.dataset.id||"",c]).filter(([c])=>!!c)),s=document.createDocumentFragment(),r=oa(a.start*a.rowHeight,"top"),o=oa((e.length-a.end)*a.rowHeight,"bottom");r&&s.appendChild(r),e.slice(a.start,a.end).forEach((c,l)=>{let d=Mn(c,a.start+l,n);d&&s.appendChild(d)}),o&&s.appendChild(o),Qe(t),t.replaceChildren(s)}function ge(t){let e=document.getElementById("screenrecordVideos");e&&e.classList.toggle("is-loading-more",!!t)}function Ye(){S.renderFrame||(S.renderFrame=requestAnimationFrame(()=>{S.renderFrame=0,q({preserve:!0})}))}function Je(t,e){if(!t)return;let a=S.videos||[],n=a.findIndex(o=>String(o?.id||"")===String(t)),s=n>0?a[n-1]:null,r=n>=0&&n{o?.(),Je(s.id||"",s.name||"")}:null,onNext:r?({close:o}={})=>{o?.(),Je(r.id||"",r.name||"")}:null})}function Ln(t,e=0){let a=escapeHtml(t.id||""),n=escapeHtml(t.name||"-"),s=escapeHtml(Xe(t.modifiedEpoch)||Ze(t.modifiedLabel||t.relativeModifiedLabel)||"-"),r=escapeHtml(le(t.size)),o=escapeHtml((t.ext||"video").toUpperCase());return`
-
![]()
+
${n}
@@ -15,37 +15,37 @@
- `}function q(t={}){let e=document.getElementById("screenrecordVideos");if(!e||!C())return;let a=t.preserve===!0,n=b.videos||[];if(b.loading&&!n.length){Ve(""),e.innerHTML=et("screen"),e.dataset.signature="",e.dataset.renderCount="0";return}if(!n.length){e.innerHTML=tt("screen"),e.dataset.signature="",e.dataset.renderCount="0",Ve("");return}Ve("");let s=pn(e,n.length),r=`${b.signature||sa(n)}|${s.start}:${s.end}|${b.loadingMore?"more":""}`;if(a&&e.dataset.signature===r){ie(e);return}yn(e,n,s),e.dataset.signature=r,e.dataset.renderCount=String(s.end-s.start),b.windowStart=s.start,b.windowEnd=s.end,ge(b.loadingMore),ie(e),requestAnimationFrame(()=>fn(e))}async function te({silent:t=!1,append:e=!1}={}){if(e&&(!b.hasMore||b.loading||b.loadingMore))return;let a=++b.loadSeq;e?(b.loadingMore=!0,ge(!0)):t||(b.loading=!0,b.loadingMore=!1,ge(!1),q());try{let n=e&&(b.nextOffset||b.videos.length)||0,s=e?ta:Math.max(ta,b.videos.length||0),r=await getJson(`/api/screenrecord/videos?offset=${n}&limit=${s}`);if(a!==b.loadSeq)return;if(!C()){b.loading=!1,b.loadingMore=!1,ge(!1);return}let o=Array.isArray(r.videos)?r.videos:[],i=e?b.videos.concat(o):o,l=sa(i);if(t&&l===b.signature){b.loading=!1,b.loadingMore=!1,ge(!1);return}b.videos=i,b.signature=l,b.total=Number.isFinite(Number(r.total))?Number(r.total):i.length,b.nextOffset=r.nextOffset==null?i.length:Number(r.nextOffset)||i.length,b.hasMore=!!r.hasMore,b.loading=!1,b.loadingMore=!1,ge(!1),q({animate:!t}),!t&&we.screen===0&&Re("screen",{reset:!0})}catch(n){if(a!==b.loadSeq)return;b.loading=!1,b.loadingMore=!1,ge(!1),!t&&C()&&(Ve(`${getUIText("screenrecord_load_failed","Failed to load screen recordings")}: ${n.message||n}`,"error"),showAppToast(n.message||getUIText("screenrecord_load_failed","Failed to load screen recordings"),{tone:"error"}))}}function wt(t){let e=new Error(t);return e.code=t,e}async function oa(t,e={}){let a=await fetch(`/api/dashcam/summary-source/${encodeURIComponent(t)}`,{cache:"no-store",signal:e.signal}),n=null;try{n=await a.json()}catch{}if(!a.ok||n?.ok===!1)throw wt("summary-source-unavailable");if(n?.mode!=="client-worker"||!Array.isArray(n.segments))throw wt("summary-source-invalid");let s=n.segments.filter(r=>r&&typeof r.url=="string"&&["zstd","bzip2","none"].includes(r.compression));if(!s.length)throw wt("summary-source-empty");return Object.freeze({route:String(n.route||t),schemaVersion:Number(n.schemaVersion)||1,segmentCount:Math.max(s.length,Number(n.segmentCount)||0),skippedSegments:Math.max(0,Number(n.skippedSegments)||0),segments:Object.freeze(s.map(r=>Object.freeze({segment:String(r.segment||""),index:Number(r.index)||0,kind:r.kind==="qlog"?"qlog":"rlog",name:String(r.name||""),url:r.url,size:Math.max(0,Number(r.size)||0),modifiedMs:Math.max(0,Number(r.modifiedMs)||0),compression:r.compression})))})}var ae=new Map;function ca(t){return!t||!Array.isArray(t.segments)||!t.segments.length?"":JSON.stringify([String(t.route||""),Number(t.schemaVersion)||0,Number(t.segmentCount)||0,Number(t.skippedSegments)||0,t.segments.map(e=>[String(e.segment||""),Number(e.index)||0,String(e.kind||""),String(e.name||""),Number(e.size)||0,Number(e.modifiedMs)||0,String(e.compression||"")])])}function ia(t){let e=ca(t);if(!e||!ae.has(e))return null;let a=ae.get(e);return ae.delete(e),ae.set(e,a),a}function la(t,e){let a=ca(t);if(!a||!e?.ok)return!1;for(ae.delete(a),ae.set(a,e);ae.size>6;)ae.delete(ae.keys().next().value);return!0}function A(t,e="",a=""){let n=document.createElement(t);return e&&(n.className=e),a!==""&&(n.textContent=String(a)),n}function Ue(t){let e=A("section","route-summary__section"),a=A("h3","route-summary__section-title",t);return e.append(a),Object.freeze({section:e,heading:a})}function xt(t,e,a={}){let n=A("div","route-summary-metric");a.variant&&(n.dataset.variant=a.variant);let s=A("dt","route-summary-metric__label",t);a.hideLabel&&s.classList.add("visually-hidden");let r=A("dd","route-summary-metric__value");return r.append(A("span","route-summary-metric__number",e.value)),e.unit&&r.append(A("span","route-summary-metric__unit",e.unit)),n.append(s,r),n}function da(t,e){let a=A("div","route-summary-composition__stat");return a.dataset.kind=e.key,e.emphasis&&(a.dataset.emphasis="true"),a.append(A("span","route-summary-composition__label",t),A("span","route-summary-composition__duration",e.duration),A("span","route-summary-composition__ratio",`${e.ratio.toFixed(1)}%`)),a}function Sn(t){let e=Object.fromEntries(t.map(o=>[o.key,Math.max(0,Number(o.seconds)||0)])),a=e.auto||0,n=e.manual||0,s=e.gas||0,r=e.brake||0;if(s+r>n&&s+r>0){let o=n/(s+r);s*=o,r*=o}return Object.freeze({auto:a,manual:n,gas:s,brake:r,other:Math.max(0,n-s-r)})}function nt(t,e){let a=A("span","route-summary-distribution__segment");return a.dataset.kind=t,a.style.setProperty("--route-summary-weight",String(e)),a}function ua(t){let e=A("div","route-summary-distribution");e.setAttribute("role","img"),e.setAttribute("aria-label",t.map(n=>`${n.label} ${n.ratio.toFixed(1)}%`).join(", "));let a=Sn(t);if(a.auto>0&&e.append(nt("auto",a.auto)),a.manual>0){let n=A("span","route-summary-distribution__manual");n.style.setProperty("--route-summary-weight",String(a.manual)),a.gas>0&&n.append(nt("gas",a.gas)),a.brake>0&&n.append(nt("brake",a.brake)),a.other>0&&n.append(nt("other",a.other)),e.append(n)}return e}function ma(t,e,a={}){let n=A("div","route-summary-event");a.kind&&(n.dataset.kind=a.kind),a.critical&&e.count>0&&(n.dataset.tone="danger");let s=A("div","route-summary-event__head");if(s.append(A("span","route-summary-event__label",t),A("span","route-summary-event__count",String(e.count))),n.append(s),e.items.length){let r=A("ul","route-summary-event__times");for(let o of e.items){let i=A("li","c-chip route-summary-event__time");i.append(A("time","",o.time)),o.peak&&i.append(A("span","route-summary-event__peak",o.peak)),r.append(i)}n.append(r)}return n}function ga(t,e,a){let n=A("div","route-summary-detail"),s=e.unit==="count"?`${e.value}${a}`:`${e.value} ${e.unit}`.trim();return n.append(A("dt","route-summary-detail__label",t),A("dd","route-summary-detail__value",s)),n}function pa(t,e=""){let a=A("div","route-summary-state");a.setAttribute("role","status"),a.setAttribute("aria-live","polite"),a.append(A("strong","route-summary-state__title",t));let n=A("span","route-summary-state__description",e);n.hidden=!e;let s=A("div","route-summary-state__progress");s.setAttribute("role","progressbar"),s.setAttribute("aria-label",t),s.setAttribute("aria-valuemin","0"),s.setAttribute("aria-valuemax","100"),s.dataset.mode="indeterminate";let r=A("span","route-summary-state__progress-fill");return s.append(r),a.append(n,s),Object.freeze({element:a,update(o,i){let l=Number(o);if(Number.isFinite(l)){let u=Math.max(0,Math.min(1,l)),d=Math.round(u*100);s.dataset.mode="determinate",s.setAttribute("aria-valuenow",String(d)),r.style.setProperty("--route-summary-progress",String(d))}else s.dataset.mode="indeterminate",s.removeAttribute("aria-valuenow"),r.style.removeProperty("--route-summary-progress");n.textContent=String(i||""),n.hidden=!n.textContent}})}function _(t,e=0){let a=Number(t);return Number.isFinite(a)?a:e}function W(t,e=1){return _(t).toLocaleString(void 0,{minimumFractionDigits:e,maximumFractionDigits:e})}function xe(t){let e=Math.max(0,Math.round(_(t))),a=Math.floor(e/3600),n=Math.floor(e%3600/60),s=e%60;return`${String(a).padStart(2,"0")}:${String(n).padStart(2,"0")}:${String(s).padStart(2,"0")}`}function Tt(t){return _(t)<=0?"":new Intl.DateTimeFormat(void 0,{hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}).format(new Date(_(t)*1e3))}function vn(t){return _(t)<=0?"":new Intl.DateTimeFormat(void 0,{year:"numeric",month:"2-digit",day:"2-digit"}).format(new Date(_(t)*1e3))}function _n(t,e){if(_(t?.wallTimeSec)>0)return Tt(t.wallTimeSec);let a=Math.max(0,_(t?.monoSec)-_(e)),n=Math.floor(a/60),s=Math.floor(a%60);return`+${String(n).padStart(2,"0")}:${String(s).padStart(2,"0")}`}function st(t,e){let a=Array.isArray(t?.items)?t.items:[];return Object.freeze({count:Math.max(0,_(t?.count)),items:Object.freeze(a.map(n=>Object.freeze({time:_n(n,e),peak:n?.peak==null?"":W(n.peak,2)})))})}function fa(t){if(!t?.ok)throw new TypeError("Invalid route summary result");let e=t.time||{},a=t.distance||{},n=t.extras||{},s=t.diagnostics||{},r=Math.max(0,_(e.totalSec)),o=Math.max(0,_(e.autoEnabledSec)),i=Math.max(0,_(e.manualSec)),l=x=>r>0?Math.max(0,Math.min(100,x/r*100)):0,u=Tt(e.startWallSec),d=Tt(e.endWallSec),m=vn(e.startWallSec),g=_(e.firstMonoSec),f=Math.max(0,_(s.processedSegments)),h=Math.max(f,_(s.requestedSegments)),p=Math.max(0,_(s.partialSegments));return Object.freeze({route:String(t.route||""),hasData:!!t.hasData,date:m,timeRange:u&&d?`${u}–${d}`:u,metrics:Object.freeze([Object.freeze({key:"duration",value:xe(r),unit:""}),Object.freeze({key:"distance",value:W(_(a.totalM)/1e3,2),unit:"km"}),Object.freeze({key:"averageSpeed",value:W(_(a.averageSpeedMs)*3.6,1),unit:"km/h"}),Object.freeze({key:"autoRatio",value:W(l(o),1),unit:"%"})]),distanceMetrics:Object.freeze([Object.freeze({key:"totalDistance",value:W(_(a.totalM)/1e3,2),unit:"km"}),Object.freeze({key:"autoDistance",value:W(_(a.autoM)/1e3,2),unit:"km"}),Object.freeze({key:"manualDistance",value:W(_(a.manualM)/1e3,2),unit:"km"}),Object.freeze({key:"averageSpeed",value:W(_(a.averageSpeedMs)*3.6,1),unit:"km/h"}),Object.freeze({key:"maxSpeed",value:W(_(a.maxSpeedMs)*3.6,1),unit:"km/h"})]),composition:Object.freeze([Object.freeze({key:"auto",seconds:o,duration:xe(o),ratio:l(o),emphasis:!0}),Object.freeze({key:"manual",seconds:i,duration:xe(i),ratio:l(i),emphasis:!1}),Object.freeze({key:"gas",seconds:_(e.manualGasSec),duration:xe(e.manualGasSec),ratio:l(_(e.manualGasSec)),emphasis:!1}),Object.freeze({key:"brake",seconds:_(e.manualBrakeSec),duration:xe(e.manualBrakeSec),ratio:l(_(e.manualBrakeSec)),emphasis:!1})]),events:Object.freeze({hardAccel:st(t.events?.hardAccel,g),overAccel:st(t.events?.overAccel,g),hardDecel:st(t.events?.hardDecel,g),overDecel:st(t.events?.overDecel,g)}),details:Object.freeze([Object.freeze({key:"stops",value:String(Math.max(0,_(n.stopCount))),unit:"count"}),Object.freeze({key:"disengagements",value:String(Math.max(0,_(n.disengageCount))),unit:"count"}),Object.freeze({key:"steerOverrides",value:String(Math.max(0,_(n.steerOverrideCount))),unit:"count"}),Object.freeze({key:"hardCorners",value:String(Math.max(0,_(n.cornerCount))),unit:"count"}),Object.freeze({key:"maxLateral",value:W(n.maxLatAccel,2),unit:"m/s²"}),Object.freeze({key:"stopTime",value:xe(e.stopSec),unit:""})]),warnings:Object.freeze({fcw:Math.max(0,_(n.warningCounts?.fcw)),ldw:Math.max(0,_(n.warningCounts?.ldw)),driverDistracted:Math.max(0,_(n.warningCounts?.driverDistracted))}),metadata:Object.freeze({source:String(t.source||"-").toUpperCase(),processed:f,requested:h,partial:p,policyVersion:_(t.policyVersion,1),elapsedMs:Math.max(0,_(s.elapsedMs))})})}var wn=Object.freeze({duration:["summary_duration","주행시간"],distance:["summary_distance","주행거리"],averageSpeed:["summary_average_speed","평균속도"],autoRatio:["summary_auto_ratio","자동주행 비율"]}),xn=Object.freeze({totalDistance:["summary_total_distance","총 거리"],autoDistance:["summary_auto_distance","자동"],manualDistance:["summary_manual_distance","수동"],averageSpeed:["summary_average_speed","평균속도"],maxSpeed:["summary_max_speed","최고속도"]}),ha=Object.freeze({auto:["report_auto","자동주행"],manual:["report_manual","수동주행"],gas:["summary_gas","가속 페달 사용"],brake:["summary_brake","브레이크 사용"]}),Tn=Object.freeze({hardAccel:["report_hard_accel","급가속"],overAccel:["report_over_accel","과가속"],hardDecel:["report_hard_decel","급감속"],overDecel:["report_over_decel","과감속"]}),Mn=Object.freeze({stops:["report_stops","정차"],disengagements:["summary_disengagements","운전자 개입"],steerOverrides:["report_steer_ovr","조향 개입"],hardCorners:["report_corner","하드코너링"],maxLateral:["report_max_lat","최대 횡가속"],stopTime:["summary_stop_time","정차 시간"]});function D(t,e){return typeof getUIText=="function"?getUIText(t,e):e}function Te(t,e){let a=t[e]||[e,e];return D(a[0],a[1])}function R(t,e="",a=""){let n=document.createElement(t);return e&&(n.className=e),a!==""&&(n.textContent=String(a)),n}function ya(t,e){t.replaceChildren();let a=R("article","route-summary");if(!e.hasData){let w=R("div","route-summary__empty",D("report_no_data","분석할 주행 데이터가 없습니다."));w.setAttribute("role","status"),a.append(w),t.append(a);return}let n=e.metrics.find(w=>w.key==="duration")||e.metrics[0],s=Ue(D("summary_duration","주행시간"));s.section.classList.add("route-summary__section--hero");let r=R("div","route-summary-hero"),o=R("div","route-summary-hero__time");if(o.append(xt(Te(wn,"duration"),n,{variant:"hero",hideLabel:!0})),e.date||e.timeRange){let w=R("p","route-summary-hero__subtitle");e.date&&w.append(R("span","route-summary-hero__date",e.date)),e.timeRange&&w.append(R("span","route-summary-hero__range",e.timeRange)),o.append(w)}let i=R("div","route-summary-composition");for(let w of e.composition)i.append(da(Te(ha,w.key),w));r.append(o,i);let l=e.composition.map(w=>({...w,label:Te(ha,w.key)}));s.section.append(r,ua(l));let u=R("div","route-summary__dashboard"),d=Ue(D("summary_distance_speed","주행 거리 / 속도"));d.section.classList.add("route-summary__section--distance");let m=R("dl","route-summary__metrics route-summary__metrics--distance");for(let w of e.distanceMetrics)m.append(xt(Te(xn,w.key),w,{variant:"compact"}));d.section.append(m);let g=Ue(D("summary_events","주행 이벤트"));g.section.classList.add("route-summary__section--events");let f=R("div","route-summary-events");for(let[w,H]of Object.entries(e.events))f.append(ma(Te(Tn,w),H,{critical:w.startsWith("hard"),kind:w}));g.section.append(f);let h=Ue(D("summary_additional_metrics","추가 지표"));h.section.classList.add("route-summary__section--details");let p=R("dl","route-summary-details"),x=D("report_unit_times","회");for(let w of e.details)p.append(ga(Te(Mn,w.key),w,x));if(h.section.append(p),e.warnings.fcw+e.warnings.ldw+e.warnings.driverDistracted>0){let w=R("p","route-summary__warning");w.textContent=`${D("report_warns","경고")} · FCW ${e.warnings.fcw} · LDW ${e.warnings.ldw} · ${D("report_warn_dm","운전자 주의")} ${e.warnings.driverDistracted}`,h.section.append(w)}let S=document.createElement("details");S.className="route-summary-metadata",S.append(R("summary","route-summary-metadata__summary",D("summary_analysis_info","분석 정보")));let T=R("dl","route-summary-metadata__list"),k=[[D("report_source","분석 소스"),e.metadata.source],[D("report_segments","세그먼트"),`${e.metadata.processed}/${e.metadata.requested}`],[D("summary_partial_segments","부분 분석"),String(e.metadata.partial)],[D("summary_policy_version","정책 버전"),String(e.metadata.policyVersion)],[D("summary_route_id","경로 ID"),e.route]];for(let[w,H]of k){let O=R("div","route-summary-metadata__row");O.append(R("dt","",w),R("dd","",H)),T.append(O)}S.append(T),u.append(d.section,h.section,g.section),a.append(s.section,u,S),t.append(a)}var An="/js/realtime/route_summary_worker.js?v=2607-02";function ba(t,e={}){return typeof Worker!="function"?Promise.reject(Object.assign(new Error("summary-worker-unsupported"),{code:"summary-worker-unsupported"})):new Promise((a,n)=>{let s=new Worker(An),r=e.signal,o=!1,i=(u,d)=>{o||(o=!0,r?.removeEventListener?.("abort",l),s.terminate(),u(d))},l=()=>{try{s.postMessage({type:"abort"})}catch{}i(n,new DOMException("Aborted","AbortError"))};if(s.onmessage=u=>{let d=u?.data||{};if(d.type==="progress")e.onProgress?.(d);else if(d.type==="warning")e.onWarning?.(d);else if(d.type==="complete")i(a,d.result);else if(d.type==="error"){let m=Object.assign(new Error(String(d.code||"summary-worker-failed")),{code:String(d.code||"summary-worker-failed")});i(n,m)}},s.onerror=()=>{i(n,Object.assign(new Error("summary-worker-failed"),{code:"summary-worker-failed"}))},r?.aborted){l();return}r?.addEventListener?.("abort",l,{once:!0}),s.postMessage({type:"analyze-route",route:t.route,schemaVersion:t.schemaVersion,segmentCount:t.segmentCount,skippedSegments:t.skippedSegments,segments:t.segments})})}var Me=null,En=0;function z(t,e,a){return typeof getUIText=="function"?getUIText(t,e,a):e}function Sa(t,e){return`routeSummary${e}${t}`}function va(t){return`
`}function Ln(t){return t?.code==="summary-worker-unsupported"?z("summary_worker_unsupported","이 브라우저에서는 주행요약 분석을 지원하지 않습니다."):t?.code==="summary-source-empty"?z("report_no_data","분석할 주행 데이터가 없습니다."):z("report_failed","주행요약을 불러오지 못했습니다.")}async function Mt(t){if(!t)return;Me?.abort();let e=++En,a=new AbortController;Me=a;let n="loading",s=Sa(e,"Loading"),r=openAppDialog({mode:"choice",html:!0,title:z("route_summary","주행요약"),messageHtml:va(s),cancelLabel:z("cancel","취소")}),o=pa(z("report_analyzing","주행 데이터를 분석하는 중…"));document.getElementById(s)?.append(o.element),r.then(()=>{n==="loading"&&Me===a&&a.abort()});try{let i=await oa(t,{signal:a.signal}),l=ia(i);if(l?o.update(1,z("summary_cached","저장된 주행요약을 불러오는 중…")):(l=await ba(i,{signal:a.signal,onProgress(g){let f=Math.max(0,Number(g.total)||0),h=Math.max(0,Number(g.completed)||0),p=Math.max(0,Number(g.totalBytes)||0),x=Math.max(0,Number(g.loadedBytes)||0),$=p>0?x/p:f>0?h/f:NaN,S=Number.isFinite($)?Math.round(Math.max(0,Math.min(1,$))*100):0,T=p>0?z("summary_progress_percent","세그먼트 {completed}/{total} · {percent}%",{completed:h,total:f,percent:S}):z("summary_progress","세그먼트 {completed}/{total}",{completed:h,total:f});o.update($,T)}}),la(i,l)),a.signal.aborted||Me!==a)return;let u=fa(l),d=Sa(e,"Content");n="result",openAppDialog({mode:"alert",html:!0,title:z("route_summary","주행요약"),messageHtml:va(d),confirmLabel:z("close","닫기")});let m=document.getElementById(d);m&&ya(m,u)}catch(i){if(i?.name==="AbortError"||a.signal.aborted)return;n="error",openAppDialog({mode:"alert",title:z("route_summary","주행요약"),message:Ln(i),confirmLabel:z("close","닫기")})}finally{Me===a&&(Me=null)}}var ct="carrot_dashcam_upload_job_id",La="carrot_dashcam_segment_sort",In=2e3,$n=10,Cn=40,Rn=3,Ia=10,_a=60,wa=120,Un=1.5,Dn=1.25,ne=null,pe=null,rt=null,xa=Promise.resolve(),$a=!1,c={initialized:!1,loading:!1,routes:[],expanded:new Set,selected:new Set,refreshTimer:null,loadingMore:!1,loadingSegments:new Set,segmentScrollTops:Object.create(null),scrollBusy:!1,scrollTimer:null,renderFrame:0,loadSeq:0,replayLoadSeq:0,layoutBound:!1,layoutTimer:null,landscape:null,layoutKey:"",total:0,nextOffset:0,hasMore:!1,routeHeight:300,routeHeights:Object.create(null),windowStart:0,windowEnd:0,signature:"",sort:"asc"};function Nn(){try{return localStorage.getItem(La)==="desc"?"desc":"asc"}catch{return"asc"}}c.sort=Nn();function se(){return c.sort==="desc"?"desc":"asc"}function Y(t){let e=String(t||"").split("--"),a=Number.parseInt(e[e.length-1]||"0",10);return Number.isFinite(a)?a:0}function He(t){return String(t||"").replace(/^0+(?=\d{3})/,"")}function he(t,e){return`/api/dashcam/${t}/${encodeURIComponent(e)}`}function ot(t,e=""){let a=document.getElementById("dashcamStatus");a&&(a.textContent=t||"",a.hidden=!t,a.classList.toggle("is-error",e==="error"))}function ye(t){return(t||[]).map(e=>[e.route||"",e.segmentCount||0,e.routeStartEpoch||0,e.routeEndEpoch||0,e.segmentsNextOffset??"",e.segmentsHasMore?"1":"0",...e.segmentFolders||[]].join("|")).join(`
-`)+"|"+(typeof LANG<"u"?LANG:"")}function ut(){return isCompactLandscapeMode()?210:310}function mt(){let t=window.matchMedia?.("(min-width: 900px)")?.matches?"wide":"narrow";return`${isCompactLandscapeMode()?"landscape":"portrait"}:${t}:${window.innerWidth}x${window.innerHeight}`}function fe(t){let e=String(t||""),a=Number(c.routeHeights?.[e]);if(Number.isFinite(a)&&a>0)return a;let n=Number(c.routeHeight)||ut();return e&&c.expanded.has(e)&&!isCompactLandscapeMode()?Math.max(560,n):Math.max(120,n)}function Hn(t=document.getElementById("dashcamRoutes")){let e=Math.max(120,fe("")),a=Math.max(e,t?.clientHeight||window.innerHeight||e),n=Math.max(1,Math.ceil(a/e));return Math.max($n,Math.min(Cn,n*Rn))}function Ca(t){let e=window.getComputedStyle?.(t);return Number.parseFloat(e?.rowGap||e?.gap||"0")||0}function On(t,e){let a=Array.isArray(e)?e:[],n=a.length,s=Math.max(1,t?.clientHeight||ut()*2),r=Math.max(0,t?.scrollTop||0),o=Math.max(120,fe("")),i=Math.max(o*2,s*Dn),l=Math.max(0,r-i),u=r+s+i,d=Ca(t),m=0,g=0,f=0,h=0;for(;m
0?d:0);if(h+T>=l)break;h+=T,f=h,m+=1}g=m;let p=f;for(;g0?d:0),g+=1;let x=Math.min(n,Math.max(g+Math.ceil(i/o),m+1));for(;g0?d:0),g+=1;let $=f;for(let T=m;T0?d:0);let S=Math.max(0,$-p);return{start:m,end:g,topHeight:f,bottomHeight:S}}function kn(t){return!t||!c.hasMore||c.loading||c.loadingMore?!1:t.scrollHeight-t.scrollTop-t.clientHeight<=Math.max(360,t.clientHeight*Un)}function Ut(){c.renderFrame&&(window.cancelAnimationFrame(c.renderFrame),c.renderFrame=0)}function J(t){let e=document.getElementById("dashcamRoutes");e&&e.classList.toggle("is-loading-more",!!t)}function Ta(t,e){if(t<=0)return null;let a=document.createElement("div");return a.className="dashcam-virtual-spacer",a.dataset.spacer=e,a.style.height=`${Math.round(t)}px`,a}function Ra(t){return t?.closest?.("[data-route-card]")?.dataset.routeCard||""}function De(t){let e=Ra(t);!e||!c.segmentScrollTops||(c.segmentScrollTops[e]=Math.max(0,t.scrollTop||0))}function Pn(t=document.getElementById("dashcamRoutes")){t&&t.querySelectorAll(".dashcam-segment-list").forEach(e=>De(e))}function Ma(t){let e=Ra(t);if(!e||!c.segmentScrollTops)return;let a=Number(c.segmentScrollTops[e]);!Number.isFinite(a)||a<=0||(t.scrollTop=a)}function zn(t=document.getElementById("dashcamRoutes")){t&&(t.querySelectorAll(".dashcam-segment-list").forEach(e=>Ma(e)),requestAnimationFrame(()=>{t.isConnected&&t.querySelectorAll(".dashcam-segment-list").forEach(e=>Ma(e))}))}function Oe(t){let e=String(t?.route||""),a=Ee(t||{segmentFolders:[]}).join(","),n=L(t).join(",");return[isCompactLandscapeMode()?"landscape":"portrait",c.expanded.has(e)?"expanded":"collapsed",typeof LANG<"u"?LANG:"",t?.title||"",t?.dateLabel||"",t?.latestModifiedEpoch||"",t?.latestModifiedLabel||"",t?.routeStartEpoch||"",t?.routeEndEpoch||"",V(t),t?.segmentsNextOffset??"",t?.segmentsHasMore?"more":"done",c.loadingSegments?.has(e)?"loading":"idle",n,a].join("|")}function Bn(t,e,a,n={}){let s=String(t?.route||""),r=Oe(t),o=s?a.get(s):null;if(o&&o.dataset.renderKey===r)return o.style.setProperty("--i",String(e)),o.dataset.routeIndex=String(e),o.classList.remove("ui-stagger-item"),o.querySelectorAll(".ui-stagger-item").forEach(l=>l.classList.remove("ui-stagger-item")),o;let i=document.createElement("template");return i.innerHTML=Pa(t,e,{animate:n.animate,animateIndex:e}),i.content.firstElementChild}function Fn(t,e,a,n={}){Pn(t);let s=new Map(Array.from(t.querySelectorAll("[data-route-card]")).map(l=>[l.dataset.routeCard||"",l]).filter(([l])=>!!l)),r=document.createDocumentFragment(),o=Ta(a.topHeight,"top"),i=Ta(a.bottomHeight,"bottom");o&&r.appendChild(o),e.slice(a.start,a.end).forEach((l,u)=>{let d=Bn(l,a.start+u,s,n);d&&r.appendChild(d)}),i&&r.appendChild(i),Qe(t),jn(t),t.replaceChildren(r),zn(t)}function jn(t){!K||!t||t.querySelectorAll?.("[data-segment-loader]").forEach(e=>{K.unobserve(e),delete e.dataset.observed})}function Ua(t){if(!t)return!1;let e=Ca(t),a=Array.from(t.querySelectorAll("[data-route-card]")),n=!1,s=0,r=0;if(a.forEach(o=>{let i=o.dataset.routeCard||"",l=Number.parseInt(o.dataset.routeIndex||"0",10)||0,u=Math.max(120,o.getBoundingClientRect().height+(l>0?e:0));!i||!Number.isFinite(u)||((!c.expanded.has(i)||isCompactLandscapeMode())&&(s+=u,r+=1),Math.abs((Number(c.routeHeights[i])||0)-u)>1&&(c.routeHeights[i]=u,n=!0))}),r){let o=s/r;Number.isFinite(o)&&Math.abs(o-c.routeHeight)>1&&(c.routeHeight=o,n=!0)}return n}function Se(){c.renderFrame||(c.renderFrame=requestAnimationFrame(()=>{c.renderFrame=0,N({preserve:!0,animate:!1})}))}function Dt(t){if(!t||!(c.routes||[]).length)return!1;let e=Array.from(t.querySelectorAll("[data-route-card]"));if(!e.length)return!0;let a=t.getBoundingClientRect(),n=e[0].getBoundingClientRect(),s=e[e.length-1].getBoundingClientRect(),r=fe("")*2;return n.top>a.top-r||s.bottom{})}function L(t){return Array.isArray(t?.segmentFolders)?t.segmentFolders:[]}function V(t){let e=Number(t?.segmentCount),a=L(t).length;return Number.isFinite(e)&&e>=a?e:a}function gt(t){return!!t?.segmentsHasMore||L(t).length=0?e:L(t).length}function G(t,e,a=se()){let n=[],s=new Set;[...t||[],...e||[]].forEach(o=>{!o||s.has(o)||(s.add(o),n.push(o))});let r=a==="desc"?-1:1;return n.sort((o,i)=>r*(Y(o)-Y(i)))}function qn(t,e){if(!t||!e)return t;let a=String(t.route||"");if(!a||a!==e.route)return t;let n=L(t),s=L(e);if(s.length<=n.length)return t;let r=G(n,s),o=Math.max(V(t),r.length);return{...t,segmentFolders:r,segmentTimes:{...e.segmentTimes||{},...t.segmentTimes||{}},segmentCount:o,segmentsNextOffset:r.lengthString(s).padStart(2,"0");return{date:`${a.getFullYear()}.${n(a.getMonth()+1)}.${n(a.getDate())}`,time:`${n(a.getHours())}:${n(a.getMinutes())}:${n(a.getSeconds())}`}}function Ae(t,e,a={}){let n=re(t),s=re(e);return!n||!s?"":n.date===s.date?a.multiline?`${n.date}
+ `}function q(t={}){let e=document.getElementById("screenrecordVideos");if(!e||!C())return;let a=t.preserve===!0,n=S.videos||[];if(S.loading&&!n.length){We(""),e.innerHTML=et("screen"),e.dataset.signature="",e.dataset.renderCount="0";return}if(!n.length){e.innerHTML=tt("screen"),e.dataset.signature="",e.dataset.renderCount="0",We("");return}We("");let s=xn(e,n.length),r=`${S.signature||ca(n)}|${s.start}:${s.end}|${S.loadingMore?"more":""}`;if(a&&e.dataset.signature===r){ie(e);return}An(e,n,s),e.dataset.signature=r,e.dataset.renderCount=String(s.end-s.start),S.windowStart=s.start,S.windowEnd=s.end,ge(S.loadingMore),ie(e),requestAnimationFrame(()=>Tn(e))}async function te({silent:t=!1,append:e=!1}={}){if(e&&(!S.hasMore||S.loading||S.loadingMore))return;let a=++S.loadSeq;e?(S.loadingMore=!0,ge(!0)):t||(S.loading=!0,S.loadingMore=!1,ge(!1),q());try{let n=e&&(S.nextOffset||S.videos.length)||0,s=e?sa:Math.max(sa,S.videos.length||0),r=await getJson(`/api/screenrecord/videos?offset=${n}&limit=${s}`);if(a!==S.loadSeq)return;if(!C()){S.loading=!1,S.loadingMore=!1,ge(!1);return}let o=Array.isArray(r.videos)?r.videos:[],c=e?S.videos.concat(o):o,l=ca(c);if(t&&l===S.signature){S.loading=!1,S.loadingMore=!1,ge(!1);return}S.videos=c,S.signature=l,S.total=Number.isFinite(Number(r.total))?Number(r.total):c.length,S.nextOffset=r.nextOffset==null?c.length:Number(r.nextOffset)||c.length,S.hasMore=!!r.hasMore,S.loading=!1,S.loadingMore=!1,ge(!1),q({animate:!t}),!t&&we.screen===0&&Ne("screen",{reset:!0})}catch(n){if(a!==S.loadSeq)return;S.loading=!1,S.loadingMore=!1,ge(!1),!t&&C()&&(We(`${getUIText("screenrecord_load_failed","Failed to load screen recordings")}: ${n.message||n}`,"error"),showAppToast(n.message||getUIText("screenrecord_load_failed","Failed to load screen recordings"),{tone:"error"}))}}function wt(t){let e=new Error(t);return e.code=t,e}async function la(t,e={}){let a=await fetch(`/api/dashcam/summary-source/${encodeURIComponent(t)}`,{cache:"no-store",signal:e.signal}),n=null;try{n=await a.json()}catch{}if(!a.ok||n?.ok===!1)throw wt("summary-source-unavailable");if(n?.mode!=="client-worker"||!Array.isArray(n.segments))throw wt("summary-source-invalid");let s=n.segments.filter(r=>r&&typeof r.url=="string"&&["zstd","bzip2","none"].includes(r.compression));if(!s.length)throw wt("summary-source-empty");return Object.freeze({route:String(n.route||t),schemaVersion:Number(n.schemaVersion)||1,segmentCount:Math.max(s.length,Number(n.segmentCount)||0),skippedSegments:Math.max(0,Number(n.skippedSegments)||0),segments:Object.freeze(s.map(r=>Object.freeze({segment:String(r.segment||""),index:Number(r.index)||0,kind:r.kind==="qlog"?"qlog":"rlog",name:String(r.name||""),url:r.url,size:Math.max(0,Number(r.size)||0),modifiedMs:Math.max(0,Number(r.modifiedMs)||0),compression:r.compression})))})}var ae=new Map;function ua(t){return!t||!Array.isArray(t.segments)||!t.segments.length?"":JSON.stringify([String(t.route||""),Number(t.schemaVersion)||0,Number(t.segmentCount)||0,Number(t.skippedSegments)||0,t.segments.map(e=>[String(e.segment||""),Number(e.index)||0,String(e.kind||""),String(e.name||""),Number(e.size)||0,Number(e.modifiedMs)||0,String(e.compression||"")])])}function da(t){let e=ua(t);if(!e||!ae.has(e))return null;let a=ae.get(e);return ae.delete(e),ae.set(e,a),a}function ma(t,e){let a=ua(t);if(!a||!e?.ok)return!1;for(ae.delete(a),ae.set(a,e);ae.size>6;)ae.delete(ae.keys().next().value);return!0}function A(t,e="",a=""){let n=document.createElement(t);return e&&(n.className=e),a!==""&&(n.textContent=String(a)),n}function De(t){let e=A("section","route-summary__section"),a=A("h3","route-summary__section-title",t);return e.append(a),Object.freeze({section:e,heading:a})}function xt(t,e,a={}){let n=A("div","route-summary-metric");a.variant&&(n.dataset.variant=a.variant);let s=A("dt","route-summary-metric__label",t);a.hideLabel&&s.classList.add("visually-hidden");let r=A("dd","route-summary-metric__value");return r.append(A("span","route-summary-metric__number",e.value)),e.unit&&r.append(A("span","route-summary-metric__unit",e.unit)),n.append(s,r),n}function ga(t,e){let a=A("div","route-summary-composition__stat");return a.dataset.kind=e.key,e.emphasis&&(a.dataset.emphasis="true"),a.append(A("span","route-summary-composition__label",t),A("span","route-summary-composition__duration",e.duration),A("span","route-summary-composition__ratio",`${e.ratio.toFixed(1)}%`)),a}function En(t){let e=Object.fromEntries(t.map(o=>[o.key,Math.max(0,Number(o.seconds)||0)])),a=e.auto||0,n=e.manual||0,s=e.gas||0,r=e.brake||0;if(s+r>n&&s+r>0){let o=n/(s+r);s*=o,r*=o}return Object.freeze({auto:a,manual:n,gas:s,brake:r,other:Math.max(0,n-s-r)})}function nt(t,e){let a=A("span","route-summary-distribution__segment");return a.dataset.kind=t,a.style.setProperty("--route-summary-weight",String(e)),a}function pa(t){let e=A("div","route-summary-distribution");e.setAttribute("role","img"),e.setAttribute("aria-label",t.map(n=>`${n.label} ${n.ratio.toFixed(1)}%`).join(", "));let a=En(t);if(a.auto>0&&e.append(nt("auto",a.auto)),a.manual>0){let n=A("span","route-summary-distribution__manual");n.style.setProperty("--route-summary-weight",String(a.manual)),a.gas>0&&n.append(nt("gas",a.gas)),a.brake>0&&n.append(nt("brake",a.brake)),a.other>0&&n.append(nt("other",a.other)),e.append(n)}return e}function fa(t,e,a={}){let n=A("div","route-summary-event");a.kind&&(n.dataset.kind=a.kind),a.critical&&e.count>0&&(n.dataset.tone="danger");let s=A("div","route-summary-event__head");if(s.append(A("span","route-summary-event__label",t),A("span","route-summary-event__count",String(e.count))),n.append(s),e.items.length){let r=A("ul","route-summary-event__times");for(let o of e.items){let c=A("li","c-chip route-summary-event__time");c.append(A("time","",o.time)),o.peak&&c.append(A("span","route-summary-event__peak",o.peak)),r.append(c)}n.append(r)}return n}function ha(t,e,a){let n=A("div","route-summary-detail"),s=e.unit==="count"?`${e.value}${a}`:`${e.value} ${e.unit}`.trim();return n.append(A("dt","route-summary-detail__label",t),A("dd","route-summary-detail__value",s)),n}function ya(t,e=""){let a=A("div","route-summary-state");a.setAttribute("role","status"),a.setAttribute("aria-live","polite"),a.append(A("strong","route-summary-state__title",t));let n=A("span","route-summary-state__description",e);n.hidden=!e;let s=A("div","route-summary-state__progress");s.setAttribute("role","progressbar"),s.setAttribute("aria-label",t),s.setAttribute("aria-valuemin","0"),s.setAttribute("aria-valuemax","100"),s.dataset.mode="indeterminate";let r=A("span","route-summary-state__progress-fill");return s.append(r),a.append(n,s),Object.freeze({element:a,update(o,c){let l=Number(o);if(Number.isFinite(l)){let d=Math.max(0,Math.min(1,l)),u=Math.round(d*100);s.dataset.mode="determinate",s.setAttribute("aria-valuenow",String(u)),r.style.setProperty("--route-summary-progress",String(u))}else s.dataset.mode="indeterminate",s.removeAttribute("aria-valuenow"),r.style.removeProperty("--route-summary-progress");n.textContent=String(c||""),n.hidden=!n.textContent}})}function v(t,e=0){let a=Number(t);return Number.isFinite(a)?a:e}function W(t,e=1){return v(t).toLocaleString(void 0,{minimumFractionDigits:e,maximumFractionDigits:e})}function xe(t){let e=Math.max(0,Math.round(v(t))),a=Math.floor(e/3600),n=Math.floor(e%3600/60),s=e%60;return`${String(a).padStart(2,"0")}:${String(n).padStart(2,"0")}:${String(s).padStart(2,"0")}`}function Tt(t){return v(t)<=0?"":new Intl.DateTimeFormat(void 0,{hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}).format(new Date(v(t)*1e3))}function In(t){return v(t)<=0?"":new Intl.DateTimeFormat(void 0,{year:"numeric",month:"2-digit",day:"2-digit"}).format(new Date(v(t)*1e3))}function $n(t,e){if(v(t?.wallTimeSec)>0)return Tt(t.wallTimeSec);let a=Math.max(0,v(t?.monoSec)-v(e)),n=Math.floor(a/60),s=Math.floor(a%60);return`+${String(n).padStart(2,"0")}:${String(s).padStart(2,"0")}`}function st(t,e){let a=Array.isArray(t?.items)?t.items:[];return Object.freeze({count:Math.max(0,v(t?.count)),items:Object.freeze(a.map(n=>Object.freeze({time:$n(n,e),peak:n?.peak==null?"":W(n.peak,2)})))})}function Sa(t){if(!t?.ok)throw new TypeError("Invalid route summary result");let e=t.time||{},a=t.distance||{},n=t.extras||{},s=t.diagnostics||{},r=Math.max(0,v(e.totalSec)),o=Math.max(0,v(e.autoEnabledSec)),c=Math.max(0,v(e.manualSec)),l=x=>r>0?Math.max(0,Math.min(100,x/r*100)):0,d=Tt(e.startWallSec),u=Tt(e.endWallSec),g=In(e.startWallSec),m=v(e.firstMonoSec),f=Math.max(0,v(s.processedSegments)),h=Math.max(f,v(s.requestedSegments)),p=Math.max(0,v(s.partialSegments));return Object.freeze({route:String(t.route||""),hasData:!!t.hasData,date:g,timeRange:d&&u?`${d}–${u}`:d,metrics:Object.freeze([Object.freeze({key:"duration",value:xe(r),unit:""}),Object.freeze({key:"distance",value:W(v(a.totalM)/1e3,2),unit:"km"}),Object.freeze({key:"averageSpeed",value:W(v(a.averageSpeedMs)*3.6,1),unit:"km/h"}),Object.freeze({key:"autoRatio",value:W(l(o),1),unit:"%"})]),distanceMetrics:Object.freeze([Object.freeze({key:"totalDistance",value:W(v(a.totalM)/1e3,2),unit:"km"}),Object.freeze({key:"autoDistance",value:W(v(a.autoM)/1e3,2),unit:"km"}),Object.freeze({key:"manualDistance",value:W(v(a.manualM)/1e3,2),unit:"km"}),Object.freeze({key:"averageSpeed",value:W(v(a.averageSpeedMs)*3.6,1),unit:"km/h"}),Object.freeze({key:"maxSpeed",value:W(v(a.maxSpeedMs)*3.6,1),unit:"km/h"})]),composition:Object.freeze([Object.freeze({key:"auto",seconds:o,duration:xe(o),ratio:l(o),emphasis:!0}),Object.freeze({key:"manual",seconds:c,duration:xe(c),ratio:l(c),emphasis:!1}),Object.freeze({key:"gas",seconds:v(e.manualGasSec),duration:xe(e.manualGasSec),ratio:l(v(e.manualGasSec)),emphasis:!1}),Object.freeze({key:"brake",seconds:v(e.manualBrakeSec),duration:xe(e.manualBrakeSec),ratio:l(v(e.manualBrakeSec)),emphasis:!1})]),events:Object.freeze({hardAccel:st(t.events?.hardAccel,m),overAccel:st(t.events?.overAccel,m),hardDecel:st(t.events?.hardDecel,m),overDecel:st(t.events?.overDecel,m)}),details:Object.freeze([Object.freeze({key:"stops",value:String(Math.max(0,v(n.stopCount))),unit:"count"}),Object.freeze({key:"disengagements",value:String(Math.max(0,v(n.disengageCount))),unit:"count"}),Object.freeze({key:"steerOverrides",value:String(Math.max(0,v(n.steerOverrideCount))),unit:"count"}),Object.freeze({key:"hardCorners",value:String(Math.max(0,v(n.cornerCount))),unit:"count"}),Object.freeze({key:"maxLateral",value:W(n.maxLatAccel,2),unit:"m/s²"}),Object.freeze({key:"stopTime",value:xe(e.stopSec),unit:""})]),warnings:Object.freeze({fcw:Math.max(0,v(n.warningCounts?.fcw)),ldw:Math.max(0,v(n.warningCounts?.ldw)),driverDistracted:Math.max(0,v(n.warningCounts?.driverDistracted))}),metadata:Object.freeze({source:String(t.source||"-").toUpperCase(),processed:f,requested:h,partial:p,policyVersion:v(t.policyVersion,1),elapsedMs:Math.max(0,v(s.elapsedMs))})})}var Cn=Object.freeze({duration:["summary_duration","주행시간"],distance:["summary_distance","주행거리"],averageSpeed:["summary_average_speed","평균속도"],autoRatio:["summary_auto_ratio","자동주행 비율"]}),Rn=Object.freeze({totalDistance:["summary_total_distance","총 거리"],autoDistance:["summary_auto_distance","자동"],manualDistance:["summary_manual_distance","수동"],averageSpeed:["summary_average_speed","평균속도"],maxSpeed:["summary_max_speed","최고속도"]}),ba=Object.freeze({auto:["report_auto","자동주행"],manual:["report_manual","수동주행"],gas:["summary_gas","가속 페달 사용"],brake:["summary_brake","브레이크 사용"]}),Un=Object.freeze({hardAccel:["report_hard_accel","급가속"],overAccel:["report_over_accel","과가속"],hardDecel:["report_hard_decel","급감속"],overDecel:["report_over_decel","과감속"]}),Nn=Object.freeze({stops:["report_stops","정차"],disengagements:["summary_disengagements","운전자 개입"],steerOverrides:["report_steer_ovr","조향 개입"],hardCorners:["report_corner","하드코너링"],maxLateral:["report_max_lat","최대 횡가속"],stopTime:["summary_stop_time","정차 시간"]});function N(t,e){return typeof getUIText=="function"?getUIText(t,e):e}function Te(t,e){let a=t[e]||[e,e];return N(a[0],a[1])}function R(t,e="",a=""){let n=document.createElement(t);return e&&(n.className=e),a!==""&&(n.textContent=String(a)),n}function _a(t,e){t.replaceChildren();let a=R("article","route-summary");if(!e.hasData){let w=R("div","route-summary__empty",N("report_no_data","분석할 주행 데이터가 없습니다."));w.setAttribute("role","status"),a.append(w),t.append(a);return}let n=e.metrics.find(w=>w.key==="duration")||e.metrics[0],s=De(N("summary_duration","주행시간"));s.section.classList.add("route-summary__section--hero");let r=R("div","route-summary-hero"),o=R("div","route-summary-hero__time");if(o.append(xt(Te(Cn,"duration"),n,{variant:"hero",hideLabel:!0})),e.date||e.timeRange){let w=R("p","route-summary-hero__subtitle");e.date&&w.append(R("span","route-summary-hero__date",e.date)),e.timeRange&&w.append(R("span","route-summary-hero__range",e.timeRange)),o.append(w)}let c=R("div","route-summary-composition");for(let w of e.composition)c.append(ga(Te(ba,w.key),w));r.append(o,c);let l=e.composition.map(w=>({...w,label:Te(ba,w.key)}));s.section.append(r,pa(l));let d=R("div","route-summary__dashboard"),u=De(N("summary_distance_speed","주행 거리 / 속도"));u.section.classList.add("route-summary__section--distance");let g=R("dl","route-summary__metrics route-summary__metrics--distance");for(let w of e.distanceMetrics)g.append(xt(Te(Rn,w.key),w,{variant:"compact"}));u.section.append(g);let m=De(N("summary_events","주행 이벤트"));m.section.classList.add("route-summary__section--events");let f=R("div","route-summary-events");for(let[w,O]of Object.entries(e.events))f.append(fa(Te(Un,w),O,{critical:w.startsWith("hard"),kind:w}));m.section.append(f);let h=De(N("summary_additional_metrics","추가 지표"));h.section.classList.add("route-summary__section--details");let p=R("dl","route-summary-details"),x=N("report_unit_times","회");for(let w of e.details)p.append(ha(Te(Nn,w.key),w,x));if(h.section.append(p),e.warnings.fcw+e.warnings.ldw+e.warnings.driverDistracted>0){let w=R("p","route-summary__warning");w.textContent=`${N("report_warns","경고")} · FCW ${e.warnings.fcw} · LDW ${e.warnings.ldw} · ${N("report_warn_dm","운전자 주의")} ${e.warnings.driverDistracted}`,h.section.append(w)}let b=document.createElement("details");b.className="route-summary-metadata",b.append(R("summary","route-summary-metadata__summary",N("summary_analysis_info","분석 정보")));let T=R("dl","route-summary-metadata__list"),k=[[N("report_source","분석 소스"),e.metadata.source],[N("report_segments","세그먼트"),`${e.metadata.processed}/${e.metadata.requested}`],[N("summary_partial_segments","부분 분석"),String(e.metadata.partial)],[N("summary_policy_version","정책 버전"),String(e.metadata.policyVersion)],[N("summary_route_id","경로 ID"),e.route]];for(let[w,O]of k){let H=R("div","route-summary-metadata__row");H.append(R("dt","",w),R("dd","",O)),T.append(H)}b.append(T),d.append(u.section,h.section,m.section),a.append(s.section,d,b),t.append(a)}var Dn="/js/realtime/route_summary_worker.js?v=2607-02";function va(t,e={}){return typeof Worker!="function"?Promise.reject(Object.assign(new Error("summary-worker-unsupported"),{code:"summary-worker-unsupported"})):new Promise((a,n)=>{let s=new Worker(Dn),r=e.signal,o=!1,c=(d,u)=>{o||(o=!0,r?.removeEventListener?.("abort",l),s.terminate(),d(u))},l=()=>{try{s.postMessage({type:"abort"})}catch{}c(n,new DOMException("Aborted","AbortError"))};if(s.onmessage=d=>{let u=d?.data||{};if(u.type==="progress")e.onProgress?.(u);else if(u.type==="warning")e.onWarning?.(u);else if(u.type==="complete")c(a,u.result);else if(u.type==="error"){let g=Object.assign(new Error(String(u.code||"summary-worker-failed")),{code:String(u.code||"summary-worker-failed")});c(n,g)}},s.onerror=()=>{c(n,Object.assign(new Error("summary-worker-failed"),{code:"summary-worker-failed"}))},r?.aborted){l();return}r?.addEventListener?.("abort",l,{once:!0}),s.postMessage({type:"analyze-route",route:t.route,schemaVersion:t.schemaVersion,segmentCount:t.segmentCount,skippedSegments:t.skippedSegments,segments:t.segments})})}var Me=null,On=0;function z(t,e,a){return typeof getUIText=="function"?getUIText(t,e,a):e}function wa(t,e){return`routeSummary${e}${t}`}function xa(t){return``}function Hn(t){return t?.code==="summary-worker-unsupported"?z("summary_worker_unsupported","이 브라우저에서는 주행요약 분석을 지원하지 않습니다."):t?.code==="summary-source-empty"?z("report_no_data","분석할 주행 데이터가 없습니다."):z("report_failed","주행요약을 불러오지 못했습니다.")}async function Mt(t){if(!t)return;Me?.abort();let e=++On,a=new AbortController;Me=a;let n="loading",s=wa(e,"Loading"),r=openAppDialog({mode:"choice",html:!0,title:z("route_summary","주행요약"),messageHtml:xa(s),cancelLabel:z("cancel","취소")}),o=ya(z("report_analyzing","주행 데이터를 분석하는 중…"));document.getElementById(s)?.append(o.element),r.then(()=>{n==="loading"&&Me===a&&a.abort()});try{let c=await la(t,{signal:a.signal}),l=da(c);if(l?o.update(1,z("summary_cached","저장된 주행요약을 불러오는 중…")):(l=await va(c,{signal:a.signal,onProgress(m){let f=Math.max(0,Number(m.total)||0),h=Math.max(0,Number(m.completed)||0),p=Math.max(0,Number(m.totalBytes)||0),x=Math.max(0,Number(m.loadedBytes)||0),$=p>0?x/p:f>0?h/f:NaN,b=Number.isFinite($)?Math.round(Math.max(0,Math.min(1,$))*100):0,T=p>0?z("summary_progress_percent","세그먼트 {completed}/{total} · {percent}%",{completed:h,total:f,percent:b}):z("summary_progress","세그먼트 {completed}/{total}",{completed:h,total:f});o.update($,T)}}),ma(c,l)),a.signal.aborted||Me!==a)return;let d=Sa(l),u=wa(e,"Content");n="result",openAppDialog({mode:"alert",html:!0,title:z("route_summary","주행요약"),messageHtml:xa(u),confirmLabel:z("close","닫기")});let g=document.getElementById(u);g&&_a(g,d)}catch(c){if(c?.name==="AbortError"||a.signal.aborted)return;n="error",openAppDialog({mode:"alert",title:z("route_summary","주행요약"),message:Hn(c),confirmLabel:z("close","닫기")})}finally{Me===a&&(Me=null)}}var kn=new Set(["complete","failed","canceled"]),Pn=Object.freeze({queued:0,preparing:1,uploading:8,notifying:98,canceling:98,complete:100,canceled:0,failed:0});function rt(t){if(t==null||t==="")return Number.NaN;let e=Number(t);return Number.isFinite(e)&&e>=0?e:Number.NaN}function Ta(t){return Math.max(0,Math.min(100,t))}function Ma(t){let e=String(t?.phase||"").trim().toLowerCase();if(e)return e;let a=String(t?.status||"").trim().toLowerCase();return a==="done"?"complete":a==="failed"||a==="canceled"?a:"queued"}function At(t={}){let e=Ma(t),a=String(t.status||"").trim().toLowerCase(),n=t.done===!0||a==="done"||a==="failed"||a==="canceled"||kn.has(e);if(a==="done"||e==="complete")return{mode:"determinate",value:100};let r=rt(t.progress);if(Number.isFinite(r))return{mode:"determinate",value:Ta(r)};let o=rt(t.bytes_current),c=rt(t.bytes_total);if(Number.isFinite(o)&&Number.isFinite(c)&&c>0){let d=8+Math.min(o,c)/c*89;return{mode:"determinate",value:Ta(Math.min(97,d))}}let l=Pn[e];return Number.isFinite(l)?{mode:"determinate",value:n?Math.max(0,l):l}:{mode:"indeterminate",value:null}}function Aa(){let t="",e=-1,a=0,n=Object.freeze({accepted:!0,phase:"queued",progressState:Object.freeze({mode:"determinate",value:0})});function s(o=""){t=String(o||""),e=-1,a=0}function r(o={}){let c=String(o.id||"");c&&c!==t&&s(c);let l=rt(o.revision);if(Number.isFinite(l)&&e>=0&&l{let o=String(r?.segment||"").trim();o&&!n.has(o)&&(n.add(o),a.push(o));let c=Array.isArray(r?.files)?r.files:[],l=Number(r?.totalSize)||c.reduce((d,u)=>d+(Number(u?.size)||0),0);s.segments+=1,s.files+=c.length,s.bytes+=l,c.forEach(d=>{let u=Fn(d);u&&(s[u]+=1)})}),s}var it="carrot_dashcam_upload_job_id",Ha="carrot_dashcam_segment_sort",Bn=2e3,jn=10,qn=40,Gn=3,ka=10,La=60,Ea=120,Vn=1.5,Wn=1.25,Jn=160,Kn=800,Yn=240,Ia=5,ne=null,pe=null,ot=null,$a=Promise.resolve(),Pa=!1,i={initialized:!1,loading:!1,routes:[],expanded:new Set,selected:new Set,refreshTimer:null,loadingMore:!1,loadingSegments:new Set,segmentScrollTops:Object.create(null),scrollBusy:!1,scrollTimer:null,renderFrame:0,loadSeq:0,replayLoadSeq:0,layoutBound:!1,layoutTimer:null,landscape:null,layoutKey:"",total:0,nextOffset:0,hasMore:!1,routeHeight:300,routeHeights:Object.create(null),windowStart:0,windowEnd:0,signature:"",sort:"asc"};function Xn(){try{return localStorage.getItem(Ha)==="desc"?"desc":"asc"}catch{return"asc"}}i.sort=Xn();function se(){return i.sort==="desc"?"desc":"asc"}function Y(t){let e=String(t||"").split("--"),a=Number.parseInt(e[e.length-1]||"0",10);return Number.isFinite(a)?a:0}function ke(t){return String(t||"").replace(/^0+(?=\d{3})/,"")}function he(t,e){return`/api/dashcam/${t}/${encodeURIComponent(e)}`}function ct(t,e=""){let a=document.getElementById("dashcamStatus");a&&(a.textContent=t||"",a.hidden=!t,a.classList.toggle("is-error",e==="error"))}function ye(t){return(t||[]).map(e=>[e.route||"",e.segmentCount||0,e.routeStartEpoch||0,e.routeEndEpoch||0,e.segmentsNextOffset??"",e.segmentsHasMore?"1":"0",...e.segmentFolders||[]].join("|")).join(`
+`)+"|"+(typeof LANG<"u"?LANG:"")}function dt(){return isCompactLandscapeMode()?210:310}function mt(){let t=window.matchMedia?.("(min-width: 900px)")?.matches?"wide":"narrow";return`${isCompactLandscapeMode()?"landscape":"portrait"}:${t}:${window.innerWidth}x${window.innerHeight}`}function fe(t){let e=String(t||""),a=Number(i.routeHeights?.[e]);if(Number.isFinite(a)&&a>0)return a;let n=Number(i.routeHeight)||dt();return e&&i.expanded.has(e)&&!isCompactLandscapeMode()?Math.max(560,n):Math.max(120,n)}function Zn(t=document.getElementById("dashcamRoutes")){let e=Math.max(120,fe("")),a=Math.max(e,t?.clientHeight||window.innerHeight||e),n=Math.max(1,Math.ceil(a/e));return Math.max(jn,Math.min(qn,n*Gn))}function za(t){let e=window.getComputedStyle?.(t);return Number.parseFloat(e?.rowGap||e?.gap||"0")||0}function Qn(t,e){let a=Array.isArray(e)?e:[],n=a.length,s=Math.max(1,t?.clientHeight||dt()*2),r=Math.max(0,t?.scrollTop||0),o=Math.max(120,fe("")),c=Math.max(o*2,s*Wn),l=Math.max(0,r-c),d=r+s+c,u=za(t),g=0,m=0,f=0,h=0;for(;g0?u:0);if(h+T>=l)break;h+=T,f=h,g+=1}m=g;let p=f;for(;m0?u:0),m+=1;let x=Math.min(n,Math.max(m+Math.ceil(c/o),g+1));for(;m0?u:0),m+=1;let $=f;for(let T=g;T0?u:0);let b=Math.max(0,$-p);return{start:g,end:m,topHeight:f,bottomHeight:b}}function es(t){return!t||!i.hasMore||i.loading||i.loadingMore?!1:t.scrollHeight-t.scrollTop-t.clientHeight<=Math.max(360,t.clientHeight*Vn)}function Nt(){i.renderFrame&&(window.cancelAnimationFrame(i.renderFrame),i.renderFrame=0)}function J(t){let e=document.getElementById("dashcamRoutes");e&&e.classList.toggle("is-loading-more",!!t)}function Ca(t,e){if(t<=0)return null;let a=document.createElement("div");return a.className="dashcam-virtual-spacer",a.dataset.spacer=e,a.style.height=`${Math.round(t)}px`,a}function Fa(t){return t?.closest?.("[data-route-card]")?.dataset.routeCard||""}function He(t){let e=Fa(t);!e||!i.segmentScrollTops||(i.segmentScrollTops[e]=Math.max(0,t.scrollTop||0))}function ts(t=document.getElementById("dashcamRoutes")){t&&t.querySelectorAll(".dashcam-segment-list").forEach(e=>He(e))}function Ra(t){let e=Fa(t);if(!e||!i.segmentScrollTops)return;let a=Number(i.segmentScrollTops[e]);!Number.isFinite(a)||a<=0||(t.scrollTop=a)}function as(t=document.getElementById("dashcamRoutes")){t&&(t.querySelectorAll(".dashcam-segment-list").forEach(e=>Ra(e)),requestAnimationFrame(()=>{t.isConnected&&t.querySelectorAll(".dashcam-segment-list").forEach(e=>Ra(e))}))}function Pe(t){let e=String(t?.route||""),a=Ee(t||{segmentFolders:[]}).join(","),n=E(t).join(",");return[isCompactLandscapeMode()?"landscape":"portrait",i.expanded.has(e)?"expanded":"collapsed",typeof LANG<"u"?LANG:"",t?.title||"",t?.dateLabel||"",t?.latestModifiedEpoch||"",t?.latestModifiedLabel||"",t?.routeStartEpoch||"",t?.routeEndEpoch||"",V(t),t?.segmentsNextOffset??"",t?.segmentsHasMore?"more":"done",i.loadingSegments?.has(e)?"loading":"idle",n,a].join("|")}function ns(t,e,a,n={}){let s=String(t?.route||""),r=Pe(t),o=s?a.get(s):null;if(o&&o.dataset.renderKey===r)return o.style.setProperty("--i",String(e)),o.dataset.routeIndex=String(e),o.classList.remove("ui-stagger-item"),o.querySelectorAll(".ui-stagger-item").forEach(l=>l.classList.remove("ui-stagger-item")),o;let c=document.createElement("template");return c.innerHTML=Ja(t,e,{animate:n.animate,animateIndex:e}),c.content.firstElementChild}function ss(t,e,a,n={}){ts(t);let s=new Map(Array.from(t.querySelectorAll("[data-route-card]")).map(l=>[l.dataset.routeCard||"",l]).filter(([l])=>!!l)),r=document.createDocumentFragment(),o=Ca(a.topHeight,"top"),c=Ca(a.bottomHeight,"bottom");o&&r.appendChild(o),e.slice(a.start,a.end).forEach((l,d)=>{let u=ns(l,a.start+d,s,n);u&&r.appendChild(u)}),c&&r.appendChild(c),Qe(t),rs(t),t.replaceChildren(r),as(t)}function rs(t){!K||!t||t.querySelectorAll?.("[data-segment-loader]").forEach(e=>{K.unobserve(e),delete e.dataset.observed})}function Ba(t){if(!t)return!1;let e=za(t),a=Array.from(t.querySelectorAll("[data-route-card]")),n=!1,s=0,r=0;if(a.forEach(o=>{let c=o.dataset.routeCard||"",l=Number.parseInt(o.dataset.routeIndex||"0",10)||0,d=Math.max(120,o.getBoundingClientRect().height+(l>0?e:0));!c||!Number.isFinite(d)||((!i.expanded.has(c)||isCompactLandscapeMode())&&(s+=d,r+=1),Math.abs((Number(i.routeHeights[c])||0)-d)>1&&(i.routeHeights[c]=d,n=!0))}),r){let o=s/r;Number.isFinite(o)&&Math.abs(o-i.routeHeight)>1&&(i.routeHeight=o,n=!0)}return n}function be(){i.renderFrame||(i.renderFrame=requestAnimationFrame(()=>{i.renderFrame=0,D({preserve:!0,animate:!1})}))}function Dt(t){if(!t||!(i.routes||[]).length)return!1;let e=Array.from(t.querySelectorAll("[data-route-card]"));if(!e.length)return!0;let a=t.getBoundingClientRect(),n=e[0].getBoundingClientRect(),s=e[e.length-1].getBoundingClientRect(),r=fe("")*2;return n.top>a.top-r||s.bottom{})}function E(t){return Array.isArray(t?.segmentFolders)?t.segmentFolders:[]}function V(t){let e=Number(t?.segmentCount),a=E(t).length;return Number.isFinite(e)&&e>=a?e:a}function gt(t){return!!t?.segmentsHasMore||E(t).length=0?e:E(t).length}function G(t,e,a=se()){let n=[],s=new Set;[...t||[],...e||[]].forEach(o=>{!o||s.has(o)||(s.add(o),n.push(o))});let r=a==="desc"?-1:1;return n.sort((o,c)=>r*(Y(o)-Y(c)))}function os(t,e){if(!t||!e)return t;let a=String(t.route||"");if(!a||a!==e.route)return t;let n=E(t),s=E(e);if(s.length<=n.length)return t;let r=G(n,s),o=Math.max(V(t),r.length);return{...t,segmentFolders:r,segmentTimes:{...e.segmentTimes||{},...t.segmentTimes||{}},segmentCount:o,segmentsNextOffset:r.lengthString(s).padStart(2,"0");return{date:`${a.getFullYear()}.${n(a.getMonth()+1)}.${n(a.getDate())}`,time:`${n(a.getHours())}:${n(a.getMinutes())}:${n(a.getSeconds())}`}}function Le(t,e,a={}){let n=re(t),s=re(e);return!n||!s?"":n.date===s.date?a.multiline?`${n.date}
${n.time} – ${s.time}`:`${n.date} ${n.time} – ${s.time}`:a.multiline?`${n.date} ${n.time}
-– ${s.date} ${s.time}`:`${n.date} ${n.time} – ${s.date} ${s.time}`}function Gn(t,e){let a=re(t?.routeStartEpoch),n=re(t?.routeEndEpoch),s=a&&n&&a.date===n.date;return{route:e,title:t?.title||He(e),dateLabel:s?a.date:"",timeRange:a&&n?s?`${a.time} – ${n.time}`:`${a.date} ${a.time} – ${n.date} ${n.time}`:String(t?.dateLabel||""),segmentCount:V(t)}}function Vn(t,e){let a=be(t,e),n=re(a?.startEpoch),s=re(a?.endEpoch);return n&&s?n.date===s.date?`${n.time}–${s.time}`:`${n.date} ${n.time}–${s.date} ${s.time}`:Ht(t,e)}function Wn(t,e,a){let n=Y(a),s=be(t,a);return Object.freeze({id:a,name:`${getUIText("segment_label","Segment")} ${n}`,timeLabel:Vn(t,a),title:`${He(e)} · ${getUIText("segment_label","Segment")} ${n}`,subtitle:Ae(s?.startEpoch,s?.endEpoch),src:he("video",a),thumbnailSrc:he("thumbnail",a)})}function Aa(t,e,a){return(a||[]).map(n=>Wn(t,e,n))}function be(t,e){let a=t?.segmentTimes?.[e];return a&&typeof a=="object"?a:null}function Jn(t){let e=re(t);return e?e.time.slice(0,5):""}function Ht(t,e){let a=be(t,e);return Jn(a?.startEpoch)||`SEG ${Y(e)}`}function Kn(t,e){let a=be(t,e);return a?Ae(a.startEpoch,a.endEpoch):""}function Yn(t){let e=String(t||"").trim();if(e.length<=22)return e;let a=e.split("--");if(a.length>=3){let n=a[0].slice(0,8),s=a[a.length-2].slice(-10),r=a[a.length-1];return`${n}…${s}--${r}`}return`${e.slice(0,10)}…${e.slice(-9)}`}function Xn(t,e){let a=be(t,e),n=Number(a?.startEpoch||0),s=Number(a?.endEpoch||0),o=Object.entries(t?.segmentTimes||{}).map(([f,h])=>({index:Y(f),startEpoch:Number(h?.startEpoch||0),endEpoch:Number(h?.endEpoch||0)})).filter(f=>f.startEpoch>0&&f.endEpoch>=f.startEpoch).sort((f,h)=>f.index-h.index)[0];if(!o)return null;let i=Y(e),l=o.startEpoch+(i-o.index)*_a,u=s-n,d=u>0&&u<=wa?u:_a,g=n>0&&Math.abs(n-l)<=wa?n:l;return{startEpoch:g,endEpoch:g+d}}function Zn(t,e){let a=Yn(e),n=Xn(t,e),s=re(n?.startEpoch),r=re(n?.endEpoch);if(s&&r){let o=`${s.time}–${r.time}`;return a?`${a} · ${o}`:o}return a||`#${Y(e)}`}var K=null;function Qn(t){return!t||!("IntersectionObserver"in window)?null:(K&&K._root===t||(K&&K.disconnect(),K=new IntersectionObserver(e=>{C()&&e.forEach(a=>{if(!a.isIntersecting)return;let n=a.target.dataset?.route||"";!n||c.loadingSegments?.has(n)||za(n).catch(()=>{})})},{root:t,rootMargin:"240px 0px",threshold:.01}),K._root=t),K)}function ke(t=document.getElementById("dashcamRoutes")){if(!t||!C())return;let e=Qn(t);if(e){t.querySelectorAll("[data-segment-loader]").forEach(n=>{n.dataset.observed!=="1"&&(n.dataset.observed="1",e.observe(n))});return}let a=t.getBoundingClientRect();t.querySelectorAll("[data-segment-loader]").forEach(n=>{let s=n.dataset.route||"";if(!s||c.loadingSegments?.has(s))return;let r=n.getBoundingClientRect();r.top<=a.bottom+160&&r.bottom>=a.top-40&&za(s).catch(()=>{})})}var At=0,Et=new Set;function Da(t){t&&(Et.add(t),!At&&(At=requestAnimationFrame(()=>{At=0,Et.forEach(e=>{e.isConnected&&De(e)}),Et.clear()})))}function Ee(t){return L(t).filter(e=>c.selected.has(e))}function Na(t){return _t(ee.statusFor(t),{reading:getUIText("segment_reading","Reading"),recent:getUIText("segment_recently_read","Recently viewed")})}function es(t){return Na(t)?.outerHTML||""}function ts(){let t=document.getElementById("dashcamRoutes");t&&t.querySelectorAll(".dashcam-segment-tile[data-segment]").forEach(e=>{let a=e.querySelector(".dashcam-segment-read-status");if(!a)return;let n=Na(e.dataset.segment);a.replaceChildren(...n?[n]:[]),a.hidden=!n})}function Ha(t){let e=String(t||"").trim();if(!e)return Promise.resolve(null);let a=xa.catch(()=>null).then(()=>postJson("/api/dashcam/read-state",{recentSegment:e}));return xa=a,a}function Ea(t){$a=!0;let e=ee.snapshot(),a=ee.select(t),n=a.state.previousSegment;return a.changed&&n&&n!==e.previousSegment&&Ha(n).catch(()=>{}),a}function as(){let t=ee.finish();return t.changed&&t.state.previousSegment&&Ha(t.state.previousSegment).catch(()=>{}),t}function Oa(){return rt||(rt=getJson("/api/dashcam/read-state").then(t=>($a||ee.restoreRecent(t?.recentSegment),ee.snapshot())).catch(()=>ee.snapshot()),rt)}function ka(t,e,a,n={}){let s=n.entry||(c.routes||[]).find(p=>p.route===t),r=n.compact===!0,o=n.animate===!0,i=escapeHtml(t),l=escapeHtml(e),u=escapeHtml(Ht(s,e)),d=es(e),m=c.selected.has(e)?" checked":"",g=["dashcam-segment-tile",r?"dashcam-segment-tile--compact":"",o?"dashcam-segment-tile--append":""].filter(Boolean).join(" "),f=r?"dashcam-segment-thumb dashcam-segment-thumb--compact":"dashcam-segment-thumb",h=r?"dashcam-segment-check dashcam-segment-check--compact":"dashcam-segment-check";return`
+– ${s.date} ${s.time}`:`${n.date} ${n.time} – ${s.date} ${s.time}`}function cs(t,e){let a=re(t?.routeStartEpoch),n=re(t?.routeEndEpoch),s=a&&n&&a.date===n.date;return{route:e,title:t?.title||ke(e),dateLabel:s?a.date:"",timeRange:a&&n?s?`${a.time} – ${n.time}`:`${a.date} ${a.time} – ${n.date} ${n.time}`:String(t?.dateLabel||""),segmentCount:V(t)}}function is(t,e){let a=Se(t,e),n=re(a?.startEpoch),s=re(a?.endEpoch);return n&&s?n.date===s.date?`${n.time}–${s.time}`:`${n.date} ${n.time}–${s.date} ${s.time}`:Ht(t,e)}function ls(t,e,a){let n=Y(a),s=Se(t,a);return Object.freeze({id:a,name:`${getUIText("segment_label","Segment")} ${n}`,timeLabel:is(t,a),title:`${ke(e)} · ${getUIText("segment_label","Segment")} ${n}`,subtitle:Le(s?.startEpoch,s?.endEpoch),src:he("video",a),thumbnailSrc:he("thumbnail",a)})}function Ua(t,e,a){return(a||[]).map(n=>ls(t,e,n))}function Se(t,e){let a=t?.segmentTimes?.[e];return a&&typeof a=="object"?a:null}function us(t){let e=re(t);return e?e.time.slice(0,5):""}function Ht(t,e){let a=Se(t,e);return us(a?.startEpoch)||`SEG ${Y(e)}`}function ds(t,e){let a=Se(t,e);return a?Le(a.startEpoch,a.endEpoch):""}function ms(t){let e=String(t||"").trim();if(e.length<=22)return e;let a=e.split("--");if(a.length>=3){let n=a[0].slice(0,8),s=a[a.length-2].slice(-10),r=a[a.length-1];return`${n}…${s}--${r}`}return`${e.slice(0,10)}…${e.slice(-9)}`}function gs(t,e){let a=Se(t,e),n=Number(a?.startEpoch||0),s=Number(a?.endEpoch||0),o=Object.entries(t?.segmentTimes||{}).map(([f,h])=>({index:Y(f),startEpoch:Number(h?.startEpoch||0),endEpoch:Number(h?.endEpoch||0)})).filter(f=>f.startEpoch>0&&f.endEpoch>=f.startEpoch).sort((f,h)=>f.index-h.index)[0];if(!o)return null;let c=Y(e),l=o.startEpoch+(c-o.index)*La,d=s-n,u=d>0&&d<=Ea?d:La,m=n>0&&Math.abs(n-l)<=Ea?n:l;return{startEpoch:m,endEpoch:m+u}}function ps(t,e){let a=ms(e),n=gs(t,e),s=re(n?.startEpoch),r=re(n?.endEpoch);if(s&&r){let o=`${s.time}–${r.time}`;return a?`${a} · ${o}`:o}return a||`#${Y(e)}`}var K=null;function fs(t){return!t||!("IntersectionObserver"in window)?null:(K&&K._root===t||(K&&K.disconnect(),K=new IntersectionObserver(e=>{C()&&e.forEach(a=>{if(!a.isIntersecting)return;let n=a.target.dataset?.route||"";!n||i.loadingSegments?.has(n)||Ka(n).catch(()=>{})})},{root:t,rootMargin:"240px 0px",threshold:.01}),K._root=t),K)}function ze(t=document.getElementById("dashcamRoutes")){if(!t||!C())return;let e=fs(t);if(e){t.querySelectorAll("[data-segment-loader]").forEach(n=>{n.dataset.observed!=="1"&&(n.dataset.observed="1",e.observe(n))});return}let a=t.getBoundingClientRect();t.querySelectorAll("[data-segment-loader]").forEach(n=>{let s=n.dataset.route||"";if(!s||i.loadingSegments?.has(s))return;let r=n.getBoundingClientRect();r.top<=a.bottom+160&&r.bottom>=a.top-40&&Ka(s).catch(()=>{})})}var Lt=0,Et=new Set;function ja(t){t&&(Et.add(t),!Lt&&(Lt=requestAnimationFrame(()=>{Lt=0,Et.forEach(e=>{e.isConnected&&He(e)}),Et.clear()})))}function Ee(t){return E(t).filter(e=>i.selected.has(e))}function qa(t){return vt(ee.statusFor(t),{reading:getUIText("segment_reading","Reading"),recent:getUIText("segment_recently_read","Recently viewed")})}function hs(t){return qa(t)?.outerHTML||""}function ys(){let t=document.getElementById("dashcamRoutes");t&&t.querySelectorAll(".dashcam-segment-tile[data-segment]").forEach(e=>{let a=e.querySelector(".dashcam-segment-read-status");if(!a)return;let n=qa(e.dataset.segment);a.replaceChildren(...n?[n]:[]),a.hidden=!n})}function Ga(t){let e=String(t||"").trim();if(!e)return Promise.resolve(null);let a=$a.catch(()=>null).then(()=>postJson("/api/dashcam/read-state",{recentSegment:e}));return $a=a,a}function Na(t){Pa=!0;let e=ee.snapshot(),a=ee.select(t),n=a.state.previousSegment;return a.changed&&n&&n!==e.previousSegment&&Ga(n).catch(()=>{}),a}function Ss(){let t=ee.finish();return t.changed&&t.state.previousSegment&&Ga(t.state.previousSegment).catch(()=>{}),t}function Va(){return ot||(ot=getJson("/api/dashcam/read-state").then(t=>(Pa||ee.restoreRecent(t?.recentSegment),ee.snapshot())).catch(()=>ee.snapshot()),ot)}function Wa(t,e,a,n={}){let s=n.entry||(i.routes||[]).find(p=>p.route===t),r=n.compact===!0,o=n.animate===!0,c=escapeHtml(t),l=escapeHtml(e),d=escapeHtml(Ht(s,e)),u=hs(e),g=i.selected.has(e)?" checked":"",m=["dashcam-segment-tile",r?"dashcam-segment-tile--compact":"",o?"dashcam-segment-tile--append":""].filter(Boolean).join(" "),f=r?"dashcam-segment-thumb dashcam-segment-thumb--compact":"dashcam-segment-thumb",h=r?"dashcam-segment-check dashcam-segment-check--compact":"dashcam-segment-check";return`
-
${u}
-
${d}
+
${d}
+
${u}
${l}
-
`}function Pa(t,e=0,a={}){let n=a.animate!==!1,s=Number.isFinite(a.animateIndex)?a.animateIndex:e,r=String(t.route||""),o=escapeHtml(Oe(t)),i=L(t),l=V(t),u=i.length,d=gt(t),m=c.loadingSegments?.has(r),g=c.expanded.has(r),f=isCompactLandscapeMode(),h=g||f,p=Ee(t),x=l>0&&p.length===l,$=x?getUIText("deselect_all","Deselect all"):getUIText("select_all","Select all"),S=i[0]||"",T=escapeHtml(r),k=escapeHtml(t.title||He(r)),w=escapeHtml(t.dateLabel||r),H=escapeHtml(Ye(t.latestModifiedEpoch)||Xe(t.latestModifiedLabel)||"-"),O=Ae(t.routeStartEpoch,t.routeEndEpoch,{multiline:!0}),v=O?`
${escapeHtml(O).replace(`
-`,"
")}
`:"",Le=S?`
-
-
![]()
+
`}function Ja(t,e=0,a={}){let n=a.animate!==!1,s=Number.isFinite(a.animateIndex)?a.animateIndex:e,r=String(t.route||""),o=escapeHtml(Pe(t)),c=E(t),l=V(t),d=c.length,u=gt(t),g=i.loadingSegments?.has(r),m=i.expanded.has(r),f=isCompactLandscapeMode(),h=m||f,p=Ee(t),x=l>0&&p.length===l,$=x?getUIText("deselect_all","Deselect all"):getUIText("select_all","Select all"),b=c[0]||"",T=escapeHtml(r),k=escapeHtml(t.title||ke(r)),w=escapeHtml(t.dateLabel||r),O=escapeHtml(Xe(t.latestModifiedEpoch)||Ze(t.latestModifiedLabel)||"-"),H=Le(t.routeStartEpoch,t.routeEndEpoch,{multiline:!0}),_=H?`
${escapeHtml(H).replace(`
+`,"
")}
`:"",Ie=b?`
`:"",$e=h?c.map((ue,Ce)=>Wa(r,ue,Ce,{compact:f,entry:t})).join(""):"",B=h&&u?`
`:"";return`
+ ${Ie}
-
+
${escapeHtml(getUIText("selected_count","{count} selected",{count:p.length}))}
@@ -77,34 +77,38 @@ ${n.time} – ${s.time}`:`${n.date} ${n.time} – ${s.time}`:a.multiline?`${n.da
-
${Ie}${F}
+
${$e}${B}
- `}function N(t={}){let e=document.getElementById("dashcamRoutes");if(!e)return;let a=t.animate!==!1,n=t.preserve===!0,s=c.routes||[];if(Ut(),!C())return;if(c.loading&&!s.length){ot(getUIText("loading","Loading...")),e.innerHTML=et("dashcam"),e.dataset.signature="",e.dataset.renderCount="0";return}if(!s.length){e.innerHTML=tt("dashcam"),e.dataset.signature="",e.dataset.renderCount="0",ot("");return}ot("");let r=On(e,s),o=`${c.signature||ye(s)}|${mt()}|${r.start}:${r.end}|${Math.round(r.topHeight)}:${Math.round(r.bottomHeight)}`;if(n&&e.dataset.signature===o){ie(e);return}Fn(e,s,r,{animate:a}),e.dataset.signature=o,e.dataset.renderCount=String(r.end-r.start),e.dataset.windowStart=String(r.start),e.dataset.windowEnd=String(r.end),c.windowStart=r.start,c.windowEnd=r.end,ie(e),requestAnimationFrame(()=>{C()&&(Ua(e)&&!c.scrollBusy&&Se(),ke(e))})}function pt(t){let e=document.getElementById("dashcamRoutes");if(!e)return!1;let a=c.routes||[],n=a.findIndex(l=>l.route===t);if(n<0)return!1;let s=Array.from(e.querySelectorAll("[data-route-card]")).find(l=>l.dataset.routeCard===t);if(!s)return!1;let r=document.createElement("template");r.innerHTML=Pa(a[n],n,{animate:!1});let o=r.content.querySelector(".dashcam-route-main"),i=s.querySelector(".dashcam-route-main");return!o||!i?!1:(i.replaceWith(o),s.dataset.renderKey=Oe(a[n]),ie(o),requestAnimationFrame(()=>{C()&&(Ua(e)&&Se(),ke(e))}),!0)}function ft(t){let e=document.getElementById("dashcamRoutes");if(!e)return!1;let a=(c.routes||[]).find(d=>d.route===t);if(!a)return!1;let n=Array.from(e.querySelectorAll("[data-route-card]")).find(d=>d.dataset.routeCard===t);if(!n)return!1;let s=Ee(a),r=V(a),o=r>0&&s.length===r,i=n.querySelector(".dashcam-selection-count");i&&(i.textContent=getUIText("selected_count","{count} selected",{count:s.length}));let l=n.querySelector('[data-action="select-route"]');l&&(l.dataset.selected=o?"1":"0",l.textContent=o?getUIText("deselect_all","Deselect all"):getUIText("select_all","Select all"));let u=n.querySelector('[data-action="upload-selected"]');return u&&(u.disabled=s.length===0),n.querySelectorAll('input[data-action="select-segment"]').forEach(d=>{let m=d.dataset.segment||"";d.checked=c.selected.has(m)}),n.dataset.renderKey=Oe(a),!0}function It(t,e=!1){let a=document.getElementById("dashcamRoutes"),n=(c.routes||[]).find(o=>o.route===t);if(!a||!n)return!1;let s=Array.from(a.querySelectorAll("[data-route-card]")).find(o=>o.dataset.routeCard===t);if(!s)return!1;let r=s.querySelector("[data-segment-loader]");return r?gt(n)?(r.classList.toggle("is-loading",!!e),!0):(r.remove(),!0):!1}function ns(t,e,a=0){if(!e.length)return!1;let n=document.getElementById("dashcamRoutes"),s=(c.routes||[]).find(p=>p.route===t);if(!n||!s)return!1;let r=Array.from(n.querySelectorAll("[data-route-card]")).find(p=>p.dataset.routeCard===t),o=r?.querySelector(".dashcam-segment-list");if(!r||!o)return!1;let i=o.scrollTop,u=o.scrollHeight>o.clientHeight+1&&o.scrollHeight-o.scrollTop-o.clientHeight<=48,d=!!c.scrollBusy;De(o);let m=isCompactLandscapeMode(),g=document.createElement("template"),f=!d;g.innerHTML=e.map((p,x)=>ka(t,p,a+x,{compact:m,animate:f,entry:s})).join("");let h=o.querySelector("[data-segment-loader]");if(o.insertBefore(g.content,h||null),ie(o),ft(t),It(t,!1),u&&!d){let p=Math.max(0,o.scrollHeight-o.clientHeight);o.scrollTop=p,De(o)}else o.scrollTop=i,De(o);return r.dataset.renderKey=Oe(s),!0}async function za(t){if(!t||c.loadingSegments?.has(t))return;let e=(c.routes||[]).find(n=>n.route===t);if(!e||!gt(e))return;let a=L(e).length;c.loadingSegments.add(t),ht({renderOnIdle:!1}),It(t,!0);try{let n=Lt(e),s=await getJson(`/api/dashcam/segments/${encodeURIComponent(t)}?offset=${n}&limit=${Ia}&sort=${se()}`),r=(c.routes||[]).find(u=>u.route===t);if(!r)return;let o=Array.isArray(s.segments)?s.segments:[],i=new Set(L(r)),l=o.filter(u=>u&&!i.has(u));if(r.segmentFolders=G(L(r),o),r.segmentTimes={...r.segmentTimes||{},...s.segmentTimes||{}},r.segmentCount=Number.isFinite(Number(s.total))?Number(s.total):V(r),r.segmentsNextOffset=s.nextOffset==null?r.segmentFolders.length:Number(s.nextOffset)||r.segmentFolders.length,r.segmentsHasMore=!!s.hasMore,c.signature=ye(c.routes),l.length&&ns(t,l,a))return}catch(n){C()&&showAppToast(n.message||getUIText("dashcam_load_failed","Failed to load dashcam list"),{tone:"error"})}finally{c.loadingSegments.delete(t),It(t,!1),requestAnimationFrame(()=>ke())}}async function ve({silent:t=!1,append:e=!1}={}){if(e&&(!c.hasMore||c.loading||c.loadingMore))return;let a=++c.loadSeq;e?(c.loadingMore=!0,J(!0)):t||(c.loading=!0,c.loadingMore=!1,J(!1),N());try{let n=e&&(c.nextOffset||c.routes.length)||0,s=c.routes.length||0,r=Hn(),o=e?r:Math.max(r,s||0),i=await getJson(`/api/dashcam/routes?offset=${n}&limit=${o}&segment_limit=${Ia}&sort=${se()}`);if(a!==c.loadSeq){e&&(c.loadingMore=!1,J(!1));return}if(!C()){c.loading=!1,c.loadingMore=!1,J(!1);return}let l=Array.isArray(i.routes)?i.routes:[],u=new Map((c.routes||[]).map(p=>[p.route,p])),d=e?l:l.map(p=>qn(p,u.get(p.route))),m=e?c.routes.concat(d):d,g=ye(m);if(t&&g===c.signature){c.loading=!1,c.loadingMore=!1,c.total=Number.isFinite(Number(i.total))?Number(i.total):m.length,c.nextOffset=i.nextOffset==null?m.length:Number(i.nextOffset)||m.length,c.hasMore=!!i.hasMore,J(!1);return}let f=new Set(m.map(p=>p.route)),h=new Set(m.flatMap(p=>L(p)));c.expanded=new Set(Array.from(c.expanded).filter(p=>f.has(p))),c.selected=new Set(Array.from(c.selected).filter(p=>h.has(p))),c.routeHeights=Object.fromEntries(Object.entries(c.routeHeights||{}).filter(([p])=>f.has(p))),c.segmentScrollTops=Object.fromEntries(Object.entries(c.segmentScrollTops||{}).filter(([p])=>f.has(p))),c.routes=m,c.signature=g,c.total=Number.isFinite(Number(i.total))?Number(i.total):m.length,c.nextOffset=i.nextOffset==null?m.length:Number(i.nextOffset)||m.length,c.hasMore=!!i.hasMore,c.loading=!1,c.loadingMore=!1,J(!1),N({animate:e}),!t&&we.dashcam===0&&Re("dashcam",{reset:!0}),requestAnimationFrame(()=>{Nt(),ke()})}catch(n){if(a!==c.loadSeq){e&&(c.loadingMore=!1,J(!1));return}c.loading=!1,c.loadingMore=!1,J(!1),!t&&C()&&(ot(`${getUIText("dashcam_load_failed","Failed to load dashcam list")}: ${n.message||n}`,"error"),showAppToast(n.message||getUIText("dashcam_load_failed","Failed to load dashcam list"),{tone:"error"}))}}function Ba(){c.refreshTimer||(c.refreshTimer=window.setInterval(()=>{CURRENT_PAGE!=="logs"||c.scrollBusy||(U==="screen"?te({silent:!0}).catch(()=>{}):!c.loading&&!c.loadingMore&&!c.loadingSegments?.size&&ve({silent:!0}).catch(()=>{}))},1e4))}function ht(t={}){let e=t.renderOnIdle!==!1;c.scrollBusy=!0,c.scrollTimer&&window.clearTimeout(c.scrollTimer),c.scrollTimer=window.setTimeout(()=>{if(c.scrollBusy=!1,e&&C()&&U==="dashcam"){let a=yt("dashcam");Dt(a)&&Se()}},380)}function Ot(t,e){let a=(c.routes||[]).find(u=>u.route===t),n=G(L(a),[],"asc"),s=Ea(e).state,r=be(a,e),o=Number(r?.endEpoch||0),i=Ae(r?.startEpoch,o),l=at(`${He(t)} · ${getUIText("segment_label","Segment")} ${Y(e)}`,he("video",e),{kind:"dashcam",currentGroup:Gn(a,t),segments:Aa(a,t,n),activeSegment:e,previousSegment:s.previousSegment,subtitle:i,subtitleForSegmentDuration:(u,d)=>{let m=be(a,u),g=Number(m?.endEpoch||0),f=Ae(m?.startEpoch,g),h=Number(d||0);return!Number.isFinite(h)||h<=0||g<=0?f:Ae(g-h,g)},onSegmentSend:u=>Pe([u],{confirm:!1,showProgress:!1,showResult:!0,showSuccessToast:!1}),onSegmentMenu:(u,{close:d}={})=>kt(t,u,{activePlayerClose:d}),onSegmentChange:u=>{Ea(u)},onClose:()=>{as()}});ze(t).then(u=>{if(!l?.isOpen?.())return;let d=(c.routes||[]).find(g=>g.route===t)||a,m=G(u,n,"asc");l.updateSegments?.(Aa(d,t,m))}).catch(()=>{})}ee.subscribe(ts);async function ss(t,e){let a=++c.replayLoadSeq,n=window.CarrotVisionReplay;if(!n?.start){showAppToast(getUIText("replay_unavailable","Drive replay is not available in this browser."),{tone:"error"});return}let s=(c.routes||[]).find(l=>l.route===t),r=G(L(s),[],"asc"),o=l=>Zn(s,l),i=n.start({route:t,segment:e,segments:r,title:o(e),titleForSegment:o});ze(t).then(l=>{a!==c.replayLoadSeq||!n.isActive?.()||n.updateSegments?.(G(l,[],"asc"),t)}).catch(()=>{}),await i}function it(t){return(Array.isArray(t)?t:[]).reduce((a,n)=>{let s=Array.isArray(n?.files)?n.files:[],r=Number(n?.totalSize)||s.reduce((o,i)=>o+(Number(i?.size)||0),0);return a.segments+=1,a.files+=s.length,a.bytes+=r,a},{segments:0,files:0,bytes:0})}function lt(t){let e=Number(t?.files||0),a=Number(t?.bytes||0),n=e>0?getUIText("upload_file_count","{count} files",{count:e}):getUIText("upload_files_unknown","files unknown"),s=a>0?Ze(a):getUIText("upload_size_unknown","size unknown");return`${n} · ${s}`}function rs(t){let e=String(t?.shareText||t?.message||""),a=it(t?.results||[]);return`
-
-
${escapeHtml(getUIText("upload_count","Upload {uploaded}/{total}",{uploaded:Number(t?.uploaded||0),total:Number(t?.total||0)}))}
-
${escapeHtml(lt(a))}
+ `}function D(t={}){let e=document.getElementById("dashcamRoutes");if(!e)return;let a=t.animate!==!1,n=t.preserve===!0,s=i.routes||[];if(Nt(),!C())return;if(i.loading&&!s.length){ct(getUIText("loading","Loading...")),e.innerHTML=et("dashcam"),e.dataset.signature="",e.dataset.renderCount="0";return}if(!s.length){e.innerHTML=tt("dashcam"),e.dataset.signature="",e.dataset.renderCount="0",ct("");return}ct("");let r=Qn(e,s),o=`${i.signature||ye(s)}|${mt()}|${r.start}:${r.end}|${Math.round(r.topHeight)}:${Math.round(r.bottomHeight)}`;if(n&&e.dataset.signature===o){ie(e);return}ss(e,s,r,{animate:a}),e.dataset.signature=o,e.dataset.renderCount=String(r.end-r.start),e.dataset.windowStart=String(r.start),e.dataset.windowEnd=String(r.end),i.windowStart=r.start,i.windowEnd=r.end,ie(e),requestAnimationFrame(()=>{C()&&(Ba(e)&&!i.scrollBusy&&be(),ze(e))})}function pt(t){let e=document.getElementById("dashcamRoutes");if(!e)return!1;let a=i.routes||[],n=a.findIndex(l=>l.route===t);if(n<0)return!1;let s=Array.from(e.querySelectorAll("[data-route-card]")).find(l=>l.dataset.routeCard===t);if(!s)return!1;let r=document.createElement("template");r.innerHTML=Ja(a[n],n,{animate:!1});let o=r.content.querySelector(".dashcam-route-main"),c=s.querySelector(".dashcam-route-main");return!o||!c?!1:(c.replaceWith(o),s.dataset.renderKey=Pe(a[n]),ie(o),requestAnimationFrame(()=>{C()&&(Ba(e)&&be(),ze(e))}),!0)}function ft(t){let e=document.getElementById("dashcamRoutes");if(!e)return!1;let a=(i.routes||[]).find(u=>u.route===t);if(!a)return!1;let n=Array.from(e.querySelectorAll("[data-route-card]")).find(u=>u.dataset.routeCard===t);if(!n)return!1;let s=Ee(a),r=V(a),o=r>0&&s.length===r,c=n.querySelector(".dashcam-selection-count");c&&(c.textContent=getUIText("selected_count","{count} selected",{count:s.length}));let l=n.querySelector('[data-action="select-route"]');l&&(l.dataset.selected=o?"1":"0",l.textContent=o?getUIText("deselect_all","Deselect all"):getUIText("select_all","Select all"));let d=n.querySelector('[data-action="upload-selected"]');return d&&(d.disabled=s.length===0),n.querySelectorAll('input[data-action="select-segment"]').forEach(u=>{let g=u.dataset.segment||"";u.checked=i.selected.has(g)}),n.dataset.renderKey=Pe(a),!0}function $t(t,e=!1){let a=document.getElementById("dashcamRoutes"),n=(i.routes||[]).find(o=>o.route===t);if(!a||!n)return!1;let s=Array.from(a.querySelectorAll("[data-route-card]")).find(o=>o.dataset.routeCard===t);if(!s)return!1;let r=s.querySelector("[data-segment-loader]");return r?gt(n)?(r.classList.toggle("is-loading",!!e),!0):(r.remove(),!0):!1}function bs(t,e,a=0){if(!e.length)return!1;let n=document.getElementById("dashcamRoutes"),s=(i.routes||[]).find(p=>p.route===t);if(!n||!s)return!1;let r=Array.from(n.querySelectorAll("[data-route-card]")).find(p=>p.dataset.routeCard===t),o=r?.querySelector(".dashcam-segment-list");if(!r||!o)return!1;let c=o.scrollTop,d=o.scrollHeight>o.clientHeight+1&&o.scrollHeight-o.scrollTop-o.clientHeight<=48,u=!!i.scrollBusy;He(o);let g=isCompactLandscapeMode(),m=document.createElement("template"),f=!u;m.innerHTML=e.map((p,x)=>Wa(t,p,a+x,{compact:g,animate:f,entry:s})).join("");let h=o.querySelector("[data-segment-loader]");if(o.insertBefore(m.content,h||null),ie(o),ft(t),$t(t,!1),d&&!u){let p=Math.max(0,o.scrollHeight-o.clientHeight);o.scrollTop=p,He(o)}else o.scrollTop=c,He(o);return r.dataset.renderKey=Pe(s),!0}async function Ka(t){if(!t||i.loadingSegments?.has(t))return;let e=(i.routes||[]).find(n=>n.route===t);if(!e||!gt(e))return;let a=E(e).length;i.loadingSegments.add(t),ht({renderOnIdle:!1}),$t(t,!0);try{let n=It(e),s=await getJson(`/api/dashcam/segments/${encodeURIComponent(t)}?offset=${n}&limit=${ka}&sort=${se()}`),r=(i.routes||[]).find(d=>d.route===t);if(!r)return;let o=Array.isArray(s.segments)?s.segments:[],c=new Set(E(r)),l=o.filter(d=>d&&!c.has(d));if(r.segmentFolders=G(E(r),o),r.segmentTimes={...r.segmentTimes||{},...s.segmentTimes||{}},r.segmentCount=Number.isFinite(Number(s.total))?Number(s.total):V(r),r.segmentsNextOffset=s.nextOffset==null?r.segmentFolders.length:Number(s.nextOffset)||r.segmentFolders.length,r.segmentsHasMore=!!s.hasMore,i.signature=ye(i.routes),l.length&&bs(t,l,a))return}catch(n){C()&&showAppToast(n.message||getUIText("dashcam_load_failed","Failed to load dashcam list"),{tone:"error"})}finally{i.loadingSegments.delete(t),$t(t,!1),requestAnimationFrame(()=>ze())}}async function _e({silent:t=!1,append:e=!1}={}){if(e&&(!i.hasMore||i.loading||i.loadingMore))return;let a=++i.loadSeq;e?(i.loadingMore=!0,J(!0)):t||(i.loading=!0,i.loadingMore=!1,J(!1),D());try{let n=e&&(i.nextOffset||i.routes.length)||0,s=i.routes.length||0,r=Zn(),o=e?r:Math.max(r,s||0),c=await getJson(`/api/dashcam/routes?offset=${n}&limit=${o}&segment_limit=${ka}&sort=${se()}`);if(a!==i.loadSeq){e&&(i.loadingMore=!1,J(!1));return}if(!C()){i.loading=!1,i.loadingMore=!1,J(!1);return}let l=Array.isArray(c.routes)?c.routes:[],d=new Map((i.routes||[]).map(p=>[p.route,p])),u=e?l:l.map(p=>os(p,d.get(p.route))),g=e?i.routes.concat(u):u,m=ye(g);if(t&&m===i.signature){i.loading=!1,i.loadingMore=!1,i.total=Number.isFinite(Number(c.total))?Number(c.total):g.length,i.nextOffset=c.nextOffset==null?g.length:Number(c.nextOffset)||g.length,i.hasMore=!!c.hasMore,J(!1);return}let f=new Set(g.map(p=>p.route)),h=new Set(g.flatMap(p=>E(p)));i.expanded=new Set(Array.from(i.expanded).filter(p=>f.has(p))),i.selected=new Set(Array.from(i.selected).filter(p=>h.has(p))),i.routeHeights=Object.fromEntries(Object.entries(i.routeHeights||{}).filter(([p])=>f.has(p))),i.segmentScrollTops=Object.fromEntries(Object.entries(i.segmentScrollTops||{}).filter(([p])=>f.has(p))),i.routes=g,i.signature=m,i.total=Number.isFinite(Number(c.total))?Number(c.total):g.length,i.nextOffset=c.nextOffset==null?g.length:Number(c.nextOffset)||g.length,i.hasMore=!!c.hasMore,i.loading=!1,i.loadingMore=!1,J(!1),D({animate:e}),!t&&we.dashcam===0&&Ne("dashcam",{reset:!0}),requestAnimationFrame(()=>{Ot(),ze()})}catch(n){if(a!==i.loadSeq){e&&(i.loadingMore=!1,J(!1));return}i.loading=!1,i.loadingMore=!1,J(!1),!t&&C()&&(ct(`${getUIText("dashcam_load_failed","Failed to load dashcam list")}: ${n.message||n}`,"error"),showAppToast(n.message||getUIText("dashcam_load_failed","Failed to load dashcam list"),{tone:"error"}))}}function Ya(){i.refreshTimer||(i.refreshTimer=window.setInterval(()=>{CURRENT_PAGE!=="logs"||i.scrollBusy||(U==="screen"?te({silent:!0}).catch(()=>{}):!i.loading&&!i.loadingMore&&!i.loadingSegments?.size&&_e({silent:!0}).catch(()=>{}))},1e4))}function ht(t={}){let e=t.renderOnIdle!==!1;i.scrollBusy=!0,i.scrollTimer&&window.clearTimeout(i.scrollTimer),i.scrollTimer=window.setTimeout(()=>{if(i.scrollBusy=!1,e&&C()&&U==="dashcam"){let a=yt("dashcam");Dt(a)&&be()}},380)}function kt(t,e){let a=(i.routes||[]).find(d=>d.route===t),n=G(E(a),[],"asc"),s=Na(e).state,r=Se(a,e),o=Number(r?.endEpoch||0),c=Le(r?.startEpoch,o),l=at(`${ke(t)} · ${getUIText("segment_label","Segment")} ${Y(e)}`,he("video",e),{kind:"dashcam",currentGroup:cs(a,t),segments:Ua(a,t,n),activeSegment:e,previousSegment:s.previousSegment,subtitle:c,subtitleForSegmentDuration:(d,u)=>{let g=Se(a,d),m=Number(g?.endEpoch||0),f=Le(g?.startEpoch,m),h=Number(u||0);return!Number.isFinite(h)||h<=0||m<=0?f:Le(m-h,m)},onSegmentSend:d=>Fe([d],{showProgress:!0,showResult:!0,showSuccessToast:!1}),onSegmentMenu:(d,{close:u}={})=>zt(t,d,{activePlayerClose:u}),onSegmentChange:d=>{Na(d)},onClose:()=>{Ss()}});Be(t).then(d=>{if(!l?.isOpen?.())return;let u=(i.routes||[]).find(m=>m.route===t)||a,g=G(d,n,"asc");l.updateSegments?.(Ua(u,t,g))}).catch(()=>{})}ee.subscribe(ys);async function _s(t,e){let a=++i.replayLoadSeq,n=window.CarrotVisionReplay;if(!n?.start){showAppToast(getUIText("replay_unavailable","Drive replay is not available in this browser."),{tone:"error"});return}let s=(i.routes||[]).find(l=>l.route===t),r=G(E(s),[],"asc"),o=l=>ps(s,l),c=n.start({route:t,segment:e,segments:r,title:o(e),titleForSegment:o});Be(t).then(l=>{a!==i.replayLoadSeq||!n.isActive?.()||n.updateSegments?.(G(l,[],"asc"),t)}).catch(()=>{}),await c}function Pt(t){let e=Number(t?.files||0),a=Number(t?.bytes||0),n=e>0?getUIText("upload_file_count","{count} files",{count:e}):getUIText("upload_files_unknown","files unknown"),s=a>0?le(a):getUIText("upload_size_unknown","size unknown");return`${n} · ${s}`}function vs(t){let e=getUIText("upload_segment_count","{count} segments",{count:Number(t?.segments||0)}),a=getUIText("upload_kind_count","{name} {count}",{name:"qcamera",count:Number(t?.qcamera||0)}),n=getUIText("upload_kind_count","{name} {count}",{name:"rlog",count:Number(t?.rlog||0)}),s=getUIText("upload_total_count","{count} files total",{count:Number(t?.files||0)}),r=Number(t?.bytes||0),o=r>0?le(r):getUIText("upload_size_unknown","size unknown");return`
+
+ ${escapeHtml(e)}
+ ${escapeHtml(`${a} · ${n}`)}
- ${escapeHtml(e)}
- `}async function $t(t){let e=String(t?.shareText||t?.message||"").trim();await openAppDialog({mode:"choice",title:getUIText("log_upload_result","Upload Result"),html:!0,messageHtml:`
${rs(t)}
`,cancelLabel:getUIText("close","Close"),copyText:e,copyLabel:getUIText("copy","Copy")})}function Ne(){let t=new Error(getUIText("upload_canceled","Upload canceled"));return t.name="DashcamUploadCanceled",t}function Fa(t){return t?.name==="DashcamUploadCanceled"||/cancell?ed/i.test(String(t?.message||""))}async function Ct(t){return t?postJson("/api/dashcam/upload/cancel",{id:t}):null}function ja(t,e=null,a={}){let n=document.createElement("div");n.className="dashcam-upload-progress",n.innerHTML=`
-
${escapeHtml(getUIText("log_uploading","Uploading logs"))}
-
0/${Number(t||0)}
-
-
${escapeHtml(e?lt(e):getUIText("loading","Loading..."))}
-
-
+
+ ${escapeHtml(o)}
+ ${escapeHtml(s)}
-
`,document.body.appendChild(n),document.body.classList.add("dialog-open"),requestAnimationFrame(()=>n.classList.add("is-open"));let s=n.querySelector(".dashcam-upload-progress__message"),r=n.querySelector(".dashcam-upload-progress__summary"),o=n.querySelector(".dashcam-upload-progress__bar span"),i=n.querySelector(".dashcam-upload-progress__cancel"),l=!1,u=typeof a.onCancel=="function"?a.onCancel:null;return i&&(i.onclick=async()=>{if(!(!u||i.disabled)){i.disabled=!0,i.textContent=getUIText("upload_canceling","Canceling...");try{await u()}catch(d){i.disabled=!1,i.textContent=a.cancelLabel||getUIText("cancel","Cancel"),showAppToast(d?.message||getUIText("error","Error"),{tone:"error",duration:3600})}}}),{setCancelHandler(d){u=typeof d=="function"?d:null,i&&(i.hidden=!u)},setCanceling(d){i&&(i.disabled=!!d,i.textContent=d?getUIText("upload_canceling","Canceling..."):a.cancelLabel||getUIText("cancel","Cancel"))},setMessage(d){s&&(s.textContent=d||"")},setProgress(d){if(!o)return;let m=Number(d);if(!Number.isFinite(m)||m<=0){o.style.animation="",o.style.transform="",o.style.width="";return}o.style.animation="none",o.style.transform="none",o.style.width=`${Math.max(4,Math.min(100,m))}%`},setSummary(d){r&&(r.textContent=d?lt(d):"")},close(){l||(l=!0,n.classList.remove("is-open"),window.setTimeout(()=>{n.remove(),syncModalBodyLock()},160))}}}function Rt(t){ne=t||null;try{t&&localStorage.setItem(ct,t)}catch{}}function le(t=null){(!t||ne===t)&&(ne=null);try{let e=localStorage.getItem(ct);(!t||e===t)&&localStorage.removeItem(ct)}catch{}}function qa(){try{return localStorage.getItem(ct)||""}catch{return""}}async function Ga(t,e,a=0,n={}){let s=null;for(;t;){if(typeof n.isCanceled=="function"&&n.isCanceled())throw Ne();s=await getJson(`/api/dashcam/upload/job?id=${encodeURIComponent(t)}`);let o=Number(s.step_current||0),i=Number(s.step_total||a||0),l=Number(s.progress),u=s.message||getUIText("log_uploading","Uploading logs");if(e.setMessage(`${o}/${i||a||0} · ${u}`),e.setProgress(l),s.status==="canceled"||s.result?.canceled)throw Ne();if(s.done)break;await waitMs(850)}let r=s?.result||{};if(r.canceled)throw Ne();if(!r||!Array.isArray(r.results))throw new Error(s?.error||getUIText("error","Error"));return r}async function dt(t={}){if(pe)return pe;let e=qa(),a=t.force===!0;return!e||!a&&e===ne?null:(pe=(async()=>{let n=null;try{n=await getJson(`/api/dashcam/upload/job?id=${encodeURIComponent(e)}`)}catch{return le(e),pe=null,null}if(n.done)return le(e),n.result?.results&&await $t(n.result),pe=null,n.result||null;Rt(e);let s=Number(n.step_total||0),r=!1,o=ja(s,null,{onCancel:async()=>{r=!0,o.setCanceling(!0),await Ct(e),le(e),o.close(),showAppToast(getUIText("upload_canceled","Upload canceled"),{duration:2600})}}),i=typeof beginAppActivity=="function"?beginAppActivity("logs",getUIText("log_uploading","Uploading logs")):null;try{o.setMessage(`${Number(n.step_current||0)}/${s} · ${n.message||getUIText("log_uploading","Uploading logs")}`),o.setProgress(Number(n.progress));let l=await Ga(e,o,s,{isCanceled:()=>r});return le(e),o.setMessage(`${Number(l.uploaded||0)}/${Number(l.total||s)}`),o.setProgress(100),o.setSummary(it(l.results||[])),showAppToast(l.message||getUIText("upload_complete_count","Upload complete {uploaded}/{total}",{uploaded:l.uploaded||0,total:l.total||s}),{tone:l.ok?"default":"error",duration:3600}),o.close(),i&&typeof endAppActivity=="function"&&(endAppActivity(i),i=null),await $t(l),l}catch(l){return o.close(),le(e),Fa(l)?r||showAppToast(getUIText("upload_canceled","Upload canceled"),{duration:2600}):showAppToast(`${getUIText("log_upload","Upload Logs")} ${getUIText("error","Error")}: ${l.message||l}`,{tone:"error",duration:4200}),null}finally{i&&typeof endAppActivity=="function"&&endAppActivity(i),ne===e&&(ne=null),pe=null}})(),pe)}async function Pe(t,e={}){if(ne){showAppToast(getUIText("upload_already_running","Upload already running."),{tone:"error",duration:3200});return}if(qa()){dt().catch(()=>{});return}let a=Array.from(new Set(t||[])).filter(Boolean);if(!a.length){showAppToast(getUIText("no_selected_segments","No segments selected."),{tone:"error"});return}let n={segments:a.length,files:0,bytes:0};if(e.confirm!==!1){try{let m=await postJson("/api/dashcam/upload/summary",{segments:a});Array.isArray(m?.summaries)&&(n=it(m.summaries))}catch{}let u=[getUIText("log_upload_confirm",`Upload ${a.length} logs to the Carrot server?`,{count:a.length}),lt(n),getUIText("upload_data_warning","This upload may use mobile data depending on your network connection.")].join(`
-
-`);if(!await appConfirm(u,{title:getUIText("log_upload","Upload Logs")}))return}let s={setCancelHandler(){},setCanceling(){},setMessage(){},setProgress(){},setSummary(){},close(){}},r=!1,o=e.showProgress===!1?s:ja(a.length,n,{onCancel:async()=>{r=!0,o.setCanceling(!0),l&&await Ct(l),le(l),o.close(),showAppToast(getUIText("upload_canceled","Upload canceled"),{duration:2600})}}),i=e.showProgress!==!1&&typeof beginAppActivity=="function"?beginAppActivity("logs",getUIText("log_uploading","Uploading logs")):null,l=null;try{if(o.setMessage(`0/${a.length} · ${getUIText("log_uploading","Uploading logs")}`),r)throw Ne();if(l=(await postJson("/api/dashcam/upload/start",{segments:a})).job_id,Rt(l),r)throw await Ct(l),Ne();let d=await Ga(l,o,a.length,{isCanceled:()=>r});le(l),o.setMessage(`${Number(d.uploaded||0)}/${Number(d.total||a.length)}`),o.setProgress(100),o.setSummary(it(d.results||[]));let m=d.message||getUIText("upload_complete_count","Upload complete {uploaded}/{total}",{uploaded:d.uploaded||0,total:d.total||a.length});e.showSuccessToast!==!1&&showAppToast(m,{tone:d.ok?"default":"error",duration:Number(e.toastDuration)||3600}),o.close(),i&&typeof endAppActivity=="function"&&(endAppActivity(i),i=null),e.showResult!==!1&&await $t(d)}catch(u){o.close(),l&&le(l);let d=u?.payload?.job_id||u?.payload?.job?.id||null;d?(Rt(d),showAppToast(getUIText("upload_already_running","Upload already running"),{tone:"error",duration:3200}),dt({force:!0}).catch(()=>{})):Fa(u)?r||showAppToast(getUIText("upload_canceled","Upload canceled"),{duration:2600}):showAppToast(`${getUIText("log_upload","Upload Logs")} ${getUIText("error","Error")}: ${u.message||u}`,{tone:"error",duration:Number(e.toastDuration)||4200})}finally{i&&typeof endAppActivity=="function"&&endAppActivity(i),l&&ne===l&&(ne=null)}}async function Va(t){let e=Number(t);if([2,5,10].includes(e))try{let a=await getJson(`/api/dashcam/recent?limit=${e}`),n=Array.isArray(a?.segments)?a.segments:[];if(!n.length){showAppToast(getUIText("no_completed_logs","No completed logs available."),{tone:"error"});return}await Pe(n)}catch(a){showAppToast(`${getUIText("recent_log_upload","Upload recent logs")}: ${a.message||a}`,{tone:"error",duration:4200})}}async function kt(t,e,a={}){let n=(c.routes||[]).find(l=>l.route===t),s=Ht(n,e),r=Kn(n,e),o=r?`${e}
-${r}`:e,i=await openAppDialog({mode:"choice",title:s,message:o,choiceLayout:"list",choices:[{label:getUIText("play","Play"),value:"play"},{label:getUIText("drive_replay","Replay"),value:"drive_replay"},{label:getUIText("log_upload","Upload Logs"),value:"upload"},{label:`qcamera ${getUIText("download","Download")}`,value:"download_qcamera"},{label:`rlog ${getUIText("download","Download")}`,value:"download_rlog"},{label:`qlog ${getUIText("download","Download")}`,value:"download_qlog"}]});if(i==="drive_replay")a.activePlayerClose?.(),await ss(t,e);else if(i==="play"&&typeof a.activePlayerClose!="function")Ot(t,e);else if(i==="upload")await Pe([e]);else if(i?.startsWith?.("download_")){let l=i.replace("download_","");window.open(he(`download/${encodeURIComponent(e)}`,l),"_blank","noopener")}}function os(t){let e=new Set;return String(t||"").split(",").forEach(a=>{let n=a.trim();if(!n)return;let s=n.match(/^(\d+)$/);if(s){e.add(Number.parseInt(s[1],10));return}let r=n.match(/^(\d+)\s*[-~]\s*(\d+)$/);if(!r)return;let o=Number.parseInt(r[1],10),i=Number.parseInt(r[2],10);o>i&&([o,i]=[i,o]),i-o>1e5&&(i=o+1e5);for(let l=o;l<=i;l+=1)e.add(l)}),e}async function ze(t){let e=[],a=new Set,n={},s=0;for(let o=0;o<1e3;o+=1){let i=await getJson(`/api/dashcam/segments/${encodeURIComponent(t)}?offset=${s}&limit=${In}&sort=${se()}`),l=Array.isArray(i.segments)?i.segments:[];Object.assign(n,i.segmentTimes||{});for(let u of l)u&&!a.has(u)&&(a.add(u),e.push(u));if(!i.hasMore||!l.length)break;s=i.nextOffset==null?e.length:Number(i.nextOffset)||e.length}let r=(c.routes||[]).find(o=>o.route===t);return r&&(r.segmentTimes={...r.segmentTimes||{},...n}),e}async function cs(t){let e=(c.routes||[]).find(h=>h.route===t);if(!e)return;let a;try{a=await ze(t)}catch{a=L(e)}a.length||(a=L(e));let n=a.map(h=>Y(h)),s=n.length?Math.min(...n):0,r=n.length?Math.max(...n):0,o=n.length?s===r?`${s}`:`${s}–${r}`:"",i=o?`${getUIText("select_range","Select range")} (${o})`:getUIText("select_range","Select range"),l=await appPrompt("",{title:i,placeholder:getUIText("range_input_hint","1, 1-2, 1,2,3,4")});if(l==null)return;let u=os(l);if(!u.size){showAppToast(getUIText("range_invalid","Enter a valid range"),{tone:"error"});return}let d=(c.routes||[]).find(h=>h.route===t);if(!d)return;let m=new Map;a.forEach(h=>m.set(Y(h),h));let g=0;u.forEach(h=>{let p=m.get(h);p&&!c.selected.has(p)&&(c.selected.add(p),g+=1)}),a.length>L(d).length&&(d.segmentFolders=G(L(d),a),d.segmentCount=Math.max(V(d),d.segmentFolders.length),d.segmentsHasMore=d.segmentFolders.length
{let o=c.expanded.has(r.route)||isCompactLandscapeMode(),i=L(r).length;if(o&&i>0)gt(r)?n.push(r):r.segmentFolders=G(L(r),[],e);else{let l=V(r);r.segmentFolders=[],r.segmentsNextOffset=0,r.segmentsHasMore=l>0}});let s=document.getElementById("dashcamRoutes");c.signature=ye(a),s&&(s.dataset.signature=""),C()&&N({animate:!1}),requestAnimationFrame(()=>ke()),n.length&&(await Promise.all(n.map(async r=>{let o=[];try{o=await ze(r.route)}catch{o=[]}se()===e&&(o.length?(r.segmentFolders=G(o,[],e),r.segmentCount=Math.max(V(r),r.segmentFolders.length),r.segmentsHasMore=r.segmentFolders.lengthr.route===t);if(!a)return;if(e){for(let r of L(a))c.selected.delete(r);ft(t)||N({animate:!1});return}let n;try{n=await ze(t)}catch{n=L(a)}let s=(c.routes||[]).find(r=>r.route===t);if(s){n.length||(n=L(s)),n.length>L(s).length&&(s.segmentFolders=G(L(s),n),s.segmentCount=Math.max(V(s),s.segmentFolders.length),s.segmentsHasMore=s.segmentFolders.length{C()&&(a.scrollTop=s,requestAnimationFrame(()=>{C()&&(a.scrollTop=s,n==="dashcam"&&typeof Se=="function"&&Se(),n==="screen"&&typeof Ke=="function"&&Ke())}))})}function en(){document.getElementById("logsTabs")?.setAttribute("aria-label",getUIText("logs","Logs"))}function tn(){let t=document.getElementById("logsMenuButton"),e=getUIText("logs_menu","Log menu");t?.setAttribute("aria-label",e),t?.setAttribute("title",e)}function ds(){let t=typeof se=="function"?se():"asc";return[{heading:getUIText("logs_sort","Sort")},{label:getUIText("sort_ascending","Sort: ascending"),value:`${Bt}:asc`,selected:t==="asc"},{label:getUIText("sort_descending","Sort: descending"),value:`${Bt}:desc`,selected:t==="desc"},{heading:getUIText("recent_log_upload","Upload recent logs")},...is.map(e=>({label:getUIText("upload_recent_logs","Upload recent {count}",{count:e}),value:`${Za}:${e}`}))]}async function us(t){let[e,a]=String(t||"").split(":");e===Bt?await Wa(a==="desc"?"desc":"asc"):e===Za&&await Va(Number(a)||0)}async function ms(){if(Pt)return;let t=document.getElementById("logsMenu");Pt=!0,t?.classList.add("is-open");let e=null;try{e=await openAppDialog({mode:"choice",title:getUIText("logs_menu","Log menu"),choiceLayout:"list",choices:ds()})}finally{Pt=!1,t?.classList.remove("is-open")}await us(e)}function gs(){let t=document.getElementById("logsMenuButton");t&&(tn(),t.dataset.bound!=="1"&&(t.dataset.bound="1",t.addEventListener("click",()=>{ms().catch(()=>{})})))}function Ye(t){let e=Number(t||0);if(!Number.isFinite(e)||e<=0)return"";let a=Math.max(0,Math.floor(Date.now()/1e3)-Math.floor(e));return a<60?getUIText("just_now","just now"):a<3600?getUIText("minutes_ago","{count} min ago",{count:Math.floor(a/60)}):a<86400?getUIText("hours_ago","{count} hr ago",{count:Math.floor(a/3600)}):getUIText("days_ago","{count} days ago",{count:Math.floor(a/86400)})}function Xe(t){let e=String(t||"").trim();if(!e)return"";if(/^(방금\s*전|just\s*now)$/i.test(e))return getUIText("just_now","just now");let a=e.match(/^(\d+)\s*(?:분\s*전|min(?:ute)?s?\s*ago)$/i);if(a)return getUIText("minutes_ago","{count} min ago",{count:a[1]});let n=e.match(/^(\d+)\s*(?:시간\s*전|hr?s?\s*ago|hour?s?\s*ago)$/i);if(n)return getUIText("hours_ago","{count} hr ago",{count:n[1]});let s=e.match(/^(\d+)\s*(?:일\s*전|day?s?\s*ago)$/i);return s?getUIText("days_ago","{count} days ago",{count:s[1]}):e}function Ze(t){let e=Number(t)||0;return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:e<1024*1024*1024?`${(e/(1024*1024)).toFixed(1)} MB`:`${(e/(1024*1024*1024)).toFixed(1)} GB`}var ps=8,fs=900;function Xa(t){if(!t)return;let e=t.dataset?.src||"";if(!e)return;let a=t.closest(".dashcam-segment-thumb"),n=!!a&&!t.hasAttribute("data-fallback");a&&a.classList.add("is-thumb-loading");let s=()=>{a&&a.classList.remove("is-thumb-loading")},r=()=>{t.removeEventListener("load",o),t.removeEventListener("error",i)},o=()=>{r(),s()},i=()=>{r();let l=Number(t.dataset.thumbRetry||0);if(!n||l>=ps){s();return}t.dataset.thumbRetry=String(l+1);let u=Math.round(fs*Math.pow(1.5,l));window.setTimeout(()=>{if(!t.isConnected||!C()){s();return}t.addEventListener("load",o),t.addEventListener("error",i),t.src=e+(e.includes("?")?"&":"?")+"ready="+(l+1)},u)};t.addEventListener("load",o),t.addEventListener("error",i),t.src=e,t.removeAttribute("data-src")}function ie(t){if(!C())return;let a=Array.from((t||document).querySelectorAll?.("img[data-src]")||[]);if(a.length){if(!("IntersectionObserver"in window)){a.forEach(Xa);return}oe||(oe=new IntersectionObserver(n=>{n.forEach(s=>{s.isIntersecting&&(oe.unobserve(s.target),Xa(s.target))})},{root:null,rootMargin:"720px 0px",threshold:.01})),a.forEach(n=>oe.observe(n))}}function hs(){oe&&(oe.disconnect(),oe=null)}function Qe(t){!oe||!t||t.querySelectorAll?.("img[data-src]").forEach(e=>{oe.unobserve(e)})}function et(t="dashcam"){let e=t==="screen"?6:4,a=t==="screen"?"logs-loading-row":"logs-loading-card";return`${Array.from({length:e},(n,s)=>`
`).join("")}
`}function tt(t="dashcam"){let a=t==="screen"?getUIText("screenrecord_empty_title","No screen recordings"):getUIText("dashcam_empty_title","No dashcam records");return`
+ `}function ws(t){let e=Array.isArray(t?.results)?t.results:[],a=Oe(e),n=e.filter(g=>g?.ok!==!0),s=e.length<=Ia?e:n.slice(0,Ia),r=Math.max(0,n.length-s.length),o=s.map(g=>{let m=g?.ok===!0,f=m?getUIText("upload_status_complete","Complete"):getUIText("upload_status_failed","Failed");return`
+ ${escapeHtml(String(g?.segment||""))}
+ ${escapeHtml(f)}
+
`}).join(""),c=r>0?`${escapeHtml(getUIText("upload_more_failed","{count} more failed",{count:r}))}
`:"",l=o||c?`${o}${c}
`:"",d=getUIText("upload_kind_count","{name} {count}",{name:"qcamera",count:Number(a.qcamera||0)}),u=getUIText("upload_kind_count","{name} {count}",{name:"rlog",count:Number(a.rlog||0)});return``}async function Ct(t){let e=String(t?.shareText||t?.message||"").trim();await openAppDialog({mode:"choice",title:getUIText("log_upload_result","Upload Result"),html:!0,messageHtml:ws(t),cancelLabel:getUIText("close","Close"),copyText:e,copyLabel:getUIText("copy","Copy")})}function lt(){let t=new Error(getUIText("upload_canceled","Upload canceled"));return t.name="DashcamUploadCanceled",t}function Xa(t){return t?.name==="DashcamUploadCanceled"||/cancell?ed/i.test(String(t?.message||""))}async function Rt(t){return t?postJson("/api/dashcam/upload/cancel",{id:t}):null}var Da=Object.freeze({queued:["upload_phase_queued","Waiting to upload"],preparing:["upload_phase_preparing","Preparing files"],uploading:["upload_phase_uploading","Uploading files"],notifying:["upload_phase_notifying","Sending completion notice"],canceling:["upload_phase_canceling","Canceling upload"],complete:["upload_phase_complete","Upload complete"],canceled:["upload_phase_canceled","Upload canceled"],failed:["upload_phase_failed","Upload failed"]});function xs(t,e=""){let a=String(t||"").trim().toLowerCase();Da[a]||(a=e==="failed"?"failed":e==="canceled"?"canceled":e==="done"?"complete":"uploading");let[n,s]=Da[a];return getUIText(n,s)}function Oa(t={},e=0,a=null,n=null){let s=Math.max(0,Number(t.step_current||0)),r=Math.max(0,Number(t.step_total||e||0)),o=String(t.phase||"preparing").trim().toLowerCase(),c=n||At(t),d=r>0&&(o==="queued"||o==="preparing"||o==="uploading")?getUIText("upload_segment_progress","Segments {current}/{total}",{current:s,total:r}):"",u=[xs(o,t.status),d].filter(Boolean).join(" · "),g=Math.max(0,Number(t.bytes_current||0)),m=Math.max(0,Number(t.bytes_total||a?.bytes||0)),f=Math.max(0,Number(t.bytes_per_second||0)),h=o==="queued"||o==="preparing",p=a?Pt(a):getUIText("loading","Loading...");return!h&&m>0&&(p=`${le(Math.min(g,m))} / ${le(m)}`,f>0&&(p+=` · ${le(f)}/s`)),{message:u,progressState:c,summary:p}}function Za(t,e=null,a={}){let n=Date.now(),s=Aa(),r={phase:"queued",step_current:0,step_total:t,progress:0},o=s.update(r),c=Oa(r,t,e,o.progressState),l=openAppProgressDialog({title:getUIText("log_uploading","Uploading logs"),message:c.message,progressState:c.progressState,progressLabel:getUIText("upload_progress_label","Upload progress"),summary:c.summary,cancelLabel:a.cancelLabel||getUIText("cancel","Cancel"),cancelingLabel:getUIText("upload_canceling","Canceling..."),onCancel:a.onCancel});return Object.freeze({completion:l.completion,setCancelHandler:l.setCancelHandler,setCanceling:l.setCanceling,setMessage:l.setMessage,setProgressState:l.setProgressState,update(d,u=t){let g=s.update(d);if(!g.accepted)return;let m=Oa(d,u,e,g.progressState);l.setMessage(m.message),l.setProgressState(m.progressState),l.setSummary(m.summary)},setSummary(d){let u=typeof d=="string"?d:d?Pt(d):"";l.setSummary(u)},async settle(){let d=Kn-(Date.now()-n);d>0&&await waitMs(d),await waitMs(Yn)},close:l.close})}function Ut(t){ne=t||null;try{t&&localStorage.setItem(it,t)}catch{}}function Ae(t=null){(!t||ne===t)&&(ne=null);try{let e=localStorage.getItem(it);(!t||e===t)&&localStorage.removeItem(it)}catch{}}function Qa(){try{return localStorage.getItem(it)||""}catch{return""}}async function en(t,e,a=0){let n=null;for(;t;){if(n=await getJson(`/api/dashcam/upload/job?id=${encodeURIComponent(t)}`),e.update(n,a),n.status==="canceled"||n.result?.canceled)throw lt();if(n.done)break;await waitMs(Jn)}let s=n?.result||{};if(s.canceled)throw lt();if(!s||!Array.isArray(s.results))throw new Error(n?.error||getUIText("error","Error"));return s}async function ut(t={}){if(pe)return pe;let e=Qa(),a=t.force===!0;return!e||!a&&e===ne?null:(pe=(async()=>{let n=null;try{n=await getJson(`/api/dashcam/upload/job?id=${encodeURIComponent(e)}`)}catch{return Ae(e),pe=null,null}if(n.done)return Ae(e),n.result?.results&&await Ct(n.result),pe=null,n.result||null;Ut(e);let s=Number(n.step_total||0),r=!1,o=Za(s,null,{onCancel:async()=>{r=!0;let l=await Rt(e);o.update(l,s)}}),c=typeof beginAppActivity=="function"?beginAppActivity("logs",getUIText("log_uploading","Uploading logs")):null;try{o.update(n,s);let l=await en(e,o,s);Ae(e);let d=Oe(l.results||[]);return o.update({phase:l.ok?"complete":"failed",status:l.ok?"done":"failed",step_current:Number(l.uploaded||0),step_total:Number(l.total||s),progress:l.ok?100:null,bytes_current:d.bytes,bytes_total:d.bytes},s),o.setSummary(d),await o.settle(),showAppToast(getUIText("upload_complete_count","Upload complete {uploaded}/{total}",{uploaded:l.uploaded||0,total:l.total||s}),{tone:l.ok?"default":"error",duration:3600}),await o.close(),c&&typeof endAppActivity=="function"&&(endAppActivity(c),c=null),await Ct(l),l}catch(l){return o.close(),Ae(e),Xa(l)?showAppToast(getUIText("upload_canceled","Upload canceled"),{duration:2600}):showAppToast(`${getUIText("log_upload","Upload Logs")} ${getUIText("error","Error")}: ${l.message||l}`,{tone:"error",duration:4200}),null}finally{c&&typeof endAppActivity=="function"&&endAppActivity(c),ne===e&&(ne=null),pe=null}})(),pe)}async function Fe(t,e={}){if(ne){showAppToast(getUIText("upload_already_running","Upload already running."),{tone:"error",duration:3200});return}if(Qa()){ut().catch(()=>{});return}let a=Array.from(new Set(t||[])).filter(Boolean);if(!a.length){showAppToast(getUIText("no_selected_segments","No segments selected."),{tone:"error"});return}let n={segments:a.length,segmentNames:a,files:0,qcamera:0,rlog:0,bytes:0};if(e.confirm!==!1){try{let u=await postJson("/api/dashcam/upload/summary",{segments:a});if(!Array.isArray(u?.summaries)||u.summaries.length!==a.length)throw new Error(getUIText("upload_summary_unavailable","Upload information is unavailable."));n=Oe(u.summaries)}catch(u){showAppToast(`${getUIText("log_upload","Upload Logs")} ${getUIText("error","Error")}: ${u?.message||u}`,{tone:"error",duration:4200});return}if(!await appConfirm("",{title:getUIText("log_upload","Upload Logs"),html:!0,messageHtml:vs(n),confirmLabel:getUIText("upload_send","Send")}))return}let s={setCancelHandler(){},setCanceling(){},setMessage(){},setProgressState(){},update(){},setSummary(){},settle(){return Promise.resolve()},close(){return Promise.resolve()}},r=!1,o=e.showProgress===!1?s:Za(a.length,n,{onCancel:async()=>{if(r=!0,l){let d=await Rt(l);o.update(d,a.length)}else o.update({phase:"canceling",step_current:0,step_total:a.length,progress:null},a.length)}}),c=e.showProgress!==!1&&typeof beginAppActivity=="function"?beginAppActivity("logs",getUIText("log_uploading","Uploading logs")):null,l=null;try{if(o.update({phase:"preparing",step_current:0,step_total:a.length,progress:null},a.length),r)throw lt();if(l=(await postJson("/api/dashcam/upload/start",{segments:a})).job_id,Ut(l),r)throw await Rt(l),lt();let u=await en(l,o,a.length);Ae(l);let g=Oe(u.results||[]);o.update({phase:u.ok?"complete":"failed",status:u.ok?"done":"failed",step_current:Number(u.uploaded||0),step_total:Number(u.total||a.length),progress:u.ok?100:null,bytes_current:g.bytes,bytes_total:g.bytes},a.length),o.setSummary(g),await o.settle();let m=getUIText("upload_complete_count","Upload complete {uploaded}/{total}",{uploaded:u.uploaded||0,total:u.total||a.length});e.showSuccessToast!==!1&&showAppToast(m,{tone:u.ok?"default":"error",duration:Number(e.toastDuration)||3600}),await o.close(),c&&typeof endAppActivity=="function"&&(endAppActivity(c),c=null),e.showResult!==!1&&await Ct(u)}catch(d){o.close(),l&&Ae(l);let u=d?.payload?.job_id||d?.payload?.job?.id||null;u?(Ut(u),showAppToast(getUIText("upload_already_running","Upload already running"),{tone:"error",duration:3200}),ut({force:!0}).catch(()=>{})):Xa(d)?showAppToast(getUIText("upload_canceled","Upload canceled"),{duration:2600}):showAppToast(`${getUIText("log_upload","Upload Logs")} ${getUIText("error","Error")}: ${d.message||d}`,{tone:"error",duration:Number(e.toastDuration)||4200})}finally{c&&typeof endAppActivity=="function"&&endAppActivity(c),l&&ne===l&&(ne=null)}}async function tn(t){let e=Number(t);if([2,5,10].includes(e))try{let a=await getJson(`/api/dashcam/recent?limit=${e}`),n=Array.isArray(a?.segments)?a.segments:[];if(!n.length){showAppToast(getUIText("no_completed_logs","No completed logs available."),{tone:"error"});return}await Fe(n)}catch(a){showAppToast(`${getUIText("recent_log_upload","Upload recent logs")}: ${a.message||a}`,{tone:"error",duration:4200})}}async function zt(t,e,a={}){let n=(i.routes||[]).find(l=>l.route===t),s=Ht(n,e),r=ds(n,e),o=r?`${e}
+${r}`:e,c=await openAppDialog({mode:"choice",title:s,message:o,choiceLayout:"list",choices:[{label:getUIText("play","Play"),value:"play"},{label:getUIText("drive_replay","Replay"),value:"drive_replay"},{label:getUIText("log_upload","Upload Logs"),value:"upload"},{label:`qcamera ${getUIText("download","Download")}`,value:"download_qcamera"},{label:`rlog ${getUIText("download","Download")}`,value:"download_rlog"},{label:`qlog ${getUIText("download","Download")}`,value:"download_qlog"}]});if(c==="drive_replay")a.activePlayerClose?.(),await _s(t,e);else if(c==="play"&&typeof a.activePlayerClose!="function")kt(t,e);else if(c==="upload")await Fe([e]);else if(c?.startsWith?.("download_")){let l=c.replace("download_","");window.open(he(`download/${encodeURIComponent(e)}`,l),"_blank","noopener")}}function Ts(t){let e=new Set;return String(t||"").split(",").forEach(a=>{let n=a.trim();if(!n)return;let s=n.match(/^(\d+)$/);if(s){e.add(Number.parseInt(s[1],10));return}let r=n.match(/^(\d+)\s*[-~]\s*(\d+)$/);if(!r)return;let o=Number.parseInt(r[1],10),c=Number.parseInt(r[2],10);o>c&&([o,c]=[c,o]),c-o>1e5&&(c=o+1e5);for(let l=o;l<=c;l+=1)e.add(l)}),e}async function Be(t){let e=[],a=new Set,n={},s=0;for(let o=0;o<1e3;o+=1){let c=await getJson(`/api/dashcam/segments/${encodeURIComponent(t)}?offset=${s}&limit=${Bn}&sort=${se()}`),l=Array.isArray(c.segments)?c.segments:[];Object.assign(n,c.segmentTimes||{});for(let d of l)d&&!a.has(d)&&(a.add(d),e.push(d));if(!c.hasMore||!l.length)break;s=c.nextOffset==null?e.length:Number(c.nextOffset)||e.length}let r=(i.routes||[]).find(o=>o.route===t);return r&&(r.segmentTimes={...r.segmentTimes||{},...n}),e}async function Ms(t){let e=(i.routes||[]).find(h=>h.route===t);if(!e)return;let a;try{a=await Be(t)}catch{a=E(e)}a.length||(a=E(e));let n=a.map(h=>Y(h)),s=n.length?Math.min(...n):0,r=n.length?Math.max(...n):0,o=n.length?s===r?`${s}`:`${s}–${r}`:"",c=o?`${getUIText("select_range","Select range")} (${o})`:getUIText("select_range","Select range"),l=await appPrompt("",{title:c,placeholder:getUIText("range_input_hint","1, 1-2, 1,2,3,4")});if(l==null)return;let d=Ts(l);if(!d.size){showAppToast(getUIText("range_invalid","Enter a valid range"),{tone:"error"});return}let u=(i.routes||[]).find(h=>h.route===t);if(!u)return;let g=new Map;a.forEach(h=>g.set(Y(h),h));let m=0;d.forEach(h=>{let p=g.get(h);p&&!i.selected.has(p)&&(i.selected.add(p),m+=1)}),a.length>E(u).length&&(u.segmentFolders=G(E(u),a),u.segmentCount=Math.max(V(u),u.segmentFolders.length),u.segmentsHasMore=u.segmentFolders.length{let o=i.expanded.has(r.route)||isCompactLandscapeMode(),c=E(r).length;if(o&&c>0)gt(r)?n.push(r):r.segmentFolders=G(E(r),[],e);else{let l=V(r);r.segmentFolders=[],r.segmentsNextOffset=0,r.segmentsHasMore=l>0}});let s=document.getElementById("dashcamRoutes");i.signature=ye(a),s&&(s.dataset.signature=""),C()&&D({animate:!1}),requestAnimationFrame(()=>ze()),n.length&&(await Promise.all(n.map(async r=>{let o=[];try{o=await Be(r.route)}catch{o=[]}se()===e&&(o.length?(r.segmentFolders=G(o,[],e),r.segmentCount=Math.max(V(r),r.segmentFolders.length),r.segmentsHasMore=r.segmentFolders.lengthr.route===t);if(!a)return;if(e){for(let r of E(a))i.selected.delete(r);ft(t)||D({animate:!1});return}let n;try{n=await Be(t)}catch{n=E(a)}let s=(i.routes||[]).find(r=>r.route===t);if(s){n.length||(n=E(s)),n.length>E(s).length&&(s.segmentFolders=G(E(s),n),s.segmentCount=Math.max(V(s),s.segmentFolders.length),s.segmentsHasMore=s.segmentFolders.length{C()&&(a.scrollTop=s,requestAnimationFrame(()=>{C()&&(a.scrollTop=s,n==="dashcam"&&typeof be=="function"&&be(),n==="screen"&&typeof Ye=="function"&&Ye())}))})}function un(){document.getElementById("logsTabs")?.setAttribute("aria-label",getUIText("logs","Logs"))}function dn(){let t=document.getElementById("logsMenuButton"),e=getUIText("logs_menu","Log menu");t?.setAttribute("aria-label",e),t?.setAttribute("title",e)}function Es(){let t=typeof se=="function"?se():"asc";return[{heading:getUIText("logs_sort","Sort")},{label:getUIText("sort_ascending","Sort: ascending"),value:`${jt}:asc`,selected:t==="asc"},{label:getUIText("sort_descending","Sort: descending"),value:`${jt}:desc`,selected:t==="desc"},{heading:getUIText("recent_log_upload","Upload recent logs")},...As.map(e=>({label:getUIText("upload_recent_logs","Upload recent {count}",{count:e}),value:`${cn}:${e}`}))]}async function Is(t){let[e,a]=String(t||"").split(":");e===jt?await an(a==="desc"?"desc":"asc"):e===cn&&await tn(Number(a)||0)}async function $s(){if(Ft)return;let t=document.getElementById("logsMenu");Ft=!0,t?.classList.add("is-open");let e=null;try{e=await openAppDialog({mode:"choice",title:getUIText("logs_menu","Log menu"),choiceLayout:"list",choices:Es()})}finally{Ft=!1,t?.classList.remove("is-open")}await Is(e)}function Cs(){let t=document.getElementById("logsMenuButton");t&&(dn(),t.dataset.bound!=="1"&&(t.dataset.bound="1",t.addEventListener("click",()=>{$s().catch(()=>{})})))}function Xe(t){let e=Number(t||0);if(!Number.isFinite(e)||e<=0)return"";let a=Math.max(0,Math.floor(Date.now()/1e3)-Math.floor(e));return a<60?getUIText("just_now","just now"):a<3600?getUIText("minutes_ago","{count} min ago",{count:Math.floor(a/60)}):a<86400?getUIText("hours_ago","{count} hr ago",{count:Math.floor(a/3600)}):getUIText("days_ago","{count} days ago",{count:Math.floor(a/86400)})}function Ze(t){let e=String(t||"").trim();if(!e)return"";if(/^(방금\s*전|just\s*now)$/i.test(e))return getUIText("just_now","just now");let a=e.match(/^(\d+)\s*(?:분\s*전|min(?:ute)?s?\s*ago)$/i);if(a)return getUIText("minutes_ago","{count} min ago",{count:a[1]});let n=e.match(/^(\d+)\s*(?:시간\s*전|hr?s?\s*ago|hour?s?\s*ago)$/i);if(n)return getUIText("hours_ago","{count} hr ago",{count:n[1]});let s=e.match(/^(\d+)\s*(?:일\s*전|day?s?\s*ago)$/i);return s?getUIText("days_ago","{count} days ago",{count:s[1]}):e}function le(t){let e=Number(t)||0;return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:e<1024*1024*1024?`${(e/(1024*1024)).toFixed(1)} MB`:`${(e/(1024*1024*1024)).toFixed(1)} GB`}var Rs=8,Us=900;function on(t){if(!t)return;let e=t.dataset?.src||"";if(!e)return;let a=t.closest(".dashcam-segment-thumb"),n=!!a&&!t.hasAttribute("data-fallback");a&&a.classList.add("is-thumb-loading");let s=()=>{a&&a.classList.remove("is-thumb-loading")},r=()=>{t.removeEventListener("load",o),t.removeEventListener("error",c)},o=()=>{r(),s()},c=()=>{r();let l=Number(t.dataset.thumbRetry||0);if(!n||l>=Rs){s();return}t.dataset.thumbRetry=String(l+1);let d=Math.round(Us*Math.pow(1.5,l));window.setTimeout(()=>{if(!t.isConnected||!C()){s();return}t.addEventListener("load",o),t.addEventListener("error",c),t.src=e+(e.includes("?")?"&":"?")+"ready="+(l+1)},d)};t.addEventListener("load",o),t.addEventListener("error",c),t.src=e,t.removeAttribute("data-src")}function ie(t){if(!C())return;let a=Array.from((t||document).querySelectorAll?.("img[data-src]")||[]);if(a.length){if(!("IntersectionObserver"in window)){a.forEach(on);return}oe||(oe=new IntersectionObserver(n=>{n.forEach(s=>{s.isIntersecting&&(oe.unobserve(s.target),on(s.target))})},{root:null,rootMargin:"720px 0px",threshold:.01})),a.forEach(n=>oe.observe(n))}}function Ns(){oe&&(oe.disconnect(),oe=null)}function Qe(t){!oe||!t||t.querySelectorAll?.("img[data-src]").forEach(e=>{oe.unobserve(e)})}function et(t="dashcam"){let e=t==="screen"?6:4,a=t==="screen"?"logs-loading-row":"logs-loading-card";return`${Array.from({length:e},(n,s)=>`
`).join("")}
`}function tt(t="dashcam"){let a=t==="screen"?getUIText("screenrecord_empty_title","No screen recordings"):getUIText("dashcam_empty_title","No dashcam records");return`
`}function at(t,e,a={}){let n=document.createElement("div"),s=String(a.kind||"video").replace(/[^a-z0-9_-]/gi,""),r=Ge(a.segments),o=r.length&&a.currentGroup?Zt({group:a.currentGroup,segments:r.map(y=>y.id),activeSegment:a.activeSegment,previousSegment:a.previousSegment}):null,i=typeof a.onSegmentSend=="function"||typeof a.onSend=="function",l=typeof a.onSegmentMenu=="function"||typeof a.onMenu=="function",u=i?``}function at(t,e,a={}){let n=document.createElement("div"),s=String(a.kind||"video").replace(/[^a-z0-9_-]/gi,""),r=Ve(a.segments),o=r.length&&a.currentGroup?ta({group:a.currentGroup,segments:r.map(y=>y.id),activeSegment:a.activeSegment,previousSegment:a.previousSegment}):null,c=typeof a.onSegmentSend=="function"||typeof a.onSend=="function",l=typeof a.onSegmentMenu=="function"||typeof a.onMenu=="function",d=c?``:"",d=l?``:"",u=l?``:"",m=r,g=new Map(m.map(y=>[y.id,y])),f=o?.snapshot().activeSegment||"",h=Object.freeze({id:"",name:t||"Video",timeLabel:"",title:t||"Video",subtitle:String(a.subtitle||""),src:String(e||""),thumbnailSrc:""}),p=g.get(f)||h;n.className=`dashcam-player-overlay dashcam-player-overlay--${s}`;let x=ea({currentGroup:a.currentGroup,labels:{browser:getUIText("segment_browser","Segments"),currentGroup:getUIText("current_group","Current group"),segmentCount:getUIText("segment_count","{count} segments")}});x.frame.innerHTML=`
+ `:"",g=r,m=new Map(g.map(y=>[y.id,y])),f=o?.snapshot().activeSegment||"",h=Object.freeze({id:"",name:t||"Video",timeLabel:"",title:t||"Video",subtitle:String(a.subtitle||""),src:String(e||""),thumbnailSrc:""}),p=m.get(f)||h;n.className=`dashcam-player-overlay dashcam-player-overlay--${s}`;let x=na({currentGroup:a.currentGroup,labels:{browser:getUIText("segment_browser","Segments"),currentGroup:getUIText("current_group","Current group"),segmentCount:getUIText("segment_count","{count} segments")}});x.frame.innerHTML=`
@@ -113,9 +117,9 @@ ${r}`:e,i=await openAppDialog({mode:"choice",title:s,message:o,choiceLayout:"lis
${escapeHtml(p.title)}
${escapeHtml(p.subtitle)}
- ${u}
${d}
+ ${u}
-
`;let $=x.frame.querySelector(".dashcam-player-title");$&&($.id="dashcamPlayerDialogTitle",x.dialog.setAttribute("aria-labelledby",$.id)),n.append(x.dialog);let S=n.querySelector("video");S.controls=!1,S.removeAttribute("controls");let T=n.querySelector(".dashcam-player-toast"),k=n.querySelector(".dashcam-player-subtitle"),w=e+(e.includes("?")?"&":"?")+"download=1",H=null,O=!0,v=null,Le=!1,Ie=!1,F=!1,de=0,$e=null,Be=null,ce=n.querySelector(".dashcam-player-transport"),ue=window.CarrotMediaTransport,X=null,me=null,Z=null,Ft=y=>{let E=String(y||"");return E+(E.includes("?")?"&":"?")+"download=1"},_e=()=>o?.snapshot().activeSegment||"",an=()=>{let y=Number(v?.duration??S.duration);return o&&typeof a.subtitleForSegmentDuration=="function"?a.subtitleForSegmentDuration(_e(),y)||p.subtitle:typeof a.subtitleForDuration=="function"&&a.subtitleForDuration(y)||p.subtitle},vt=()=>{if(!k)return;let y=an();k.textContent=y||"",k.hidden=!y};S.addEventListener("loadedmetadata",vt);let j=y=>{!T||O||!y||(T.textContent=y,T.classList.add("is-visible"),H&&window.clearTimeout(H),H=window.setTimeout(()=>T.classList.remove("is-visible"),850))},Ce=()=>{if(!F){F=!0;try{a.onClose?.(_e())}catch{}de+=1,H&&window.clearTimeout(H),$e?.dispose?.(),$e=null;try{v?.destroy?.()}catch{}n.remove()}},jt=y=>{Ie||typeof y!="function"||(Ie=!0,Promise.resolve().then(()=>y({close:Ce})).catch(E=>{typeof showAppToast=="function"&&showAppToast(E?.message||String(E),{tone:"error"})}).finally(()=>{Ie=!1}))},qt=()=>{if(!(!X||!Z)){if(o){let y=o.snapshot();X.disabled=!y.canMovePrevious,Z.disabled=!y.canMoveNext;return}X.disabled=typeof a.onPrevious!="function",Z.disabled=typeof a.onNext!="function"}},Fe=()=>{if(!me||!ue?.setActionState)return;let y=(typeof v?.paused=="boolean"?v.paused:S.paused)||(typeof v?.ended=="boolean"?v.ended:S.ended);ue.setActionState(me,y?"play":"pause",{label:y?getUIText("replay_play","Play replay"):getUIText("replay_pause","Pause replay")}),me.setAttribute("aria-pressed",String(!y))},nn=async()=>{try{let y=typeof v?.ended=="boolean"?v.ended:S.ended,E=typeof v?.paused=="boolean"?v.paused:S.paused;if(y&&(v?v.currentTime=0:S.currentTime=0),E||y){let M=v?.play?v.play():S.play();M&&typeof M.then=="function"&&await M}else v?.pause?v.pause():S.pause()}catch(y){typeof showAppToast=="function"&&showAppToast(y?.message||getUIText("playback_failed","Playback failed"),{tone:"error"})}finally{window.requestAnimationFrame(Fe)}},sn=y=>{let E=()=>{if(F||y!==de)return;let M=v?.play?v.play():S.play();M&&typeof M.catch=="function"&&M.catch(()=>{})};window.setTimeout(()=>{if(F||y!==de)return;let M=v?.media||S;M.readyState>=2?E():M.addEventListener("canplay",E,{once:!0})},0)},Gt=(y,E={})=>{if(!y?.src||F)return!1;let M=E.autoplay!==!1;p=y,de+=1;let B=de;return $&&($.textContent=p.title||p.name),vt(),v?(v.source={type:"video",title:p.title||p.name,sources:[{src:p.src,type:"video/mp4"}]},v.download=Ft(p.src)):(S.src=p.src,S.load()),M&&sn(B),!0},Vt=(y,E=!0)=>{if(!y?.changed)return!1;let M=g.get(y.state.activeSegment);return M?(Be?.setActive(y.state.activeSegment,!0),qt(),Gt(M,{autoplay:E}),a.onSegmentChange?.(y.state.activeSegment),!0):!1},rn=y=>o?Vt(o.select(y)):!1,Wt=y=>o?Vt(o.move(y)):!1;o&&x.segmentHost&&(Be=Qt(m,{activeSegmentId:_e(),labels:{segments:getUIText("segment_browser","Segments"),reading:getUIText("segment_reading","Reading"),recent:getUIText("segment_recently_read","Recently viewed")},onSelect:rn,statusFor:y=>o.statusFor(y)}),x.segmentHost.hidden=!1,x.segmentHost.append(Be.element)),ce&&ue?.createActionButton?(X=ue.createActionButton("previous",{label:getUIText("replay_previous_segment","Previous segment"),disabled:o?!o.snapshot().canMovePrevious:typeof a.onPrevious!="function",onActivate:()=>jt(o?()=>Wt(-1):a.onPrevious)}),me=ue.createActionButton("play",{playLabel:getUIText("replay_play","Play replay"),pauseLabel:getUIText("replay_pause","Pause replay"),onActivate:nn}),Z=ue.createActionButton("next",{label:getUIText("replay_next_segment","Next segment"),disabled:o?!o.snapshot().canMoveNext:typeof a.onNext!="function",onActivate:()=>jt(o?()=>Wt(1):a.onNext)}),ce.append(X,me,Z),["pointerdown","mousedown","touchstart"].forEach(y=>{ce.addEventListener(y,E=>E.stopPropagation(),{passive:!0})}),["play","pause","ended"].forEach(y=>{S.addEventListener(y,Fe)}),me.setAttribute("aria-keyshortcuts","Space K"),X.setAttribute("aria-keyshortcuts","Shift+P"),Z.setAttribute("aria-keyshortcuts","Shift+N"),Fe()):ce&&(ce.hidden=!0),$e=ue?.createInputController?.({media:S,stage:x.frame,rates:[.5,1,1.5,2],canInteract:()=>!F&&n.isConnected,canPrevious:()=>!!(X&&!X.disabled),canNext:()=>!!(Z&&!Z.disabled),onTogglePlayback:()=>me?.click(),onPrevious:()=>X?.click(),onNext:()=>Z?.click(),onRate:y=>{v?v.speed=y:S.playbackRate=y},onSeek:Fe,blockedPointerSelector:["button","input","select","a",".plyr__controls",".dashcam-player-top",".dashcam-player-transport"].join(", ")})||null,n.addEventListener("click",y=>{y.target===n&&Ce()}),n.querySelector(".dashcam-player-close")?.addEventListener("click",Ce);let Jt=(y,E,M)=>{if(y.stopPropagation(),Le||typeof E!="function")return;Le=!0;let B=y.currentTarget;B.disabled=!0,n.classList.add(M),Promise.resolve().then(()=>E({close:Ce})).catch(Q=>{typeof showAppToast=="function"&&showAppToast(Q?.message||String(Q),{tone:"error"})}).finally(()=>{n.classList.remove(M),B.disabled=!1,Le=!1})};return n.querySelector(".dashcam-player-send")?.addEventListener("click",y=>{let E=_e(),M=typeof a.onSegmentSend=="function"?({close:B}={})=>a.onSegmentSend(E,{close:B}):a.onSend;Jt(y,M,"is-action-open")}),n.querySelector(".dashcam-player-menu")?.addEventListener("click",y=>{let E=_e(),M=typeof a.onSegmentMenu=="function"?({close:B}={})=>a.onSegmentMenu(E,{close:B}):a.onMenu;Jt(y,M,"is-menu-open")}),document.body.appendChild(n),requestAnimationFrame(()=>{if(!F){n.classList.add("is-open");try{v=new Plyr(S,{controls:["play","progress","current-time","duration","settings","fullscreen","download"],hideControls:!1,seekTime:5,settings:["speed"],speed:{selected:1,options:[.5,1,1.5,2]},keyboard:{focused:!0,global:!1},fullscreen:{enabled:!0,fallback:!0,iosNative:!0},urls:{download:Ft(p.src)}}),S.controls=!1,S.removeAttribute("controls"),v.source={type:"video",title:p.title||p.name,sources:[{src:p.src,type:"video/mp4"}]},v.once("ready",()=>{let y=v.elements?.container||n;ce&&ce.parentElement!==y&&y.appendChild(ce),((M,B)=>{y.querySelectorAll(M).forEach(Q=>Q.addEventListener("click",()=>j(B)))})('[data-plyr="download"]',`⤓ ${getUIText("download","Download")}`),y.addEventListener("keydown",M=>{M.key==="ArrowLeft"?j(`⏪ ${getUIText("rewind_5","5s")}`):M.key==="ArrowRight"&&j(`${getUIText("forward_5","5s")} ⏩`)}),v.on("play",()=>j(`▶ ${getUIText("play","Play")}`)),v.on("pause",()=>j(`⏸ ${getUIText("pause","Pause")}`)),v.on("ended",()=>j(getUIText("ended","End"))),v.on("ratechange",()=>j(`⚡ ${v.speed}x`)),v.on("enterfullscreen",()=>j(`⛶ ${getUIText("fullscreen","Fullscreen")}`)),v.on("exitfullscreen",()=>j(getUIText("fullscreen_exit","Exit fullscreen"))),S.addEventListener("enterpictureinpicture",()=>j("⊞ PiP")),S.addEventListener("leavepictureinpicture",()=>j(`⊟ ${getUIText("pip_exit","Exit PiP")}`)),window.setTimeout(()=>{O=!1},350)})}catch{S.controls=!0,S.setAttribute("controls",""),S.src=p.src}}}),Object.freeze({close:Ce,isOpen:()=>!F&&n.isConnected,updateSegments:y=>{if(!o||F)return null;let E=Ge(y);if(!E.length)return o.snapshot();let M=_e(),B=p.src;m=E,g=new Map(m.map(qe=>[qe.id,qe]));let Q=o.replaceSegments(m.map(qe=>qe.id),{activeSegment:M}),je=g.get(Q.state.activeSegment);return Be?.render(m,Q.state.activeSegment),qt(),je&&(Q.state.activeSegment!==M||je.src!==B?Gt(je,{autoplay:!1}):(p=je,$&&($.textContent=p.title||p.name),vt())),Q.state}})}function St(t,e={}){let a=Ya(t),n=e.load!==!1;a!==U&&bt(U),U=a;for(let s of ls()){let r=Ya(s.dataset.logsTab)===U;s.classList.toggle("is-active",r),s.setAttribute("aria-selected",r?"true":"false");let o=document.getElementById(s.getAttribute("aria-controls")||"");o&&(o.hidden=!r)}Qa?.sync(),n&&(U==="screen"&&!b.initialized?(b.initialized=!0,te().catch(()=>{})):U==="screen"?(q(),te({silent:!0}).catch(()=>{})):c.initialized&&ve({silent:!0}).catch(()=>{})),e.restoreScroll!==!1&&Re(U)}function ys(t){t&&(en(),t.dataset.bound!=="1"&&(t.dataset.bound="1",t.addEventListener("click",e=>{let a=e.target?.closest?.(zt);a&&t.contains(a)&&St(a.dataset.logsTab)}),Qa=window.CarrotUI?.segmentedControl?.create(t,{itemSelector:zt,onActivate:e=>St(e?.dataset?.logsTab)})||null))}function bs(t){(t?.detail?.page||"")!=="logs"&&(bt(U),Ut(),c.loadSeq+=1,b.loadSeq+=1,c.loading=!1,c.loadingMore=!1,c.loadingSegments?.clear?.(),J(!1),b.loading=!1,c.scrollBusy=!1,c.scrollTimer&&(window.clearTimeout(c.scrollTimer),c.scrollTimer=null),c.layoutTimer&&(window.clearTimeout(c.layoutTimer),c.layoutTimer=null),hs())}function Ss(){let t=document.getElementById("logsTabs"),e=document.getElementById("dashcamRoutes"),a=document.getElementById("screenrecordVideos");gs(),ys(t),c.layoutBound||(c.layoutBound=!0,c.landscape=isCompactLandscapeMode(),c.layoutKey=mt(),window.addEventListener("carrot:pagechange",bs),window.addEventListener("carrot:languagechange",()=>{tn(),en(),c.signature="",b.signature="",c.routeHeights=Object.create(null);let n=document.getElementById("dashcamRoutes");n&&(n.dataset.signature="");let s=document.getElementById("screenrecordVideos");s&&(s.dataset.signature=""),C()&&(N({animate:!1}),typeof q=="function"&&q({animate:!1}))}),window.addEventListener("resize",()=>{CURRENT_PAGE==="logs"&&(c.layoutTimer&&window.clearTimeout(c.layoutTimer),c.layoutTimer=window.setTimeout(()=>{if(c.layoutTimer=null,!C())return;let n=isCompactLandscapeMode(),s=mt();if(c.layoutKey===s)return;c.landscape=n,c.layoutKey=s,c.routeHeights=Object.create(null),c.routeHeight=ut();let r=document.getElementById("dashcamRoutes");r&&(r.dataset.signature=""),N({animate:!1}),typeof q=="function"&&q({preserve:!0,animate:!1})},120))},{passive:!0})),e&&e.dataset.bound!=="1"&&(e.dataset.bound="1",e.addEventListener("scroll",()=>{ht(),bt("dashcam"),Dt(e)&&Se(),Nt(e)},{passive:!0}),e.addEventListener("scroll",n=>{let s=n.target?.closest?.(".dashcam-segment-list");!s||s===e||Da(s)},{passive:!0,capture:!0}),e.addEventListener("click",n=>{let s=n.target?.closest?.("[data-action]");if(!s)return;let r=s.dataset.action,o=s.dataset.route||"",i=s.dataset.segment||"";if(r==="toggle-route")c.expanded.has(o)?c.expanded.delete(o):c.expanded.add(o),o&&c.routeHeights&&delete c.routeHeights[o],pt(o)||N({animate:!1});else if(r==="play")Ot(o,i);else if(r==="segment-menu")n.stopPropagation(),kt(o,i).catch(()=>{});else if(r==="route-menu")n.stopPropagation(),Ka(o).catch(()=>{});else if(r==="select-route"){let l=s.dataset.selected==="1";Ja(o,l).catch(()=>{})}else if(r==="upload-selected"){let l=c.routes.find(d=>d.route===o),u=Ee(l||{segmentFolders:[]});Pe(u).catch(()=>{})}}),e.addEventListener("change",n=>{let s=n.target;if(!s?.matches?.('input[data-action="select-segment"]'))return;let r=s.dataset.segment||"";s.checked?c.selected.add(r):c.selected.delete(r);let o=s.closest("[data-route-card]")?.dataset.routeCard||"";ft(o)||N({animate:!1})})),a&&a.dataset.bound!=="1"&&(a.dataset.bound="1",a.addEventListener("scroll",()=>{ht(),bt("screen"),Ke(),ra(a)&&te({silent:!0,append:!0}).catch(()=>{})},{passive:!0}),a.addEventListener("click",n=>{let s=n.target?.closest?.("[data-action]");if(s)if(s.dataset.action==="download-screenrecord"){let r=s.dataset.id||"";r&&window.open(Je("download",r),"_blank","noopener")}else s.dataset.action==="play-screenrecord"&&We(s.dataset.id||"",s.dataset.name||"")}))}function vs(){Ss(),St(U,{load:!1}),Ba(),Oa().catch(()=>{}),dt().catch(()=>{}),U==="screen"?b.initialized?(q({preserve:!0}),te({silent:!0}).catch(()=>{})):(b.initialized=!0,te().catch(()=>{})):c.initialized?(N({animate:!1,preserve:!0}),ve({silent:!0}).catch(()=>{})):(c.initialized=!0,ve().catch(()=>{}))}var _s=Object.freeze({init:vs,activateTab:St,dashcam:Object.freeze({load:ve,render:N}),screenrecord:Object.freeze({load:te,render:q})});globalThis.CarrotLogsRuntime=_s;})();
+ `;let $=x.frame.querySelector(".dashcam-player-title");$&&($.id="dashcamPlayerDialogTitle",x.dialog.setAttribute("aria-labelledby",$.id)),n.append(x.dialog);let b=n.querySelector("video");b.controls=!1,b.removeAttribute("controls");let T=n.querySelector(".dashcam-player-toast"),k=n.querySelector(".dashcam-player-subtitle"),w=e+(e.includes("?")?"&":"?")+"download=1",O=null,H=!0,_=null,Ie=!1,$e=!1,B=!1,ue=0,Ce=null,je=null,ce=n.querySelector(".dashcam-player-transport"),de=window.CarrotMediaTransport,X=null,me=null,Z=null,qt=y=>{let L=String(y||"");return L+(L.includes("?")?"&":"?")+"download=1"},ve=()=>o?.snapshot().activeSegment||"",mn=()=>{let y=Number(_?.duration??b.duration);return o&&typeof a.subtitleForSegmentDuration=="function"?a.subtitleForSegmentDuration(ve(),y)||p.subtitle:typeof a.subtitleForDuration=="function"&&a.subtitleForDuration(y)||p.subtitle},_t=()=>{if(!k)return;let y=mn();k.textContent=y||"",k.hidden=!y};b.addEventListener("loadedmetadata",_t);let j=y=>{!T||H||!y||(T.textContent=y,T.classList.add("is-visible"),O&&window.clearTimeout(O),O=window.setTimeout(()=>T.classList.remove("is-visible"),850))},Re=()=>{if(!B){B=!0;try{a.onClose?.(ve())}catch{}ue+=1,O&&window.clearTimeout(O),Ce?.dispose?.(),Ce=null;try{_?.destroy?.()}catch{}n.remove()}},Gt=y=>{$e||typeof y!="function"||($e=!0,Promise.resolve().then(()=>y({close:Re})).catch(L=>{typeof showAppToast=="function"&&showAppToast(L?.message||String(L),{tone:"error"})}).finally(()=>{$e=!1}))},Vt=()=>{if(!(!X||!Z)){if(o){let y=o.snapshot();X.disabled=!y.canMovePrevious,Z.disabled=!y.canMoveNext;return}X.disabled=typeof a.onPrevious!="function",Z.disabled=typeof a.onNext!="function"}},Ue=()=>{if(!me||!de?.setActionState)return;let y=(typeof _?.paused=="boolean"?_.paused:b.paused)||(typeof _?.ended=="boolean"?_.ended:b.ended);de.setActionState(me,y?"play":"pause",{label:y?getUIText("replay_play","Play replay"):getUIText("replay_pause","Pause replay")}),me.setAttribute("aria-pressed",String(!y))},Wt=()=>{try{_?.pause?_.pause():b.pause()}finally{window.requestAnimationFrame(Ue)}},gn=async()=>{try{let y=typeof _?.ended=="boolean"?_.ended:b.ended,L=typeof _?.paused=="boolean"?_.paused:b.paused;if(y&&(_?_.currentTime=0:b.currentTime=0),L||y){let M=_?.play?_.play():b.play();M&&typeof M.then=="function"&&await M}else Wt()}catch(y){typeof showAppToast=="function"&&showAppToast(y?.message||getUIText("playback_failed","Playback failed"),{tone:"error"})}finally{window.requestAnimationFrame(Ue)}},pn=y=>{let L=()=>{if(B||y!==ue)return;let M=_?.play?_.play():b.play();M&&typeof M.catch=="function"&&M.catch(()=>{})};window.setTimeout(()=>{if(B||y!==ue)return;let M=_?.media||b;M.readyState>=2?L():M.addEventListener("canplay",L,{once:!0})},0)},Jt=(y,L={})=>{if(!y?.src||B)return!1;let M=L.autoplay!==!1;p=y,ue+=1;let F=ue;return $&&($.textContent=p.title||p.name),_t(),_?(_.source={type:"video",title:p.title||p.name,sources:[{src:p.src,type:"video/mp4"}]},_.download=qt(p.src)):(b.src=p.src,b.load()),M&&pn(F),!0},Kt=(y,L=!0)=>{if(!y?.changed)return!1;let M=m.get(y.state.activeSegment);return M?(je?.setActive(y.state.activeSegment,!0),Vt(),Jt(M,{autoplay:L}),a.onSegmentChange?.(y.state.activeSegment),!0):!1},fn=y=>o?Kt(o.select(y)):!1,Yt=y=>o?Kt(o.move(y)):!1;o&&x.segmentHost&&(je=aa(g,{activeSegmentId:ve(),labels:{segments:getUIText("segment_browser","Segments"),reading:getUIText("segment_reading","Reading"),recent:getUIText("segment_recently_read","Recently viewed")},onSelect:fn,statusFor:y=>o.statusFor(y)}),x.segmentHost.hidden=!1,x.segmentHost.append(je.element)),ce&&de?.createActionButton?(X=de.createActionButton("previous",{label:getUIText("replay_previous_segment","Previous segment"),disabled:o?!o.snapshot().canMovePrevious:typeof a.onPrevious!="function",onActivate:()=>Gt(o?()=>Yt(-1):a.onPrevious)}),me=de.createActionButton("play",{playLabel:getUIText("replay_play","Play replay"),pauseLabel:getUIText("replay_pause","Pause replay"),onActivate:gn}),Z=de.createActionButton("next",{label:getUIText("replay_next_segment","Next segment"),disabled:o?!o.snapshot().canMoveNext:typeof a.onNext!="function",onActivate:()=>Gt(o?()=>Yt(1):a.onNext)}),ce.append(X,me,Z),["pointerdown","mousedown","touchstart"].forEach(y=>{ce.addEventListener(y,L=>L.stopPropagation(),{passive:!0})}),["play","pause","ended"].forEach(y=>{b.addEventListener(y,Ue)}),me.setAttribute("aria-keyshortcuts","Space K"),X.setAttribute("aria-keyshortcuts","Shift+P"),Z.setAttribute("aria-keyshortcuts","Shift+N"),Ue()):ce&&(ce.hidden=!0),Ce=de?.createInputController?.({media:b,stage:x.frame,rates:[.5,1,1.5,2],canInteract:()=>!B&&n.isConnected,canPrevious:()=>!!(X&&!X.disabled),canNext:()=>!!(Z&&!Z.disabled),onTogglePlayback:()=>me?.click(),onPrevious:()=>X?.click(),onNext:()=>Z?.click(),onRate:y=>{_?_.speed=y:b.playbackRate=y},onSeek:Ue,blockedPointerSelector:["button","input","select","a",".plyr__controls",".dashcam-player-top",".dashcam-player-transport"].join(", ")})||null,n.addEventListener("click",y=>{y.target===n&&Re()}),n.querySelector(".dashcam-player-close")?.addEventListener("click",Re);let Xt=(y,L,M)=>{if(y.stopPropagation(),Ie||typeof L!="function")return;Ie=!0;let F=y.currentTarget;F.disabled=!0,n.classList.add(M),Promise.resolve().then(()=>L({close:Re})).catch(Q=>{typeof showAppToast=="function"&&showAppToast(Q?.message||String(Q),{tone:"error"})}).finally(()=>{n.classList.remove(M),F.disabled=!1,Ie=!1})};return n.querySelector(".dashcam-player-send")?.addEventListener("click",y=>{let L=ve(),M=typeof a.onSegmentSend=="function"?({close:F}={})=>a.onSegmentSend(L,{close:F}):a.onSend;Wt(),Xt(y,M,"is-action-open")}),n.querySelector(".dashcam-player-menu")?.addEventListener("click",y=>{let L=ve(),M=typeof a.onSegmentMenu=="function"?({close:F}={})=>a.onSegmentMenu(L,{close:F}):a.onMenu;Xt(y,M,"is-menu-open")}),document.body.appendChild(n),requestAnimationFrame(()=>{if(!B){n.classList.add("is-open");try{_=new Plyr(b,{controls:["play","progress","current-time","duration","settings","fullscreen","download"],hideControls:!1,seekTime:5,settings:["speed"],speed:{selected:1,options:[.5,1,1.5,2]},keyboard:{focused:!0,global:!1},fullscreen:{enabled:!0,fallback:!0,iosNative:!0},urls:{download:qt(p.src)}}),b.controls=!1,b.removeAttribute("controls"),_.source={type:"video",title:p.title||p.name,sources:[{src:p.src,type:"video/mp4"}]},_.once("ready",()=>{let y=_.elements?.container||n;ce&&ce.parentElement!==y&&y.appendChild(ce),((M,F)=>{y.querySelectorAll(M).forEach(Q=>Q.addEventListener("click",()=>j(F)))})('[data-plyr="download"]',`⤓ ${getUIText("download","Download")}`),y.addEventListener("keydown",M=>{M.key==="ArrowLeft"?j(`⏪ ${getUIText("rewind_5","5s")}`):M.key==="ArrowRight"&&j(`${getUIText("forward_5","5s")} ⏩`)}),_.on("play",()=>j(`▶ ${getUIText("play","Play")}`)),_.on("pause",()=>j(`⏸ ${getUIText("pause","Pause")}`)),_.on("ended",()=>j(getUIText("ended","End"))),_.on("ratechange",()=>j(`⚡ ${_.speed}x`)),_.on("enterfullscreen",()=>j(`⛶ ${getUIText("fullscreen","Fullscreen")}`)),_.on("exitfullscreen",()=>j(getUIText("fullscreen_exit","Exit fullscreen"))),b.addEventListener("enterpictureinpicture",()=>j("⊞ PiP")),b.addEventListener("leavepictureinpicture",()=>j(`⊟ ${getUIText("pip_exit","Exit PiP")}`)),window.setTimeout(()=>{H=!1},350)})}catch{b.controls=!0,b.setAttribute("controls",""),b.src=p.src}}}),Object.freeze({close:Re,isOpen:()=>!B&&n.isConnected,updateSegments:y=>{if(!o||B)return null;let L=Ve(y);if(!L.length)return o.snapshot();let M=ve(),F=p.src;g=L,m=new Map(g.map(Ge=>[Ge.id,Ge]));let Q=o.replaceSegments(g.map(Ge=>Ge.id),{activeSegment:M}),qe=m.get(Q.state.activeSegment);return je?.render(g,Q.state.activeSegment),Vt(),qe&&(Q.state.activeSegment!==M||qe.src!==F?Jt(qe,{autoplay:!1}):(p=qe,$&&($.textContent=p.title||p.name),_t())),Q.state}})}function bt(t,e={}){let a=rn(t),n=e.load!==!1;a!==U&&St(U),U=a;for(let s of Ls()){let r=rn(s.dataset.logsTab)===U;s.classList.toggle("is-active",r),s.setAttribute("aria-selected",r?"true":"false");let o=document.getElementById(s.getAttribute("aria-controls")||"");o&&(o.hidden=!r)}ln?.sync(),n&&(U==="screen"&&!S.initialized?(S.initialized=!0,te().catch(()=>{})):U==="screen"?(q(),te({silent:!0}).catch(()=>{})):i.initialized&&_e({silent:!0}).catch(()=>{})),e.restoreScroll!==!1&&Ne(U)}function Ds(t){t&&(un(),t.dataset.bound!=="1"&&(t.dataset.bound="1",t.addEventListener("click",e=>{let a=e.target?.closest?.(Bt);a&&t.contains(a)&&bt(a.dataset.logsTab)}),ln=window.CarrotUI?.segmentedControl?.create(t,{itemSelector:Bt,onActivate:e=>bt(e?.dataset?.logsTab)})||null))}function Os(t){(t?.detail?.page||"")!=="logs"&&(St(U),Nt(),i.loadSeq+=1,S.loadSeq+=1,i.loading=!1,i.loadingMore=!1,i.loadingSegments?.clear?.(),J(!1),S.loading=!1,i.scrollBusy=!1,i.scrollTimer&&(window.clearTimeout(i.scrollTimer),i.scrollTimer=null),i.layoutTimer&&(window.clearTimeout(i.layoutTimer),i.layoutTimer=null),Ns())}function Hs(){let t=document.getElementById("logsTabs"),e=document.getElementById("dashcamRoutes"),a=document.getElementById("screenrecordVideos");Cs(),Ds(t),i.layoutBound||(i.layoutBound=!0,i.landscape=isCompactLandscapeMode(),i.layoutKey=mt(),window.addEventListener("carrot:pagechange",Os),window.addEventListener("carrot:languagechange",()=>{dn(),un(),i.signature="",S.signature="",i.routeHeights=Object.create(null);let n=document.getElementById("dashcamRoutes");n&&(n.dataset.signature="");let s=document.getElementById("screenrecordVideos");s&&(s.dataset.signature=""),C()&&(D({animate:!1}),typeof q=="function"&&q({animate:!1}))}),window.addEventListener("resize",()=>{CURRENT_PAGE==="logs"&&(i.layoutTimer&&window.clearTimeout(i.layoutTimer),i.layoutTimer=window.setTimeout(()=>{if(i.layoutTimer=null,!C())return;let n=isCompactLandscapeMode(),s=mt();if(i.layoutKey===s)return;i.landscape=n,i.layoutKey=s,i.routeHeights=Object.create(null),i.routeHeight=dt();let r=document.getElementById("dashcamRoutes");r&&(r.dataset.signature=""),D({animate:!1}),typeof q=="function"&&q({preserve:!0,animate:!1})},120))},{passive:!0})),e&&e.dataset.bound!=="1"&&(e.dataset.bound="1",e.addEventListener("scroll",()=>{ht(),St("dashcam"),Dt(e)&&be(),Ot(e)},{passive:!0}),e.addEventListener("scroll",n=>{let s=n.target?.closest?.(".dashcam-segment-list");!s||s===e||ja(s)},{passive:!0,capture:!0}),e.addEventListener("click",n=>{let s=n.target?.closest?.("[data-action]");if(!s)return;let r=s.dataset.action,o=s.dataset.route||"",c=s.dataset.segment||"";if(r==="toggle-route")i.expanded.has(o)?i.expanded.delete(o):i.expanded.add(o),o&&i.routeHeights&&delete i.routeHeights[o],pt(o)||D({animate:!1});else if(r==="play")kt(o,c);else if(r==="segment-menu")n.stopPropagation(),zt(o,c).catch(()=>{});else if(r==="route-menu")n.stopPropagation(),sn(o).catch(()=>{});else if(r==="select-route"){let l=s.dataset.selected==="1";nn(o,l).catch(()=>{})}else if(r==="upload-selected"){let l=i.routes.find(u=>u.route===o),d=Ee(l||{segmentFolders:[]});Fe(d).catch(()=>{})}}),e.addEventListener("change",n=>{let s=n.target;if(!s?.matches?.('input[data-action="select-segment"]'))return;let r=s.dataset.segment||"";s.checked?i.selected.add(r):i.selected.delete(r);let o=s.closest("[data-route-card]")?.dataset.routeCard||"";ft(o)||D({animate:!1})})),a&&a.dataset.bound!=="1"&&(a.dataset.bound="1",a.addEventListener("scroll",()=>{ht(),St("screen"),Ye(),ia(a)&&te({silent:!0,append:!0}).catch(()=>{})},{passive:!0}),a.addEventListener("click",n=>{let s=n.target?.closest?.("[data-action]");if(s)if(s.dataset.action==="download-screenrecord"){let r=s.dataset.id||"";r&&window.open(Ke("download",r),"_blank","noopener")}else s.dataset.action==="play-screenrecord"&&Je(s.dataset.id||"",s.dataset.name||"")}))}function ks(){Hs(),bt(U,{load:!1}),Ya(),Va().catch(()=>{}),ut().catch(()=>{}),U==="screen"?S.initialized?(q({preserve:!0}),te({silent:!0}).catch(()=>{})):(S.initialized=!0,te().catch(()=>{})):i.initialized?(D({animate:!1,preserve:!0}),_e({silent:!0}).catch(()=>{})):(i.initialized=!0,_e().catch(()=>{}))}var Ps=Object.freeze({init:ks,activateTab:bt,dashcam:Object.freeze({load:_e,render:D}),screenrecord:Object.freeze({load:te,render:q})});globalThis.CarrotLogsRuntime=Ps;})();
diff --git a/openpilot/selfdrive/carrot/web/js/realtime/mini_hud_mode.js b/openpilot/selfdrive/carrot/web/js/realtime/mini_hud_mode.js
index 22b23f3fe8..48fb6922dc 100644
--- a/openpilot/selfdrive/carrot/web/js/realtime/mini_hud_mode.js
+++ b/openpilot/selfdrive/carrot/web/js/realtime/mini_hud_mode.js
@@ -77,7 +77,6 @@
'[role="dialog"][aria-modal="true"]',
".training-guide-modal",
".dashcam-player-overlay.is-open",
- ".dashcam-upload-progress",
].join(",");
return Array.from(document.querySelectorAll(modalSelector)).some(isRendered);
}
diff --git a/openpilot/selfdrive/carrot/web/js/translations/en.js b/openpilot/selfdrive/carrot/web/js/translations/en.js
index 60838b97cd..1c6a638ed3 100644
--- a/openpilot/selfdrive/carrot/web/js/translations/en.js
+++ b/openpilot/selfdrive/carrot/web/js/translations/en.js
@@ -1232,6 +1232,16 @@ window.CarrotTranslations.register("en", {
log_upload: "Upload Logs",
log_upload_confirm: "Upload {count} logs to the Carrot server?",
upload_data_warning: "This upload may use mobile data depending on your network connection.",
+ upload_original_files_confirm: "Upload the original files from these segments to the Carrot server?",
+ upload_segments_label: "Segments",
+ upload_segment_progress: "Segments {current}/{total}",
+ upload_more_failed: "{count} more failed",
+ upload_segment_count: "{count} segments",
+ upload_kind_count: "{name} {count}",
+ upload_summary_label: "Upload summary",
+ upload_total_count: "{count} files total",
+ upload_total_files: "Total {count} files · {size}",
+ upload_summary_unavailable: "Upload information is unavailable.",
upload_file_count: "{count} files",
upload_files_unknown: "files unknown",
upload_size_unknown: "size unknown",
@@ -1241,6 +1251,18 @@ window.CarrotTranslations.register("en", {
upload_complete_count: "Upload complete {uploaded}/{total}",
upload_canceled: "Upload canceled.",
upload_canceling: "Canceling...",
+ upload_send: "Send",
+ upload_progress_label: "Log upload progress",
+ upload_phase_queued: "Waiting to upload",
+ upload_phase_preparing: "Preparing original files",
+ upload_phase_uploading: "Uploading original files",
+ upload_phase_notifying: "Sending completion notice",
+ upload_phase_canceling: "Canceling upload",
+ upload_phase_complete: "Upload complete",
+ upload_phase_canceled: "Upload canceled",
+ upload_phase_failed: "Upload failed",
+ upload_status_complete: "Complete",
+ upload_status_failed: "Failed",
upload_already_running: "Upload already running.",
dashcam_load_failed: "Failed to load dashcam list",
screenrecord_empty: "No screen recordings found.",
diff --git a/openpilot/selfdrive/carrot/web/js/translations/ko.js b/openpilot/selfdrive/carrot/web/js/translations/ko.js
index 67c587dc69..7078426f83 100644
--- a/openpilot/selfdrive/carrot/web/js/translations/ko.js
+++ b/openpilot/selfdrive/carrot/web/js/translations/ko.js
@@ -222,7 +222,7 @@ window.CarrotTranslations.register("ko", {
reboot: "재부팅",
backup: "Backup",
restore: "Restore",
- copy: "Copy",
+ copy: "복사",
view: "View",
device_info: "기기 정보",
device_tab_error: "기기 정보를 불러오지 못했습니다.",
@@ -1230,6 +1230,16 @@ window.CarrotTranslations.register("ko", {
log_upload: "로그 전송",
log_upload_confirm: "당근 서버에 {count}개 로그를 전송할까요?",
upload_data_warning: "네트워크 연결 상태에 따라 모바일 데이터가 사용될 수 있습니다.",
+ upload_original_files_confirm: "다음 세그먼트의 원본 파일을 당근 서버에 전송할까요?",
+ upload_segments_label: "세그먼트",
+ upload_segment_progress: "세그먼트 {current}/{total}",
+ upload_more_failed: "외 {count}개 실패",
+ upload_segment_count: "세그먼트 {count}개",
+ upload_kind_count: "{name} {count}개",
+ upload_summary_label: "전송 요약",
+ upload_total_count: "총 {count}개",
+ upload_total_files: "총 {count}개 · {size}",
+ upload_summary_unavailable: "전송 정보를 불러올 수 없습니다.",
upload_file_count: "{count}개 파일",
upload_files_unknown: "파일 수 알 수 없음",
upload_size_unknown: "용량 알 수 없음",
@@ -1239,6 +1249,18 @@ window.CarrotTranslations.register("ko", {
upload_complete_count: "전송 완료 {uploaded}/{total}",
upload_canceled: "전송이 취소되었습니다.",
upload_canceling: "취소 중...",
+ upload_send: "전송",
+ upload_progress_label: "로그 전송 진행률",
+ upload_phase_queued: "전송 대기 중",
+ upload_phase_preparing: "원본 파일 준비 중",
+ upload_phase_uploading: "원본 파일 전송 중",
+ upload_phase_notifying: "전송 완료 알림 중",
+ upload_phase_canceling: "전송 취소 중",
+ upload_phase_complete: "전송 완료",
+ upload_phase_canceled: "전송 취소됨",
+ upload_phase_failed: "전송 실패",
+ upload_status_complete: "완료",
+ upload_status_failed: "실패",
upload_already_running: "이미 로그 전송 중입니다.",
dashcam_load_failed: "대시캠 목록 로드 실패",
screenrecord_empty: "화면녹화 폴더/영상이 없습니다.",
diff --git a/openpilot/selfdrive/carrot/web/js/translations/zh.js b/openpilot/selfdrive/carrot/web/js/translations/zh.js
index 4d30347eb4..8954ce65c5 100644
--- a/openpilot/selfdrive/carrot/web/js/translations/zh.js
+++ b/openpilot/selfdrive/carrot/web/js/translations/zh.js
@@ -222,7 +222,7 @@ window.CarrotTranslations.register("zh", {
reboot: "重启",
backup: "备份设置",
restore: "恢复设置",
- copy: "Copy",
+ copy: "复制",
view: "View",
device_info: "设备信息",
device_tab_error: "设备信息加载失败。",
@@ -1167,6 +1167,16 @@ window.CarrotTranslations.register("zh", {
log_upload: "发送日志",
log_upload_confirm: "要向 Carrot 服务器发送 {count} 个日志吗?",
upload_data_warning: "This upload may use mobile data depending on your network connection.",
+ upload_original_files_confirm: "要将这些片段的原始文件上传到 Carrot 服务器吗?",
+ upload_segments_label: "片段",
+ upload_segment_progress: "片段 {current}/{total}",
+ upload_more_failed: "另有 {count} 个失败",
+ upload_segment_count: "{count} 个片段",
+ upload_kind_count: "{name} {count} 个",
+ upload_summary_label: "发送摘要",
+ upload_total_count: "共 {count} 个文件",
+ upload_total_files: "共 {count} 个文件 · {size}",
+ upload_summary_unavailable: "无法获取上传信息。",
upload_file_count: "{count} files",
upload_files_unknown: "files unknown",
upload_size_unknown: "size unknown",
@@ -1176,6 +1186,18 @@ window.CarrotTranslations.register("zh", {
upload_complete_count: "发送完成 {uploaded}/{total}",
upload_canceled: "发送已取消。",
upload_canceling: "正在取消...",
+ upload_send: "发送",
+ upload_progress_label: "日志发送进度",
+ upload_phase_queued: "等待发送",
+ upload_phase_preparing: "正在准备原始文件",
+ upload_phase_uploading: "正在发送原始文件",
+ upload_phase_notifying: "正在发送完成通知",
+ upload_phase_canceling: "正在取消发送",
+ upload_phase_complete: "发送完成",
+ upload_phase_canceled: "发送已取消",
+ upload_phase_failed: "发送失败",
+ upload_status_complete: "完成",
+ upload_status_failed: "失败",
upload_already_running: "日志发送已在进行中。",
dashcam_load_failed: "行车记录列表加载失败",
screenrecord_empty: "没有屏幕录制视频。",
diff --git a/openpilot/selfdrive/carrot/web/src/entries/design_system.css b/openpilot/selfdrive/carrot/web/src/entries/design_system.css
index b5135e1728..c7b7a9e858 100644
--- a/openpilot/selfdrive/carrot/web/src/entries/design_system.css
+++ b/openpilot/selfdrive/carrot/web/src/entries/design_system.css
@@ -2,6 +2,7 @@
@import "../ui/primitives/controls.css";
@import "../ui/primitives/chip.css";
@import "../ui/components/dialog/style.css";
+@import "../ui/components/progress/style.css";
@import "../ui/components/menu/style.css";
@import "../ui/components/popover/style.css";
@import "../ui/components/feedback/style.css";
diff --git a/openpilot/selfdrive/carrot/web/src/features/logs/dashcam.js b/openpilot/selfdrive/carrot/web/src/features/logs/dashcam.js
index f38b6faaf5..99b42c544c 100644
--- a/openpilot/selfdrive/carrot/web/src/features/logs/dashcam.js
+++ b/openpilot/selfdrive/carrot/web/src/features/logs/dashcam.js
@@ -20,6 +20,11 @@ import {
syncLogsMenu,
unobserveLogsLazyImages,
} from "./runtime.js";
+import {
+ createDashcamUploadProgressModel,
+ dashcamUploadProgressState,
+} from "./upload_progress.js";
+import { dashcamUploadStats } from "./upload_summary.js";
// Logs page — Dashcam tab.
// Route + segment virtual listing, FFmpeg thumb/preview lazy load,
@@ -36,6 +41,10 @@ const DASHCAM_SEGMENT_SECONDS = 60;
const DASHCAM_REPLAY_TIME_TOLERANCE_SECONDS = 120;
const DASHCAM_LOAD_AHEAD_VIEWPORTS = 1.5;
const DASHCAM_ROUTE_WINDOW_OVERSCAN_VIEWPORTS = 1.25;
+const DASHCAM_UPLOAD_POLL_INTERVAL_MS = 160;
+const DASHCAM_UPLOAD_PROGRESS_MIN_VISIBLE_MS = 800;
+const DASHCAM_UPLOAD_PROGRESS_FINISH_HOLD_MS = 240;
+const DASHCAM_UPLOAD_RESULT_VISIBLE_LIMIT = 5;
let dashcamUploadActiveJobId = null;
let dashcamUploadResumePromise = null;
let dashcamReadStateLoadPromise = null;
@@ -1189,8 +1198,7 @@ function openDashcamPlayer(route, segment) {
return formatDashcamTimeRange(targetEndEpoch - seconds, targetEndEpoch);
},
onSegmentSend: (target) => uploadDashcamSegments([target], {
- confirm: false,
- showProgress: false,
+ showProgress: true,
showResult: true,
showSuccessToast: false,
}),
@@ -1239,18 +1247,6 @@ async function openDashcamDriveReplay(route, segment) {
await startPromise;
}
-function dashcamUploadStats(items) {
- const list = Array.isArray(items) ? items : [];
- return list.reduce((stats, item) => {
- const files = Array.isArray(item?.files) ? item.files : [];
- const totalSize = Number(item?.totalSize) || files.reduce((sum, file) => sum + (Number(file?.size) || 0), 0);
- stats.segments += 1;
- stats.files += files.length;
- stats.bytes += totalSize;
- return stats;
- }, { segments: 0, files: 0, bytes: 0 });
-}
-
function dashcamUploadSummaryLabel(stats) {
const fileCount = Number(stats?.files || 0);
const bytes = Number(stats?.bytes || 0);
@@ -1261,15 +1257,82 @@ function dashcamUploadSummaryLabel(stats) {
return `${fileLabel} · ${sizeLabel}`;
}
+function dashcamUploadConfirmHtml(stats) {
+ const segmentCountLabel = getUIText("upload_segment_count", "{count} segments", {
+ count: Number(stats?.segments || 0),
+ });
+ const qcameraLabel = getUIText("upload_kind_count", "{name} {count}", {
+ name: "qcamera",
+ count: Number(stats?.qcamera || 0),
+ });
+ const rlogLabel = getUIText("upload_kind_count", "{name} {count}", {
+ name: "rlog",
+ count: Number(stats?.rlog || 0),
+ });
+ const totalCountLabel = getUIText("upload_total_count", "{count} files total", {
+ count: Number(stats?.files || 0),
+ });
+ const bytes = Number(stats?.bytes || 0);
+ const sizeLabel = bytes > 0 ? formatLogBytes(bytes) : getUIText("upload_size_unknown", "size unknown");
+ return `
+
+ ${escapeHtml(segmentCountLabel)}
+ ${escapeHtml(`${qcameraLabel} · ${rlogLabel}`)}
+
+
+ ${escapeHtml(sizeLabel)}
+ ${escapeHtml(totalCountLabel)}
+
+ `;
+}
+
function dashcamUploadResultHtml(result) {
- const text = String(result?.shareText || result?.message || "");
- const stats = dashcamUploadStats(result?.results || []);
- return `
-
-
${escapeHtml(getUIText("upload_count", "Upload {uploaded}/{total}", { uploaded: Number(result?.uploaded || 0), total: Number(result?.total || 0) }))}
-
${escapeHtml(dashcamUploadSummaryLabel(stats))}
+ const results = Array.isArray(result?.results) ? result.results : [];
+ const stats = dashcamUploadStats(results);
+ const failedResults = results.filter((item) => item?.ok !== true);
+ const visibleResults = results.length <= DASHCAM_UPLOAD_RESULT_VISIBLE_LIMIT
+ ? results
+ : failedResults.slice(0, DASHCAM_UPLOAD_RESULT_VISIBLE_LIMIT);
+ const hiddenFailureCount = Math.max(0, failedResults.length - visibleResults.length);
+ const segmentRows = visibleResults
+ .map((item) => {
+ const ok = item?.ok === true;
+ const status = ok
+ ? getUIText("upload_status_complete", "Complete")
+ : getUIText("upload_status_failed", "Failed");
+ return `
+ ${escapeHtml(String(item?.segment || ""))}
+ ${escapeHtml(status)}
+
`;
+ })
+ .join("");
+ const overflowRow = hiddenFailureCount > 0
+ ? `
${escapeHtml(getUIText("upload_more_failed", "{count} more failed", {
+ count: hiddenFailureCount,
+ }))}
`
+ : "";
+ const resultList = segmentRows || overflowRow
+ ? `
${segmentRows}${overflowRow}
`
+ : "";
+ const qcameraLabel = getUIText("upload_kind_count", "{name} {count}", {
+ name: "qcamera",
+ count: Number(stats.qcamera || 0),
+ });
+ const rlogLabel = getUIText("upload_kind_count", "{name} {count}", {
+ name: "rlog",
+ count: Number(stats.rlog || 0),
+ });
+ return `
`;
}
@@ -1279,7 +1342,7 @@ async function showDashcamUploadResult(result) {
mode: "choice",
title: getUIText("log_upload_result", "Upload Result"),
html: true,
- messageHtml: `
${dashcamUploadResultHtml(result)}
`,
+ messageHtml: dashcamUploadResultHtml(result),
cancelLabel: getUIText("close", "Close"),
copyText: text,
copyLabel: getUIText("copy", "Copy"),
@@ -1301,82 +1364,121 @@ async function cancelDashcamUploadJob(jobId) {
return postJson("/api/dashcam/upload/cancel", { id: jobId });
}
-function openDashcamUploadProgress(total, stats = null, options = {}) {
- const overlay = document.createElement("div");
- overlay.className = "dashcam-upload-progress";
- overlay.innerHTML = `
-
${escapeHtml(getUIText("log_uploading", "Uploading logs"))}
-
0/${Number(total || 0)}
-
-
${escapeHtml(stats ? dashcamUploadSummaryLabel(stats) : getUIText("loading", "Loading..."))}
-
-
-
-
`;
- document.body.appendChild(overlay);
- document.body.classList.add("dialog-open");
- requestAnimationFrame(() => overlay.classList.add("is-open"));
- const message = overlay.querySelector(".dashcam-upload-progress__message");
- const summary = overlay.querySelector(".dashcam-upload-progress__summary");
- const bar = overlay.querySelector(".dashcam-upload-progress__bar span");
- const cancelButton = overlay.querySelector(".dashcam-upload-progress__cancel");
- let closed = false;
- let cancelHandler = typeof options.onCancel === "function" ? options.onCancel : null;
- if (cancelButton) {
- cancelButton.onclick = async () => {
- if (!cancelHandler || cancelButton.disabled) return;
- cancelButton.disabled = true;
- cancelButton.textContent = getUIText("upload_canceling", "Canceling...");
- try {
- await cancelHandler();
- } catch (e) {
- cancelButton.disabled = false;
- cancelButton.textContent = options.cancelLabel || getUIText("cancel", "Cancel");
- showAppToast(e?.message || getUIText("error", "Error"), { tone: "error", duration: 3600 });
- }
- };
+const DASHCAM_UPLOAD_PHASE_TEXT = Object.freeze({
+ queued: ["upload_phase_queued", "Waiting to upload"],
+ preparing: ["upload_phase_preparing", "Preparing files"],
+ uploading: ["upload_phase_uploading", "Uploading files"],
+ notifying: ["upload_phase_notifying", "Sending completion notice"],
+ canceling: ["upload_phase_canceling", "Canceling upload"],
+ complete: ["upload_phase_complete", "Upload complete"],
+ canceled: ["upload_phase_canceled", "Upload canceled"],
+ failed: ["upload_phase_failed", "Upload failed"],
+});
+
+function dashcamUploadPhaseLabel(phase, status = "") {
+ let normalized = String(phase || "").trim().toLowerCase();
+ if (!DASHCAM_UPLOAD_PHASE_TEXT[normalized]) {
+ normalized = status === "failed"
+ ? "failed"
+ : status === "canceled"
+ ? "canceled"
+ : status === "done"
+ ? "complete"
+ : "uploading";
+ }
+ const [key, fallback] = DASHCAM_UPLOAD_PHASE_TEXT[normalized];
+ return getUIText(key, fallback);
+}
+
+function dashcamUploadProgressView(snapshot = {}, totalFallback = 0, stats = null, stateOverride = null) {
+ const current = Math.max(0, Number(snapshot.step_current || 0));
+ const total = Math.max(0, Number(snapshot.step_total || totalFallback || 0));
+ const phase = String(snapshot.phase || "preparing").trim().toLowerCase();
+ const progressState = stateOverride || dashcamUploadProgressState(snapshot);
+ const showSegmentCount = total > 0 && (phase === "queued" || phase === "preparing" || phase === "uploading");
+ const segmentLabel = showSegmentCount
+ ? getUIText("upload_segment_progress", "Segments {current}/{total}", { current, total })
+ : "";
+ const message = [
+ dashcamUploadPhaseLabel(phase, snapshot.status),
+ segmentLabel,
+ ].filter(Boolean).join(" · ");
+ const bytesCurrent = Math.max(0, Number(snapshot.bytes_current || 0));
+ const bytesTotal = Math.max(0, Number(snapshot.bytes_total || stats?.bytes || 0));
+ const bytesPerSecond = Math.max(0, Number(snapshot.bytes_per_second || 0));
+ const isPreparing = phase === "queued" || phase === "preparing";
+ let summary = stats ? dashcamUploadSummaryLabel(stats) : getUIText("loading", "Loading...");
+ if (!isPreparing && bytesTotal > 0) {
+ summary = `${formatLogBytes(Math.min(bytesCurrent, bytesTotal))} / ${formatLogBytes(bytesTotal)}`;
+ if (bytesPerSecond > 0) summary += ` · ${formatLogBytes(bytesPerSecond)}/s`;
}
return {
- setCancelHandler(handler) {
- cancelHandler = typeof handler === "function" ? handler : null;
- if (cancelButton) cancelButton.hidden = !cancelHandler;
- },
- setCanceling(active) {
- if (!cancelButton) return;
- cancelButton.disabled = Boolean(active);
- cancelButton.textContent = active
- ? getUIText("upload_canceling", "Canceling...")
- : (options.cancelLabel || getUIText("cancel", "Cancel"));
- },
- setMessage(text) {
- if (message) message.textContent = text || "";
- },
- setProgress(percent) {
- if (!bar) return;
- const value = Number(percent);
- if (!Number.isFinite(value) || value <= 0) {
- bar.style.animation = "";
- bar.style.transform = "";
- bar.style.width = "";
- return;
- }
- bar.style.animation = "none";
- bar.style.transform = "none";
- bar.style.width = `${Math.max(4, Math.min(100, value))}%`;
+ message,
+ progressState,
+ summary,
+ };
+}
+
+function openDashcamUploadProgress(total, stats = null, options = {}) {
+ const openedAt = Date.now();
+ const model = createDashcamUploadProgressModel();
+ const initialSnapshot = {
+ phase: "queued",
+ step_current: 0,
+ step_total: total,
+ progress: 0,
+ };
+ const initialState = model.update(initialSnapshot);
+ const initial = dashcamUploadProgressView(
+ initialSnapshot,
+ total,
+ stats,
+ initialState.progressState,
+ );
+ const dialog = openAppProgressDialog({
+ title: getUIText("log_uploading", "Uploading logs"),
+ message: initial.message,
+ progressState: initial.progressState,
+ progressLabel: getUIText("upload_progress_label", "Upload progress"),
+ summary: initial.summary,
+ cancelLabel: options.cancelLabel || getUIText("cancel", "Cancel"),
+ cancelingLabel: getUIText("upload_canceling", "Canceling..."),
+ onCancel: options.onCancel,
+ });
+ return Object.freeze({
+ completion: dialog.completion,
+ setCancelHandler: dialog.setCancelHandler,
+ setCanceling: dialog.setCanceling,
+ setMessage: dialog.setMessage,
+ setProgressState: dialog.setProgressState,
+ update(snapshot, totalFallback = total) {
+ const modelState = model.update(snapshot);
+ if (!modelState.accepted) return;
+ const view = dashcamUploadProgressView(
+ snapshot,
+ totalFallback,
+ stats,
+ modelState.progressState,
+ );
+ dialog.setMessage(view.message);
+ dialog.setProgressState(view.progressState);
+ dialog.setSummary(view.summary);
},
setSummary(nextStats) {
- if (summary) summary.textContent = nextStats ? dashcamUploadSummaryLabel(nextStats) : "";
+ const summary = typeof nextStats === "string"
+ ? nextStats
+ : nextStats
+ ? dashcamUploadSummaryLabel(nextStats)
+ : "";
+ dialog.setSummary(summary);
},
- close() {
- if (closed) return;
- closed = true;
- overlay.classList.remove("is-open");
- window.setTimeout(() => {
- overlay.remove();
- syncModalBodyLock();
- }, 160);
+ async settle() {
+ const remaining = DASHCAM_UPLOAD_PROGRESS_MIN_VISIBLE_MS - (Date.now() - openedAt);
+ if (remaining > 0) await waitMs(remaining);
+ await waitMs(DASHCAM_UPLOAD_PROGRESS_FINISH_HOLD_MS);
},
- };
+ close: dialog.close,
+ });
}
function rememberDashcamUploadJob(jobId) {
@@ -1402,24 +1504,16 @@ function getRememberedDashcamUploadJob() {
}
}
-async function pollDashcamUploadJob(jobId, progress, totalFallback = 0, options = {}) {
+async function pollDashcamUploadJob(jobId, progress, totalFallback = 0) {
let snapshot = null;
while (jobId) {
- if (typeof options.isCanceled === "function" && options.isCanceled()) {
- throw makeDashcamUploadCanceledError();
- }
snapshot = await getJson(`/api/dashcam/upload/job?id=${encodeURIComponent(jobId)}`);
- const current = Number(snapshot.step_current || 0);
- const total = Number(snapshot.step_total || totalFallback || 0);
- const percent = Number(snapshot.progress);
- const message = snapshot.message || getUIText("log_uploading", "Uploading logs");
- progress.setMessage(`${current}/${total || totalFallback || 0} · ${message}`);
- progress.setProgress(percent);
+ progress.update(snapshot, totalFallback);
if (snapshot.status === "canceled" || snapshot.result?.canceled) {
throw makeDashcamUploadCanceledError();
}
if (snapshot.done) break;
- await waitMs(850);
+ await waitMs(DASHCAM_UPLOAD_POLL_INTERVAL_MS);
}
const result = snapshot?.result || {};
@@ -1459,11 +1553,8 @@ async function resumeDashcamUploadJobIfNeeded(options = {}) {
const progress = openDashcamUploadProgress(total, null, {
onCancel: async () => {
cancelRequested = true;
- progress.setCanceling(true);
- await cancelDashcamUploadJob(jobId);
- clearRememberedDashcamUploadJob(jobId);
- progress.close();
- showAppToast(getUIText("upload_canceled", "Upload canceled"), { duration: 2600 });
+ const cancelSnapshot = await cancelDashcamUploadJob(jobId);
+ progress.update(cancelSnapshot, total);
},
});
let activityId = typeof beginAppActivity === "function"
@@ -1471,18 +1562,26 @@ async function resumeDashcamUploadJobIfNeeded(options = {}) {
: null;
try {
- progress.setMessage(`${Number(snapshot.step_current || 0)}/${total} · ${snapshot.message || getUIText("log_uploading", "Uploading logs")}`);
- progress.setProgress(Number(snapshot.progress));
- const result = await pollDashcamUploadJob(jobId, progress, total, { isCanceled: () => cancelRequested });
+ progress.update(snapshot, total);
+ const result = await pollDashcamUploadJob(jobId, progress, total);
clearRememberedDashcamUploadJob(jobId);
- progress.setMessage(`${Number(result.uploaded || 0)}/${Number(result.total || total)}`);
- progress.setProgress(100);
- progress.setSummary(dashcamUploadStats(result.results || []));
- showAppToast(result.message || getUIText("upload_complete_count", "Upload complete {uploaded}/{total}", {
+ const resultStats = dashcamUploadStats(result.results || []);
+ progress.update({
+ phase: result.ok ? "complete" : "failed",
+ status: result.ok ? "done" : "failed",
+ step_current: Number(result.uploaded || 0),
+ step_total: Number(result.total || total),
+ progress: result.ok ? 100 : null,
+ bytes_current: resultStats.bytes,
+ bytes_total: resultStats.bytes,
+ }, total);
+ progress.setSummary(resultStats);
+ await progress.settle();
+ showAppToast(getUIText("upload_complete_count", "Upload complete {uploaded}/{total}", {
uploaded: result.uploaded || 0,
total: result.total || total,
}), { tone: result.ok ? "default" : "error", duration: 3600 });
- progress.close();
+ await progress.close();
if (activityId && typeof endAppActivity === "function") {
endAppActivity(activityId);
activityId = null;
@@ -1493,7 +1592,7 @@ async function resumeDashcamUploadJobIfNeeded(options = {}) {
progress.close();
clearRememberedDashcamUploadJob(jobId);
if (isDashcamUploadCanceledError(e)) {
- if (!cancelRequested) showAppToast(getUIText("upload_canceled", "Upload canceled"), { duration: 2600 });
+ showAppToast(getUIText("upload_canceled", "Upload canceled"), { duration: 2600 });
} else {
showAppToast(`${getUIText("log_upload", "Upload Logs")} ${getUIText("error", "Error")}: ${e.message || e}`, { tone: "error", duration: 4200 });
}
@@ -1523,27 +1622,49 @@ async function uploadDashcamSegments(segments, options = {}) {
showAppToast(getUIText("no_selected_segments", "No segments selected."), { tone: "error" });
return;
}
- let uploadStats = { segments: targets.length, files: 0, bytes: 0 };
+ let uploadStats = {
+ segments: targets.length,
+ segmentNames: targets,
+ files: 0,
+ qcamera: 0,
+ rlog: 0,
+ bytes: 0,
+ };
if (options.confirm !== false) {
try {
const summary = await postJson("/api/dashcam/upload/summary", { segments: targets });
- if (Array.isArray(summary?.summaries)) uploadStats = dashcamUploadStats(summary.summaries);
- } catch {}
- const confirmMessage = [
- getUIText("log_upload_confirm", `Upload ${targets.length} logs to the Carrot server?`, { count: targets.length }),
- dashcamUploadSummaryLabel(uploadStats),
- getUIText("upload_data_warning", "This upload may use mobile data depending on your network connection."),
- ].join("\n\n");
- const ok = await appConfirm(confirmMessage, { title: getUIText("log_upload", "Upload Logs") });
+ if (!Array.isArray(summary?.summaries) || summary.summaries.length !== targets.length) {
+ throw new Error(getUIText("upload_summary_unavailable", "Upload information is unavailable."));
+ }
+ uploadStats = dashcamUploadStats(summary.summaries);
+ } catch (error) {
+ showAppToast(`${getUIText("log_upload", "Upload Logs")} ${getUIText("error", "Error")}: ${error?.message || error}`, {
+ tone: "error",
+ duration: 4200,
+ });
+ return;
+ }
+ const ok = await appConfirm("", {
+ title: getUIText("log_upload", "Upload Logs"),
+ html: true,
+ messageHtml: dashcamUploadConfirmHtml(uploadStats),
+ confirmLabel: getUIText("upload_send", "Send"),
+ });
if (!ok) return;
}
const silentProgress = {
setCancelHandler() {},
setCanceling() {},
setMessage() {},
- setProgress() {},
+ setProgressState() {},
+ update() {},
setSummary() {},
- close() {},
+ settle() {
+ return Promise.resolve();
+ },
+ close() {
+ return Promise.resolve();
+ },
};
let cancelRequested = false;
const progress = options.showProgress === false
@@ -1551,11 +1672,17 @@ async function uploadDashcamSegments(segments, options = {}) {
: openDashcamUploadProgress(targets.length, uploadStats, {
onCancel: async () => {
cancelRequested = true;
- progress.setCanceling(true);
- if (jobId) await cancelDashcamUploadJob(jobId);
- clearRememberedDashcamUploadJob(jobId);
- progress.close();
- showAppToast(getUIText("upload_canceled", "Upload canceled"), { duration: 2600 });
+ if (jobId) {
+ const cancelSnapshot = await cancelDashcamUploadJob(jobId);
+ progress.update(cancelSnapshot, targets.length);
+ } else {
+ progress.update({
+ phase: "canceling",
+ step_current: 0,
+ step_total: targets.length,
+ progress: null,
+ }, targets.length);
+ }
},
});
let activityId = options.showProgress !== false && typeof beginAppActivity === "function"
@@ -1563,7 +1690,12 @@ async function uploadDashcamSegments(segments, options = {}) {
: null;
let jobId = null;
try {
- progress.setMessage(`0/${targets.length} · ${getUIText("log_uploading", "Uploading logs")}`);
+ progress.update({
+ phase: "preparing",
+ step_current: 0,
+ step_total: targets.length,
+ progress: null,
+ }, targets.length);
if (cancelRequested) throw makeDashcamUploadCanceledError();
const started = await postJson("/api/dashcam/upload/start", { segments: targets });
jobId = started.job_id;
@@ -1572,12 +1704,21 @@ async function uploadDashcamSegments(segments, options = {}) {
await cancelDashcamUploadJob(jobId);
throw makeDashcamUploadCanceledError();
}
- const result = await pollDashcamUploadJob(jobId, progress, targets.length, { isCanceled: () => cancelRequested });
+ const result = await pollDashcamUploadJob(jobId, progress, targets.length);
clearRememberedDashcamUploadJob(jobId);
- progress.setMessage(`${Number(result.uploaded || 0)}/${Number(result.total || targets.length)}`);
- progress.setProgress(100);
- progress.setSummary(dashcamUploadStats(result.results || []));
- const message = result.message || getUIText("upload_complete_count", "Upload complete {uploaded}/{total}", {
+ const resultStats = dashcamUploadStats(result.results || []);
+ progress.update({
+ phase: result.ok ? "complete" : "failed",
+ status: result.ok ? "done" : "failed",
+ step_current: Number(result.uploaded || 0),
+ step_total: Number(result.total || targets.length),
+ progress: result.ok ? 100 : null,
+ bytes_current: resultStats.bytes,
+ bytes_total: resultStats.bytes,
+ }, targets.length);
+ progress.setSummary(resultStats);
+ await progress.settle();
+ const message = getUIText("upload_complete_count", "Upload complete {uploaded}/{total}", {
uploaded: result.uploaded || 0,
total: result.total || targets.length,
});
@@ -1587,7 +1728,7 @@ async function uploadDashcamSegments(segments, options = {}) {
duration: Number(options.toastDuration) || 3600,
});
}
- progress.close();
+ await progress.close();
if (activityId && typeof endAppActivity === "function") {
endAppActivity(activityId);
activityId = null;
@@ -1602,7 +1743,7 @@ async function uploadDashcamSegments(segments, options = {}) {
showAppToast(getUIText("upload_already_running", "Upload already running"), { tone: "error", duration: 3200 });
resumeDashcamUploadJobIfNeeded({ force: true }).catch(() => {});
} else if (isDashcamUploadCanceledError(e)) {
- if (!cancelRequested) showAppToast(getUIText("upload_canceled", "Upload canceled"), { duration: 2600 });
+ showAppToast(getUIText("upload_canceled", "Upload canceled"), { duration: 2600 });
} else {
showAppToast(`${getUIText("log_upload", "Upload Logs")} ${getUIText("error", "Error")}: ${e.message || e}`, {
tone: "error",
diff --git a/openpilot/selfdrive/carrot/web/src/features/logs/runtime.js b/openpilot/selfdrive/carrot/web/src/features/logs/runtime.js
index 744d6d6eb3..3537667de4 100644
--- a/openpilot/selfdrive/carrot/web/src/features/logs/runtime.js
+++ b/openpilot/selfdrive/carrot/web/src/features/logs/runtime.js
@@ -465,6 +465,14 @@ function openLogsVideoPlayer(title, src, options = {}) {
});
transportPlayButton.setAttribute("aria-pressed", String(!paused));
};
+ const pauseCurrentPlayback = () => {
+ try {
+ if (player?.pause) player.pause();
+ else videoEl.pause();
+ } finally {
+ window.requestAnimationFrame(syncTransportPlayState);
+ }
+ };
const toggleTransportPlayback = async () => {
try {
const ended = typeof player?.ended === "boolean" ? player.ended : videoEl.ended;
@@ -476,10 +484,8 @@ function openLogsVideoPlayer(title, src, options = {}) {
if (paused || ended) {
const playResult = player?.play ? player.play() : videoEl.play();
if (playResult && typeof playResult.then === "function") await playResult;
- } else if (player?.pause) {
- player.pause();
} else {
- videoEl.pause();
+ pauseCurrentPlayback();
}
} catch (error) {
if (typeof showAppToast === "function") {
@@ -638,6 +644,7 @@ function openLogsVideoPlayer(title, src, options = {}) {
const handler = typeof options.onSegmentSend === "function"
? ({ close: closePlayer } = {}) => options.onSegmentSend(targetSegment, { close: closePlayer })
: options.onSend;
+ pauseCurrentPlayback();
runTopAction(event, handler, "is-action-open");
});
overlay.querySelector(".dashcam-player-menu")?.addEventListener("click", (event) => {
diff --git a/openpilot/selfdrive/carrot/web/src/features/logs/style.css b/openpilot/selfdrive/carrot/web/src/features/logs/style.css
index 6f35c8bf3d..7985a235f0 100644
--- a/openpilot/selfdrive/carrot/web/src/features/logs/style.css
+++ b/openpilot/selfdrive/carrot/web/src/features/logs/style.css
@@ -778,222 +778,6 @@
}
}
-.dashcam-share-card {
- display: grid;
- gap: 10px;
- min-height: 0;
-}
-
-.dashcam-share-card__summary {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 10px;
- flex-wrap: wrap;
- color: var(--md-on-surface);
- font-size: 13px;
- font-weight: 750;
-}
-
-.dashcam-share-card__summary span:last-child {
- color: var(--md-on-surface-var);
- font-size: 12px;
- font-weight: 800;
- text-align: right;
-}
-
-.dashcam-share-card pre {
- margin: 0;
- max-height: min(42vh, 360px);
- overflow: auto;
- padding: 12px;
- border-radius: 8px;
- border: 1px solid color-mix(in srgb, var(--md-outline-var) 46%, transparent);
- background: color-mix(in srgb, var(--md-surface-cont-l) 84%, #000);
- color: var(--md-on-surface);
- font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
- white-space: pre-wrap;
- word-break: break-word;
-}
-
-#appDialog:has(.dashcam-share-dialog) .app-dialog__choiceBtn {
- min-height: 40px;
- text-align: center !important;
- justify-content: center;
-}
-
-#appDialog:has(.dashcam-share-dialog) {
- align-items: center;
- padding: 8px;
-}
-
-#appDialog:has(.dashcam-share-dialog) .app-dialog__sheet {
- max-height: calc(var(--app-vv-height, 100dvh) - 16px);
-}
-
-#appDialog:has(.dashcam-share-dialog) .app-dialog__body {
- min-height: 0;
-}
-
-#appDialog:has(.dashcam-share-dialog) .app-dialog__actions {
- margin-top: 10px;
-}
-
-.dashcam-upload-progress {
- position: fixed;
- inset: 0;
- z-index: 170;
- display: grid;
- place-items: center;
- padding: 18px;
- background: var(--popup-backdrop);
- opacity: 0;
- transition: opacity .16s ease;
-}
-
-.dashcam-upload-progress.is-open {
- opacity: 1;
-}
-
-.dashcam-upload-progress__sheet {
- width: min(92vw, 430px);
- padding: 18px;
- border-radius: var(--popup-radius);
- border: 1px solid var(--popup-border-color);
- background: var(--popup-bg);
- box-shadow: var(--popup-shadow);
-}
-
-.dashcam-upload-progress__title {
- color: var(--md-on-surface);
- font-size: 17px;
- font-weight: 850;
-}
-
-.dashcam-upload-progress__message {
- margin-top: 8px;
- color: var(--md-on-surface-var);
- font-size: 13px;
- font-weight: 700;
-}
-
-.dashcam-upload-progress__bar {
- height: 4px;
- margin-top: 16px;
- overflow: hidden;
- border-radius: 999px;
- background: color-mix(in srgb, var(--md-outline-var) 30%, transparent);
-}
-
-.dashcam-upload-progress__bar span {
- display: block;
- width: 42%;
- height: 100%;
- border-radius: inherit;
- background: var(--md-primary);
- animation: dashcam-upload-indeterminate 1.1s ease-in-out infinite;
-}
-
-.dashcam-upload-progress__summary {
- margin-top: 10px;
- color: var(--md-on-surface-var);
- font-size: 12px;
- font-weight: 800;
-}
-
-.dashcam-upload-progress__actions {
- display: flex;
- justify-content: flex-end;
- margin-top: 14px;
-}
-
-.dashcam-upload-progress__cancel {
- min-height: 38px;
- padding-inline: 16px;
- border-radius: var(--dialog-control-radius);
- border-color: color-mix(in srgb, var(--md-error) 42%, var(--md-outline-var));
- color: color-mix(in srgb, var(--md-error) 82%, var(--md-on-surface));
- background: transparent;
-}
-
-.dashcam-upload-progress__cancel:disabled {
- cursor: wait;
- opacity: 0.62;
-}
-
-@media (min-aspect-ratio: 13/10), (horizontal-viewport-segments: 2), (vertical-viewport-segments: 2), (min-width: 640px) and (min-height: 650px) {
- .dashcam-share-card {
- gap: 8px;
- }
-
- .dashcam-share-card pre {
- max-height: min(42vh, 190px);
- padding: 10px;
- font-size: 11px;
- line-height: 1.45;
- }
-
- #appDialog:has(.dashcam-share-dialog) .app-dialog__sheet {
- width: min(72vw, 520px);
- max-height: calc(var(--app-vv-height, 100dvh) - 16px);
- padding: 12px 14px;
- }
-
- #appDialog:has(.dashcam-share-dialog) .app-dialog__head {
- margin-bottom: 6px;
- }
-
- #appDialog:has(.dashcam-share-dialog) .app-dialog__title {
- font-size: 14px;
- }
-
- #appDialog:has(.dashcam-share-dialog) .app-dialog__actions {
- margin-top: 8px;
- }
-
- .dashcam-upload-progress {
- padding: 8px;
- }
-
- .dashcam-upload-progress__sheet {
- width: min(70vw, 520px);
- max-height: calc(var(--app-vv-height, 100dvh) - 16px);
- overflow: auto;
- padding: 12px;
- }
-
- .dashcam-upload-progress__title {
- font-size: 14px;
- }
-
- .dashcam-upload-progress__message {
- margin-top: 5px;
- font-size: 12px;
- }
-
- .dashcam-upload-progress__bar {
- margin-top: 10px;
- }
-
- .dashcam-upload-progress__summary {
- margin-top: 8px;
- font-size: 11px;
- }
-
- .dashcam-upload-progress__actions {
- margin-top: 10px;
- }
-
- .dashcam-upload-progress__cancel {
- min-height: 34px;
- }
-}
-
-@keyframes dashcam-upload-indeterminate {
- 0% { transform: translateX(-115%); }
- 100% { transform: translateX(260%); }
-}
-
.dashcam-player-overlay {
position: fixed;
inset: 0;
diff --git a/openpilot/selfdrive/carrot/web/src/features/logs/upload_progress.js b/openpilot/selfdrive/carrot/web/src/features/logs/upload_progress.js
new file mode 100644
index 0000000000..e4ed4903a2
--- /dev/null
+++ b/openpilot/selfdrive/carrot/web/src/features/logs/upload_progress.js
@@ -0,0 +1,122 @@
+const TERMINAL_UPLOAD_PHASES = new Set(["complete", "failed", "canceled"]);
+const PHASE_PROGRESS_FALLBACK = Object.freeze({
+ queued: 0,
+ preparing: 1,
+ uploading: 8,
+ notifying: 98,
+ canceling: 98,
+ complete: 100,
+ canceled: 0,
+ failed: 0,
+});
+
+function finiteNonNegative(value) {
+ if (value == null || value === "") return Number.NaN;
+ const numeric = Number(value);
+ return Number.isFinite(numeric) && numeric >= 0 ? numeric : Number.NaN;
+}
+
+function boundedPercent(value) {
+ return Math.max(0, Math.min(100, value));
+}
+
+function normalizedPhase(snapshot) {
+ const phase = String(snapshot?.phase || "").trim().toLowerCase();
+ if (phase) return phase;
+ const status = String(snapshot?.status || "").trim().toLowerCase();
+ if (status === "done") return "complete";
+ if (status === "failed" || status === "canceled") return status;
+ return "queued";
+}
+
+export function dashcamUploadProgressState(snapshot = {}) {
+ const phase = normalizedPhase(snapshot);
+ const status = String(snapshot.status || "").trim().toLowerCase();
+ const terminal = snapshot.done === true
+ || status === "done"
+ || status === "failed"
+ || status === "canceled"
+ || TERMINAL_UPLOAD_PHASES.has(phase);
+ const completed = status === "done" || phase === "complete";
+
+ if (completed) return { mode: "determinate", value: 100 };
+
+ const reported = finiteNonNegative(snapshot.progress);
+ if (Number.isFinite(reported)) {
+ return { mode: "determinate", value: boundedPercent(reported) };
+ }
+
+ const bytesCurrent = finiteNonNegative(snapshot.bytes_current);
+ const bytesTotal = finiteNonNegative(snapshot.bytes_total);
+ if (Number.isFinite(bytesCurrent) && Number.isFinite(bytesTotal) && bytesTotal > 0) {
+ const transferPercent = 8 + ((Math.min(bytesCurrent, bytesTotal) / bytesTotal) * 89);
+ return {
+ mode: "determinate",
+ value: boundedPercent(Math.min(97, transferPercent)),
+ };
+ }
+
+ const fallback = PHASE_PROGRESS_FALLBACK[phase];
+ if (Number.isFinite(fallback)) {
+ return {
+ mode: "determinate",
+ value: terminal ? Math.max(0, fallback) : fallback,
+ };
+ }
+
+ return { mode: "indeterminate", value: null };
+}
+
+export function createDashcamUploadProgressModel() {
+ let jobId = "";
+ let revision = -1;
+ let value = 0;
+ let last = Object.freeze({
+ accepted: true,
+ phase: "queued",
+ progressState: Object.freeze({ mode: "determinate", value: 0 }),
+ });
+
+ function reset(nextJobId = "") {
+ jobId = String(nextJobId || "");
+ revision = -1;
+ value = 0;
+ }
+
+ function update(snapshot = {}) {
+ const nextJobId = String(snapshot.id || "");
+ if (nextJobId && nextJobId !== jobId) reset(nextJobId);
+
+ const nextRevision = finiteNonNegative(snapshot.revision);
+ if (Number.isFinite(nextRevision) && revision >= 0 && nextRevision < revision) {
+ return Object.freeze({ ...last, accepted: false });
+ }
+ if (Number.isFinite(nextRevision)) revision = nextRevision;
+
+ const phase = normalizedPhase(snapshot);
+ const nextState = dashcamUploadProgressState(snapshot);
+ const completed = phase === "complete" || String(snapshot.status || "").toLowerCase() === "done";
+ if (nextState.mode === "determinate") {
+ value = completed ? 100 : Math.max(value, nextState.value);
+ }
+ last = Object.freeze({
+ accepted: true,
+ phase,
+ revision,
+ progressState: Object.freeze(
+ nextState.mode === "determinate"
+ ? { mode: "determinate", value }
+ : nextState,
+ ),
+ });
+ return last;
+ }
+
+ return Object.freeze({
+ update,
+ reset,
+ state() {
+ return last;
+ },
+ });
+}
diff --git a/openpilot/selfdrive/carrot/web/src/features/logs/upload_summary.js b/openpilot/selfdrive/carrot/web/src/features/logs/upload_summary.js
new file mode 100644
index 0000000000..08442c1130
--- /dev/null
+++ b/openpilot/selfdrive/carrot/web/src/features/logs/upload_summary.js
@@ -0,0 +1,46 @@
+"use strict";
+
+const UPLOAD_ARTIFACT_KINDS = new Set(["qcamera", "rlog"]);
+
+export function dashcamUploadArtifactKind(file) {
+ const declared = String(file?.kind || "").trim().toLowerCase();
+ if (UPLOAD_ARTIFACT_KINDS.has(declared)) return declared;
+ const name = String(file?.name || "").trim().toLowerCase();
+ if (name.startsWith("qcamera.")) return "qcamera";
+ if (name === "rlog" || name.startsWith("rlog.")) return "rlog";
+ return "";
+}
+
+export function dashcamUploadStats(items) {
+ const list = Array.isArray(items) ? items : [];
+ const segmentNames = [];
+ const seenSegments = new Set();
+ const stats = {
+ segments: 0,
+ segmentNames,
+ files: 0,
+ qcamera: 0,
+ rlog: 0,
+ bytes: 0,
+ };
+
+ list.forEach((item) => {
+ const segment = String(item?.segment || "").trim();
+ if (segment && !seenSegments.has(segment)) {
+ seenSegments.add(segment);
+ segmentNames.push(segment);
+ }
+ const files = Array.isArray(item?.files) ? item.files : [];
+ const totalSize = Number(item?.totalSize)
+ || files.reduce((sum, file) => sum + (Number(file?.size) || 0), 0);
+ stats.segments += 1;
+ stats.files += files.length;
+ stats.bytes += totalSize;
+ files.forEach((file) => {
+ const kind = dashcamUploadArtifactKind(file);
+ if (kind) stats[kind] += 1;
+ });
+ });
+
+ return stats;
+}
diff --git a/openpilot/selfdrive/carrot/web/src/ui/components/dialog/dialog.js b/openpilot/selfdrive/carrot/web/src/ui/components/dialog/dialog.js
index bb485ca758..3d963c522e 100644
--- a/openpilot/selfdrive/carrot/web/src/ui/components/dialog/dialog.js
+++ b/openpilot/selfdrive/carrot/web/src/ui/components/dialog/dialog.js
@@ -1,4 +1,10 @@
import { createFocusTrap as createDefaultFocusTrap } from "./focus_trap.js";
+import {
+ createAppProgressView,
+ normalizeAppProgressState,
+} from "../progress/progress.js";
+
+export { normalizeAppProgressState };
export const APP_DIALOG_CLOSE_DELAY = 180;
@@ -9,6 +15,7 @@ export const APP_DIALOG_VARIANT_CLASSES = Object.freeze([
"app-dialog--choice-value-grid",
"app-dialog--form",
"app-dialog--input",
+ "app-dialog--progress",
]);
const MODAL_SURFACE_IDS = Object.freeze([
@@ -218,9 +225,34 @@ export function createDialogController(environment = {}) {
resolveDialogState(activeDialog, result);
}
- function cancelAppDialog() {
- if (!activeDialog || activeDialog.submitting) return;
- resolveDialogState(activeDialog, dialogCancelResult(activeDialog));
+ async function cancelAppDialog() {
+ const state = activeDialog;
+ if (!state || state.submitting || state.canceling) return;
+ if (typeof state.onCancel !== "function") {
+ resolveDialogState(state, dialogCancelResult(state));
+ return;
+ }
+ state.canceling = true;
+ if (appDialogCancel) {
+ appDialogCancel.disabled = true;
+ appDialogCancel.textContent = state.cancelingLabel;
+ }
+ try {
+ await state.onCancel();
+ if (state.closeOnCancel && activeDialog === state && !state.closing) {
+ resolveDialogState(state, dialogCancelResult(state));
+ }
+ } catch (error) {
+ if (activeDialog !== state || state.closing) return;
+ state.canceling = false;
+ if (appDialogCancel) {
+ appDialogCancel.disabled = false;
+ appDialogCancel.textContent = state.cancelLabel;
+ }
+ if (typeof target.showAppToast === "function") {
+ target.showAppToast(error?.message || String(error), { tone: "error", duration: 3600 });
+ }
+ }
}
async function confirmAppDialog() {
@@ -427,9 +459,14 @@ export function createDialogController(environment = {}) {
mode,
serial: ++dialogSerial,
onSubmit: options.onSubmit,
+ onCancel: options.onCancel,
+ closeOnCancel: options.closeOnCancel !== false,
submitting: false,
+ canceling: false,
closing: false,
confirmLabel,
+ cancelLabel,
+ cancelingLabel: options.cancelingLabel || text("canceling", "Canceling..."),
submittingLabel: options.submittingLabel || text("saving", "Saving..."),
lastFocus: inheritedLastFocus || documentRoot.activeElement || null,
focusTrap: null,
@@ -483,11 +520,70 @@ export function createDialogController(environment = {}) {
});
}
+ function openAppProgressDialog(options = {}) {
+ const completion = openAppDialog({
+ mode: "choice",
+ title: options.title,
+ message: " ",
+ cancelLabel: options.cancelLabel,
+ cancelingLabel: options.cancelingLabel,
+ onCancel: options.onCancel,
+ closeOnCancel: false,
+ });
+ const state = activeDialog;
+ appDialog?.classList?.add("app-dialog--progress");
+ if (appDialogBody) appDialogBody.style.flex = "0 0 auto";
+ const progressView = createAppProgressView(documentRoot, options);
+ if (appDialogBody && progressView) appDialogBody.replaceChildren(progressView.element);
+
+ const isCurrent = () => Boolean(state && activeDialog === state && !state.closing);
+ const controller = {
+ completion,
+ setCancelHandler(handler) {
+ if (!isCurrent()) return;
+ state.onCancel = typeof handler === "function" ? handler : null;
+ if (appDialogCancel) {
+ appDialogCancel.hidden = !state.onCancel;
+ appDialogCancel.setAttribute("aria-hidden", state.onCancel ? "false" : "true");
+ }
+ },
+ setCanceling(active) {
+ if (!isCurrent() || !appDialogCancel) return;
+ state.canceling = Boolean(active);
+ appDialogCancel.disabled = state.canceling;
+ appDialogCancel.textContent = state.canceling ? state.cancelingLabel : state.cancelLabel;
+ },
+ setMessage(value) {
+ if (isCurrent()) progressView?.setMessage(value);
+ },
+ setProgressState(value) {
+ if (isCurrent()) progressView?.setProgressState(value);
+ },
+ setProgress(value) {
+ controller.setProgressState(value);
+ },
+ setSummary(value) {
+ if (isCurrent()) progressView?.setSummary(value);
+ },
+ close() {
+ if (isCurrent()) resolveDialogState(state, true);
+ return completion;
+ },
+ };
+ controller.setMessage(options.message);
+ controller.setProgressState(options.progressState ?? options.progress);
+ controller.setSummary(options.summary);
+ controller.setCancelHandler(options.onCancel);
+ return Object.freeze(controller);
+ }
+
function appConfirm(message, options = {}) {
return openAppDialog({
mode: "confirm",
title: options.title,
message,
+ messageHtml: options.messageHtml,
+ html: options.html,
confirmLabel: options.confirmLabel,
cancelLabel: options.cancelLabel,
});
@@ -555,6 +651,7 @@ export function createDialogController(environment = {}) {
return Object.freeze({
openAppDialog,
+ openAppProgressDialog,
appAlert,
appConfirm,
appPrompt,
diff --git a/openpilot/selfdrive/carrot/web/src/ui/components/dialog/facade.js b/openpilot/selfdrive/carrot/web/src/ui/components/dialog/facade.js
index b4ac41fd42..3a22d1b692 100644
--- a/openpilot/selfdrive/carrot/web/src/ui/components/dialog/facade.js
+++ b/openpilot/selfdrive/carrot/web/src/ui/components/dialog/facade.js
@@ -3,6 +3,7 @@ import { createFocusTrap } from "./focus_trap.js";
export const DIALOG_LEGACY_GLOBALS = Object.freeze([
"openAppDialog",
+ "openAppProgressDialog",
"appAlert",
"appConfirm",
"appPrompt",
@@ -27,6 +28,7 @@ export function installDialogFacade(target = globalThis, documentRoot = target.d
});
const facade = Object.freeze({
openAppDialog: controller.openAppDialog,
+ openAppProgressDialog: controller.openAppProgressDialog,
appAlert: controller.appAlert,
appConfirm: controller.appConfirm,
appPrompt: controller.appPrompt,
diff --git a/openpilot/selfdrive/carrot/web/src/ui/components/dialog/style.css b/openpilot/selfdrive/carrot/web/src/ui/components/dialog/style.css
index 143bb4a29d..0e8aff2667 100644
--- a/openpilot/selfdrive/carrot/web/src/ui/components/dialog/style.css
+++ b/openpilot/selfdrive/carrot/web/src/ui/components/dialog/style.css
@@ -93,7 +93,7 @@
.app-dialog__metaList {
display: flex;
flex-direction: column;
- gap: 6px;
+ gap: var(--sp-sm);
}
.app-dialog__metaLine {
@@ -109,6 +109,140 @@
line-height: 1.45;
}
+.app-dialog__metaGroup {
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-sm);
+ padding-top: var(--sp-xs);
+}
+
+.app-dialog__metaLabel {
+ color: var(--md-on-surface-var);
+ font-size: var(--fs-label-sm);
+ font-weight: 800;
+}
+
+.app-dialog__metaCodeList {
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-xs);
+}
+
+.app-dialog__metaCode {
+ overflow-wrap: anywhere;
+ color: var(--md-on-surface);
+ font: 700 var(--fs-label-sm)/1.45 var(--font-mono);
+}
+
+.app-dialog__metaSummary {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--sp-sm) var(--sp-lg);
+ color: var(--md-on-surface-var);
+ font-size: var(--fs-label-md);
+}
+
+.app-dialog__metaSummary strong {
+ margin-left: auto;
+ color: var(--md-on-surface);
+ font-weight: 850;
+}
+
+.app-dialog__uploadBrief {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: center;
+ gap: var(--sp-md);
+ min-width: 0;
+ padding: var(--sp-xs) 0;
+}
+
+.app-dialog__uploadBriefCopy,
+.app-dialog__uploadBriefAmount {
+ display: grid;
+ min-width: 0;
+ gap: 2px;
+}
+
+.app-dialog__uploadBriefTitle {
+ color: var(--md-on-surface);
+ font-size: var(--fs-body-md);
+ font-weight: 850;
+ line-height: 1.35;
+}
+
+.app-dialog__uploadBriefKinds,
+.app-dialog__uploadBriefAmount span {
+ color: var(--md-on-surface-var);
+ font-size: var(--fs-label-sm);
+ font-weight: 750;
+ line-height: 1.4;
+}
+
+.app-dialog__uploadBriefKinds {
+ overflow-wrap: anywhere;
+}
+
+.app-dialog__uploadBriefAmount {
+ justify-items: end;
+ text-align: right;
+ white-space: nowrap;
+}
+
+.app-dialog__uploadBriefAmount strong {
+ color: var(--md-on-surface);
+ font-size: var(--fs-title-xs);
+ font-weight: 850;
+ line-height: 1.2;
+}
+
+.app-dialog__metaResultList {
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-xs);
+}
+
+.app-dialog__metaResult {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--sp-md);
+ min-width: 0;
+ padding: var(--sp-xs) 0;
+}
+
+.app-dialog__metaResult .app-dialog__metaCode {
+ min-width: 0;
+}
+
+.app-dialog__metaOverflow {
+ padding: var(--sp-xs) 0;
+ color: var(--md-error);
+ font-size: var(--fs-label-sm);
+ font-weight: 800;
+}
+
+.app-dialog__metaState {
+ flex: 0 0 auto;
+ color: var(--md-on-surface-var);
+ font-size: var(--fs-label-sm);
+ font-weight: 850;
+}
+
+.app-dialog__metaState[data-tone="success"] {
+ color: var(--md-success);
+}
+
+.app-dialog__metaState[data-tone="error"] {
+ color: var(--md-error);
+}
+
+.app-dialog--progress .app-dialog__body {
+ flex: 0 0 auto;
+ overflow: visible;
+}
+
.app-dialog__choices {
--dialog-choice-heading-fg: var(--md-on-surface-var);
--dialog-choice-heading-size: var(--fs-label-sm);
diff --git a/openpilot/selfdrive/carrot/web/src/ui/components/progress/progress.js b/openpilot/selfdrive/carrot/web/src/ui/components/progress/progress.js
new file mode 100644
index 0000000000..0d24ab5dab
--- /dev/null
+++ b/openpilot/selfdrive/carrot/web/src/ui/components/progress/progress.js
@@ -0,0 +1,97 @@
+export function normalizeAppProgressState(input) {
+ if (input && typeof input === "object") {
+ const mode = String(input.mode || "").trim().toLowerCase();
+ if (mode === "indeterminate") return { mode, value: null };
+ if (mode === "determinate") {
+ const numeric = input.value == null ? Number.NaN : Number(input.value);
+ if (Number.isFinite(numeric)) {
+ return {
+ mode,
+ value: Math.max(0, Math.min(100, numeric)),
+ };
+ }
+ return { mode: "indeterminate", value: null };
+ }
+ }
+
+ const numeric = input == null ? Number.NaN : Number(input);
+ return Number.isFinite(numeric)
+ ? { mode: "determinate", value: Math.max(0, Math.min(100, numeric)) }
+ : { mode: "indeterminate", value: null };
+}
+
+export function applyAppProgressState(progress, output, input) {
+ const state = normalizeAppProgressState(input);
+ if (!progress) return state;
+
+ const determinate = state.mode === "determinate";
+ progress.dataset.progressMode = state.mode;
+ progress.setAttribute("aria-busy", determinate ? "false" : "true");
+ if (!determinate) {
+ progress.removeAttribute("value");
+ progress.removeAttribute("aria-valuenow");
+ progress.removeAttribute("data-progress-value");
+ if (output) {
+ output.textContent = "…";
+ output.removeAttribute("data-progress-value");
+ }
+ return state;
+ }
+
+ const rounded = Math.round(state.value);
+ progress.max = 100;
+ progress.value = state.value;
+ progress.setAttribute("value", String(state.value));
+ progress.setAttribute("aria-valuenow", String(rounded));
+ progress.dataset.progressValue = String(state.value);
+ if (output) {
+ output.textContent = `${rounded}%`;
+ output.dataset.progressValue = String(state.value);
+ }
+ return state;
+}
+
+function element(documentRoot, tag, className) {
+ const node = documentRoot.createElement(tag);
+ node.className = className;
+ return node;
+}
+
+export function createAppProgressView(documentRoot, options = {}) {
+ if (!documentRoot?.createElement) return null;
+
+ const root = element(documentRoot, "div", "app-progress");
+ const header = element(documentRoot, "div", "app-progress__header");
+ const message = element(documentRoot, "div", "app-progress__message");
+ const output = element(documentRoot, "output", "app-progress__value");
+ const progress = element(documentRoot, "progress", "app-progress__track");
+ const summary = element(documentRoot, "div", "app-progress__summary");
+
+ message.setAttribute("aria-live", "polite");
+ output.setAttribute("aria-live", "polite");
+ progress.max = 100;
+ progress.setAttribute("max", "100");
+ if (options.progressLabel) progress.setAttribute("aria-label", String(options.progressLabel));
+
+ header.append(message, output);
+ root.append(header, progress, summary);
+
+ const controller = {
+ element: root,
+ progressElement: progress,
+ valueElement: output,
+ setMessage(value) {
+ message.textContent = String(value || "");
+ },
+ setProgressState(value) {
+ return applyAppProgressState(progress, output, value);
+ },
+ setSummary(value) {
+ summary.textContent = String(value || "");
+ },
+ };
+ controller.setMessage(options.message);
+ controller.setProgressState(options.progressState ?? options.progress);
+ controller.setSummary(options.summary);
+ return Object.freeze(controller);
+}
diff --git a/openpilot/selfdrive/carrot/web/src/ui/components/progress/style.css b/openpilot/selfdrive/carrot/web/src/ui/components/progress/style.css
new file mode 100644
index 0000000000..cab63a0a9f
--- /dev/null
+++ b/openpilot/selfdrive/carrot/web/src/ui/components/progress/style.css
@@ -0,0 +1,108 @@
+.app-progress {
+ display: grid;
+ gap: var(--sp-md);
+}
+
+.app-progress__header {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: var(--sp-md);
+ min-width: 0;
+}
+
+.app-progress__message {
+ min-width: 0;
+ color: var(--md-on-surface);
+ font-size: var(--fs-body-md);
+ font-weight: 800;
+ font-variant-numeric: tabular-nums;
+ overflow-wrap: anywhere;
+}
+
+.app-progress__value {
+ flex: 0 0 auto;
+ color: var(--md-primary);
+ font-size: var(--fs-title-xs);
+ font-weight: 900;
+ font-variant-numeric: tabular-nums;
+}
+
+.app-progress__track {
+ display: block;
+ width: 100%;
+ min-width: 0;
+ height: 10px;
+ overflow: hidden;
+ appearance: none;
+ -webkit-appearance: none;
+ box-sizing: border-box;
+ border: 0;
+ border-radius: var(--r-round);
+ background: color-mix(in srgb, var(--md-primary) 14%, var(--md-surface-cont-h));
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--md-primary) 18%, transparent);
+}
+
+.app-progress__track::-webkit-progress-bar {
+ border-radius: inherit;
+ background: color-mix(in srgb, var(--md-primary) 14%, var(--md-surface-cont-h));
+}
+
+.app-progress__track::-webkit-progress-value {
+ border-radius: inherit;
+ background: linear-gradient(
+ 90deg,
+ color-mix(in srgb, var(--md-primary) 76%, var(--md-action-filled)),
+ var(--md-primary)
+ );
+ box-shadow: 0 0 8px color-mix(in srgb, var(--md-primary) 62%, transparent);
+ transition: width var(--motion-medium) var(--ease-linear);
+}
+
+.app-progress__track::-moz-progress-bar {
+ border-radius: inherit;
+ background: linear-gradient(
+ 90deg,
+ color-mix(in srgb, var(--md-primary) 76%, var(--md-action-filled)),
+ var(--md-primary)
+ );
+ box-shadow: 0 0 8px color-mix(in srgb, var(--md-primary) 62%, transparent);
+ transition: width var(--motion-medium) var(--ease-linear);
+}
+
+.app-progress__track:indeterminate {
+ background:
+ linear-gradient(90deg, transparent 0 22%, var(--md-primary) 40% 60%, transparent 78% 100%)
+ 0 0 / 220% 100% no-repeat,
+ color-mix(in srgb, var(--md-primary) 14%, var(--md-surface-cont-h));
+ animation: app-progress-indeterminate 1.05s var(--ease-linear) infinite;
+}
+
+.app-progress__summary {
+ color: var(--md-on-surface-var);
+ font-size: var(--fs-label-md);
+ font-weight: 750;
+ font-variant-numeric: tabular-nums;
+}
+
+@keyframes app-progress-indeterminate {
+ from {
+ background-position: 100% 0, 0 0;
+ }
+
+ to {
+ background-position: -100% 0, 0 0;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .app-progress__track::-webkit-progress-value,
+ .app-progress__track::-moz-progress-bar {
+ transition: none;
+ }
+
+ .app-progress__track:indeterminate {
+ animation: none;
+ background-position: 50% 0, 0 0;
+ }
+}
diff --git a/openpilot/selfdrive/carrot/web/tests/dashcam_upload_summary.test.mjs b/openpilot/selfdrive/carrot/web/tests/dashcam_upload_summary.test.mjs
new file mode 100644
index 0000000000..1215d4eb04
--- /dev/null
+++ b/openpilot/selfdrive/carrot/web/tests/dashcam_upload_summary.test.mjs
@@ -0,0 +1,45 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+ dashcamUploadArtifactKind,
+ dashcamUploadStats,
+} from "../src/features/logs/upload_summary.js";
+
+test("upload artifacts are classified from server kind or original filename", () => {
+ assert.equal(dashcamUploadArtifactKind({ kind: "qcamera", name: "anything" }), "qcamera");
+ assert.equal(dashcamUploadArtifactKind({ name: "qcamera.ts" }), "qcamera");
+ assert.equal(dashcamUploadArtifactKind({ name: "rlog.zst" }), "rlog");
+ assert.equal(dashcamUploadArtifactKind({ name: "qlog.zst" }), "");
+});
+
+test("upload stats preserve original segment names and count selected artifacts", () => {
+ const stats = dashcamUploadStats([
+ {
+ segment: "00001da7--8626316d57--0",
+ files: [
+ { kind: "qcamera", name: "qcamera.ts", size: 100 },
+ { kind: "rlog", name: "rlog.zst", size: 40 },
+ ],
+ totalSize: 140,
+ },
+ {
+ segment: "00001da7--8626316d57--1",
+ files: [
+ { name: "qcamera.mp4", size: 80 },
+ { name: "rlog.bz2", size: 20 },
+ ],
+ totalSize: 100,
+ },
+ ]);
+
+ assert.deepEqual(stats.segmentNames, [
+ "00001da7--8626316d57--0",
+ "00001da7--8626316d57--1",
+ ]);
+ assert.equal(stats.segments, 2);
+ assert.equal(stats.qcamera, 2);
+ assert.equal(stats.rlog, 2);
+ assert.equal(stats.files, 4);
+ assert.equal(stats.bytes, 240);
+});
diff --git a/openpilot/selfdrive/carrot/web/tests/logs_player_transport.test.mjs b/openpilot/selfdrive/carrot/web/tests/logs_player_transport.test.mjs
index 14c9183151..83328f3475 100644
--- a/openpilot/selfdrive/carrot/web/tests/logs_player_transport.test.mjs
+++ b/openpilot/selfdrive/carrot/web/tests/logs_player_transport.test.mjs
@@ -80,14 +80,93 @@ test("dashcam and screen-record players expose adjacent-item navigation", () =>
assert.match(screenrecord, /onNext:\s*next/);
});
-test("dashcam player direct send opens the shared upload result without a success toast", () => {
+test("dashcam player direct send shows progress and the shared result without a success toast", () => {
const dashcam = read("src/features/logs/dashcam.js");
+ const runtime = read("src/features/logs/runtime.js");
- assert.match(dashcam, /onSegmentSend:[\s\S]*?showProgress:\s*false,[\s\S]*?showResult:\s*true,[\s\S]*?showSuccessToast:\s*false/);
+ assert.match(dashcam, /onSegmentSend:[\s\S]*?showProgress:\s*true,[\s\S]*?showResult:\s*true,[\s\S]*?showSuccessToast:\s*false/);
+ assert.doesNotMatch(dashcam, /onSegmentSend:[\s\S]*?showProgress:\s*false/);
+ assert.match(runtime, /dashcam-player-send[\s\S]*?pauseCurrentPlayback\(\);[\s\S]*?runTopAction/);
assert.match(dashcam, /if \(options\.showSuccessToast !== false\)[\s\S]*?showAppToast\(message/);
assert.match(dashcam, /if \(options\.showResult !== false\) await showDashcamUploadResult\(result\)/);
});
+test("dashcam upload confirmation stays compact and uses an explicit send action", () => {
+ const dashcam = read("src/features/logs/dashcam.js");
+ const dialog = read("src/ui/components/dialog/dialog.js");
+ const dialogStyle = read("src/ui/components/dialog/style.css");
+
+ assert.match(dashcam, /messageHtml:\s*dashcamUploadConfirmHtml\(uploadStats\)/);
+ assert.match(dashcam, /upload_segment_count/);
+ assert.match(dashcam, /upload_kind_count/);
+ assert.match(dashcam, /confirmLabel:\s*getUIText\("upload_send",\s*"Send"\)/);
+ assert.match(dashcam, /app-dialog__uploadBrief/);
+ assert.doesNotMatch(
+ dashcam.match(/function dashcamUploadConfirmHtml[\s\S]*?\n}\n/)?.[0] || "",
+ /app-dialog__metaCodeList|upload_original_files_confirm/,
+ );
+ assert.doesNotMatch(dashcam, /getUIText\("upload_data_warning"/);
+ assert.match(dialog, /messageHtml:\s*options\.messageHtml/);
+ assert.match(dialog, /html:\s*options\.html/);
+ assert.match(dialogStyle, /\.app-dialog__uploadBrief\s*\{[^}]*grid-template-columns:\s*minmax\(0,\s*1fr\)\s*auto/s);
+ assert.match(dialogStyle, /\.app-dialog__uploadBriefAmount\s*\{[^}]*white-space:\s*nowrap/s);
+ assert.doesNotMatch(dialogStyle, /\.app-dialog__uploadBrief\s*\{[^}]*background:/s);
+ assert.doesNotMatch(dialogStyle, /\.app-dialog__metaResult\s*\{[^}]*border-radius:/s);
+ assert.doesNotMatch(dialogStyle, /\.app-dialog__metaResult(List)?\s*\{[^}]*border(?:-top|-bottom)?:/s);
+ assert.doesNotMatch(dialogStyle, /\.app-dialog__metaSummary\s*\{[^}]*border-top:/s);
+ assert.doesNotMatch(
+ dashcam.match(/function dashcamUploadResultHtml[\s\S]*?\n}\n/)?.[0] || "",
+ /app-dialog__metaGroup|app-dialog__metaLabel/,
+ );
+ assert.match(dashcam, /onSegmentSend:\s*\(target\)\s*=>\s*uploadDashcamSegments\(\[target\],\s*\{[\s\S]*?showProgress:\s*true/);
+});
+
+test("dashcam upload progress and result reuse the shared dialog components", () => {
+ const dashcam = read("src/features/logs/dashcam.js");
+ const logsStyle = read("src/features/logs/style.css");
+ const dialog = read("src/ui/components/dialog/dialog.js");
+ const dialogFacade = read("src/ui/components/dialog/facade.js");
+ const dialogStyle = read("src/ui/components/dialog/style.css");
+ const progressComponent = read("src/ui/components/progress/progress.js");
+ const progressStyle = read("src/ui/components/progress/style.css");
+ const uploadProgress = read("src/features/logs/upload_progress.js");
+
+ assert.match(dashcam, /openAppProgressDialog\(\{/);
+ assert.match(dashcam, /function dashcamUploadProgressView/);
+ assert.match(dashcam, /dashcamUploadProgressState\(snapshot\)/);
+ assert.match(dashcam, /DASHCAM_UPLOAD_POLL_INTERVAL_MS\s*=\s*160/);
+ assert.match(dashcam, /DASHCAM_UPLOAD_PROGRESS_MIN_VISIBLE_MS\s*=\s*800/);
+ assert.match(dashcam, /await progress\.settle\(\)/);
+ assert.match(dashcam, /createDashcamUploadProgressModel\(\)/);
+ assert.match(dashcam, /bytes_per_second/);
+ assert.match(dashcam, /formatLogBytes\(bytesPerSecond\).*\/s/);
+ assert.match(uploadProgress, /mode:\s*"indeterminate"/);
+ assert.match(uploadProgress, /Math\.min\(bytesCurrent,\s*bytesTotal\)\s*\/\s*bytesTotal/);
+ assert.match(dashcam, /DASHCAM_UPLOAD_RESULT_VISIBLE_LIMIT\s*=\s*5/);
+ assert.match(dashcam, /progress\.update\(snapshot,\s*totalFallback\)/);
+ assert.doesNotMatch(dashcam, /snapshot\.message\s*\|\|/);
+ assert.doesNotMatch(dashcam, /result\.message\s*\|\|\s*getUIText\("upload_complete_count"/);
+ assert.match(dashcam, /app-dialog__metaResultList/);
+ assert.doesNotMatch(dashcam, /dashcam-share-card|dashcam-upload-progress/);
+ assert.doesNotMatch(logsStyle, /dashcam-share-card|dashcam-upload-progress/);
+ assert.match(dialog, /function openAppProgressDialog/);
+ assert.match(dialog, /closeOnCancel:\s*false/);
+ assert.match(dialog, /state\.closeOnCancel && activeDialog === state/);
+ assert.match(dialog, /createAppProgressView\(documentRoot, options\)/);
+ assert.match(dialog, /appDialogBody\.replaceChildren\(progressView\.element\)/);
+ assert.match(progressComponent, /function normalizeAppProgressState/);
+ assert.match(progressComponent, /progress\.value = state\.value/);
+ assert.match(progressComponent, /progress\.setAttribute\("value", String\(state\.value\)\)/);
+ assert.match(progressComponent, /output\.textContent = `\$\{rounded\}%`/);
+ assert.match(dialogFacade, /openAppProgressDialog/);
+ assert.match(dialogStyle, /\.app-dialog--progress \.app-dialog__body/);
+ assert.match(progressStyle, /\.app-progress__track\s*\{[^}]*width:\s*100%/s);
+ assert.match(progressStyle, /\.app-progress__track::\-webkit-progress-value/);
+ assert.match(progressStyle, /\.app-progress__track:indeterminate/);
+ assert.match(progressStyle, /transition:\s*width var\(--motion-medium\) var\(--ease-linear\)/);
+ assert.match(progressStyle, /@media \(prefers-reduced-motion:\s*reduce\)/);
+});
+
test("landscape settings keep the shared submenu back control", () => {
const chrome = read("src/features/settings/styles/item_chrome.css");
const navigation = read("js/shared/ui/navigation.js");
diff --git a/openpilot/selfdrive/carrot/web/tests/upload_progress_state.test.mjs b/openpilot/selfdrive/carrot/web/tests/upload_progress_state.test.mjs
new file mode 100644
index 0000000000..72f4260ff8
--- /dev/null
+++ b/openpilot/selfdrive/carrot/web/tests/upload_progress_state.test.mjs
@@ -0,0 +1,130 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+ createDashcamUploadProgressModel,
+ dashcamUploadProgressState,
+} from "../src/features/logs/upload_progress.js";
+import { normalizeAppProgressState } from "../src/ui/components/dialog/dialog.js";
+import { applyAppProgressState } from "../src/ui/components/progress/progress.js";
+
+test("shared progress state preserves explicit indeterminate mode", () => {
+ assert.deepEqual(
+ normalizeAppProgressState({ mode: "indeterminate", value: 0 }),
+ { mode: "indeterminate", value: null },
+ );
+ assert.deepEqual(
+ normalizeAppProgressState({ mode: "determinate", value: 142 }),
+ { mode: "determinate", value: 100 },
+ );
+ assert.deepEqual(
+ normalizeAppProgressState(null),
+ { mode: "indeterminate", value: null },
+ );
+});
+
+test("queued and zero-byte upload stages stay explicitly determinate", () => {
+ assert.deepEqual(
+ dashcamUploadProgressState({ phase: "queued", progress: 0 }),
+ { mode: "determinate", value: 0 },
+ );
+ assert.deepEqual(
+ dashcamUploadProgressState({ phase: "uploading", progress: 0, bytes_current: 0, bytes_total: 1024 }),
+ { mode: "determinate", value: 0 },
+ );
+});
+
+test("reported or measured transfer progress becomes determinate", () => {
+ assert.deepEqual(
+ dashcamUploadProgressState({ phase: "uploading", progress: 27 }),
+ { mode: "determinate", value: 27 },
+ );
+ assert.deepEqual(
+ dashcamUploadProgressState({
+ phase: "uploading",
+ progress: null,
+ bytes_current: 256,
+ bytes_total: 1024,
+ }),
+ { mode: "determinate", value: 30.25 },
+ );
+});
+
+test("completed uploads always end at 100 percent", () => {
+ assert.deepEqual(
+ dashcamUploadProgressState({ phase: "complete", status: "done", done: true, progress: 0 }),
+ { mode: "determinate", value: 100 },
+ );
+});
+
+test("cancel and failure phases preserve the last reported position", () => {
+ const model = createDashcamUploadProgressModel();
+ model.update({ id: "job-b", revision: 1, phase: "uploading", progress: 57 });
+ assert.deepEqual(
+ model.update({ id: "job-b", revision: 2, phase: "canceling", progress: 57 }).progressState,
+ { mode: "determinate", value: 57 },
+ );
+ assert.deepEqual(
+ model.update({ id: "job-b", revision: 3, phase: "canceled", status: "canceled", progress: 57 }).progressState,
+ { mode: "determinate", value: 57 },
+ );
+
+ model.reset("job-c");
+ model.update({ id: "job-c", revision: 1, phase: "uploading", progress: 33 });
+ assert.deepEqual(
+ model.update({ id: "job-c", revision: 2, phase: "failed", status: "failed", progress: 33 }).progressState,
+ { mode: "determinate", value: 33 },
+ );
+});
+
+test("upload progress model rejects stale snapshots and never moves backward", () => {
+ const model = createDashcamUploadProgressModel();
+ assert.deepEqual(
+ model.update({ id: "job-a", revision: 1, phase: "preparing", progress: 4 }).progressState,
+ { mode: "determinate", value: 4 },
+ );
+ assert.equal(
+ model.update({ id: "job-a", revision: 0, phase: "uploading", progress: 80 }).accepted,
+ false,
+ );
+ assert.deepEqual(
+ model.update({ id: "job-a", revision: 2, phase: "uploading", progress: 2 }).progressState,
+ { mode: "determinate", value: 4 },
+ );
+ assert.deepEqual(
+ model.update({ id: "job-a", revision: 3, phase: "notifying", progress: 98 }).progressState,
+ { mode: "determinate", value: 98 },
+ );
+ assert.deepEqual(
+ model.update({ id: "job-a", revision: 4, phase: "complete", status: "done" }).progressState,
+ { mode: "determinate", value: 100 },
+ );
+});
+
+test("native progress element receives both value property and accessible state", () => {
+ const makeElement = () => ({
+ dataset: {},
+ attributes: new Map(),
+ textContent: "",
+ setAttribute(name, value) {
+ this.attributes.set(name, String(value));
+ },
+ removeAttribute(name) {
+ this.attributes.delete(name);
+ },
+ });
+ const progress = makeElement();
+ const output = makeElement();
+
+ applyAppProgressState(progress, output, { mode: "determinate", value: 42.4 });
+ assert.equal(progress.value, 42.4);
+ assert.equal(progress.attributes.get("value"), "42.4");
+ assert.equal(progress.attributes.get("aria-valuenow"), "42");
+ assert.equal(progress.dataset.progressValue, "42.4");
+ assert.equal(output.textContent, "42%");
+
+ applyAppProgressState(progress, output, { mode: "indeterminate" });
+ assert.equal(progress.attributes.has("value"), false);
+ assert.equal(progress.attributes.has("aria-valuenow"), false);
+ assert.equal(output.textContent, "…");
+});
diff --git a/openpilot/selfdrive/carrot/web_upload.py b/openpilot/selfdrive/carrot/web_upload.py
index ba65eac745..0a81886c7b 100644
--- a/openpilot/selfdrive/carrot/web_upload.py
+++ b/openpilot/selfdrive/carrot/web_upload.py
@@ -4,7 +4,7 @@
import os
import time
import urllib.parse
-from collections.abc import Callable, Mapping
+from collections.abc import Callable, Mapping, Sequence
from contextlib import ExitStack
from typing import Any
@@ -195,16 +195,33 @@ async def upload_folder_to_web(
base_url: str,
token: str,
should_cancel: Callable[[], bool] | None = None,
+ filenames: Sequence[str] | None = None,
+ on_progress: Callable[[str, int, int, int], None] | None = None,
) -> bool:
def check_cancel() -> None:
if should_cancel and should_cancel():
raise RuntimeError("upload canceled")
check_cancel()
- try:
- entries = sorted(entry.name for entry in os.scandir(local_folder) if entry.is_file(follow_symlinks=False))
- except OSError as e:
- raise RuntimeError(f"cannot read segment folder: {e}") from e
+ if filenames is None:
+ try:
+ entries = sorted(entry.name for entry in os.scandir(local_folder) if entry.is_file(follow_symlinks=False))
+ except OSError as e:
+ raise RuntimeError(f"cannot read segment folder: {e}") from e
+ else:
+ entries = []
+ seen: set[str] = set()
+ for raw_name in filenames:
+ filename = str(raw_name or "")
+ if not filename or filename in (".", "..") or "/" in filename or "\\" in filename:
+ raise RuntimeError("invalid upload filename")
+ if filename in seen:
+ continue
+ local_path = os.path.join(local_folder, filename)
+ if not os.path.isfile(local_path):
+ raise RuntimeError(f"upload file not found: {filename}")
+ seen.add(filename)
+ entries.append(filename)
if not token:
raise RuntimeError("upload session is not configured")
@@ -217,9 +234,15 @@ def check_cancel() -> None:
file_size = os.path.getsize(local_path)
sent = 0
- async def send_file(path: str = local_path):
+ async def send_file(
+ path: str = local_path,
+ upload_name: str = filename,
+ upload_size: int = file_size,
+ ):
nonlocal sent
sent = 0
+ if on_progress:
+ on_progress(upload_name, sent, upload_size, 0)
check_cancel()
with open(path, "rb") as f:
while True:
@@ -228,6 +251,8 @@ async def send_file(path: str = local_path):
if not chunk:
break
sent += len(chunk)
+ if on_progress:
+ on_progress(upload_name, sent, upload_size, len(chunk))
yield chunk
last_error: Exception | None = None