diff --git a/.gitignore b/.gitignore index 7df42e7f..b3289fcd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ create_2_Open_Interface_Spec.txt logs node_modules/ +__pycache__/ +*.py[cod] .pio .vscode/ config.h diff --git a/pi/bin/__pycache__/chromegtts-daemon.cpython-314.pyc b/pi/bin/__pycache__/chromegtts-daemon.cpython-314.pyc deleted file mode 100644 index c92e60c9..00000000 Binary files a/pi/bin/__pycache__/chromegtts-daemon.cpython-314.pyc and /dev/null differ diff --git a/plans/onvif-reolink.md b/plans/onvif-reolink.md index d5eeb3a0..645ba574 100644 --- a/plans/onvif-reolink.md +++ b/plans/onvif-reolink.md @@ -39,4 +39,133 @@ - uhhh idk - obviously, camera on the screen - probably add a variant of the mobile controls just to retitle the things from the rover controls to the camera controls - - and just use the same control columns \ No newline at end of file + - and just use the same control columns + +-- slop generated below -- + +## clarified implementation direction + +This is not intended to become a generic ONVIF camera framework. The camera integration is for one specific Reolink PTZ camera. Once the camera arrives, we will run a one-time ONVIF capability discovery against that exact camera, record what it exposes, and then build the integration around those known capabilities. + +The one-time discovery should capture: +- ONVIF services exposed by the camera +- media profiles and stream URIs +- snapshot URI support +- PTZ support and movement modes +- pan/tilt/zoom ranges and speed ranges +- preset/home support +- imaging controls +- any ONVIF-exposed spotlight, IR, or night-vision controls +- whether PTZ status reporting is reliable + +After that, runtime code should assume this known camera profile instead of trying to dynamically support every possible ONVIF camera. + +## claiming and operator rules + +The PTZ camera is a single scarce controllable resource. + +Only verified/VIP users can claim it during normal operation. Only one user can operate it at a time. The active operator gets live WebRTC video and PTZ control for a limited turn, probably around five minutes. Other remote users should only receive slow snapshots. Local spectators may be allowed live video because LAN traffic is not the bandwidth problem. + +A user operating the PTZ camera must not also be operating a rover. When a user tries to move from a rover to PTZ, the existing rover-switch safety rule should be reused: switching is allowed if another driver remains on that rover, or if the current rover is docked and charging. Otherwise, the server should block the PTZ handoff and tell the user to dock and charge their rover first. + +This should be implemented by refactoring the existing rover switching check into a shared helper, such as `canLeaveCurrentRover(socket)`, then using that helper from both rover switching and PTZ claiming. + +## streaming model + +Camera video should come from the Reolink camera over the local network, likely RTSP into MediaMTX. Browser playback should use the existing MediaMTX WHEP/WebRTC pipeline. + +The existing video session and MediaMTX auth system should be extended with a `ptz` source type. Remote live WHEP access should be allowed for the current PTZ operator, local spectators, and authorized admins according to normal server rules. Remote non-operators should not get live video. + +Slow snapshots should use a PTZ-specific snapshot path or socket gateway, modeled after the existing room camera snapshot system, but with PTZ-specific authorization rules. + +## lockdown behavior + +No extra UI work is needed for lockdown because the app already visually blocks things in lockdown mode. + +Server-side lockdown enforcement is still required everywhere. In lockdown mode, only lockdown admins/users may claim, queue, operate, subscribe to snapshots, request live PTZ video, or use PTZ replay sources. If lockdown starts while a normal user is operating PTZ, the server should immediately revoke their operator state, remove them from the PTZ queue if needed, revoke PTZ video sessions, and stop accepting PTZ commands from them. + +## reusable existing systems + +Strong reuse targets: +- rover switch safety logic from `roverManager/roverLifecycle.js` +- `videoSessions` +- `videoSocketService` +- `videoAuthService` +- `WhepPlayer` +- `sessionService` session sync +- VIP panel/card structure +- alert system +- replay source validation and replay worker architecture + +Adapted reuse targets: +- turn queue/timer structure from `turnService` +- turn alert listener behavior +- room camera snapshot socket/feed pattern +- `RoomCameraFeed` for slow preview display +- replay source catalog and ffmpeg workers +- existing control input concepts, but with a PTZ-specific command pipeline + +Do not directly merge PTZ into `roomCameraService` or `commandService`. PTZ should have its own service boundary because it has ownership, queueing, ONVIF control, video authorization, and camera-specific state. + +## operator UI and controls + +The VIP tab should get a PTZ camera card. The card should be technical/utilitarian and match the existing site style. It should show: +- current camera operator +- queue/turn state +- turn time remaining when relevant +- whether the current user can claim or must wait +- whether the current user must dock and charge before switching +- a slow snapshot preview +- a button to open the fullscreen PTZ controller when the user is the active operator + +The fullscreen PTZ controller should take over the whole app surface while open. It should not feel like a normal side panel. When active, the user is in camera-operation mode, not rover-driving mode. + +Desktop controls: +- movement input pans and tilts the camera +- camera up/down or equivalent camera tilt controls zoom in/out +- available special controls expose only what the one-time ONVIF probe proved exists +- if ONVIF exposes presets/home, provide those controls +- if ONVIF exposes spotlight, IR, or night mode, provide those controls +- if those features are not exposed through ONVIF, leave them out until a Reolink-specific fallback is intentionally added +- include a compact right-side status/control panel with operator, queue, camera state, and available controls + +Mobile controls: +- reuse the existing mobile control layout concept where practical +- relabel/re-map rover movement controls for PTZ movement +- keep the camera view as the main screen +- use the existing mobile control columns/pads as inspiration, but send PTZ commands instead of rover commands + +Input/control implementation: +- do not send PTZ through the existing rover `commandService` +- create PTZ-specific socket events/handlers owned by the PTZ camera service +- use a PTZ-specific client command pipeline that maps existing input intent into PTZ commands +- server must enforce that only the active PTZ operator can send movement/zoom/control commands +- client-side input interception is only for UX; server-side operator checks are the real authority +- all movement controls should send stop commands on key/button release, blur, disconnect, controller close, or turn loss + +## NEW UI STUFF +- desktop: + - sidebar like there is now + - replay panel in sidebar + - better indicators of light and OR modes + - list of controls using keybind things +- mobile: + - one sidebar on the right + - reuse rover drive control panel for camera movement + - reuse gpio toggle buttons for spotlight and IR + - reuse camera tilt slider for zoom + - relabeled variants where needed for reused mobile controls + - scroll sidebar down to see replay panel +- both: + - the VIP panel + - sucks. + - wasted space + - put snapshot and everything else side by side + - add a display of ptz's turn queue + - camera should open when you request control over it. no need to have it be another button to press + - should show state of camera + - the fullscreen interface + - should be inside a cardframe, with no title bar + - reuse anything whereever possible + - needs to be ACTUALLY FULLSCREEN, not with space around the edges anywhere + - needs to match the global styling, and use cardframes internally for stuff. \ No newline at end of file diff --git a/plans/ptz unification.md b/plans/ptz unification.md new file mode 100644 index 00000000..dd0ef37e --- /dev/null +++ b/plans/ptz unification.md @@ -0,0 +1,33 @@ +- make ptz camera better integrated +- keep current fullscreen interface, its good. + - but remove the card from the vip panel + - clean up the fullscreen interface to match the rest of the page better + - have a clear close button + - on desktop, have some stuff in sidebar and some stuff below the video + - video should keep the rest of the space + - reuse rover HUD elements like chat input and not your turn indicator + - probably make it so that the rest of the page unmounts or unloads or whatever when youre in it +- make it feel more like youre switching to a different rover instead of switching to a completely different thing +- simplify and reuse components wherever possible, frontend and backend +- right now, it feels tacked on, badly integrated, and incomplete +- needs a much better UI flow +- still needs to be a VIP feature +- for users on ptz, make their chats have a rover badge that has the ptz name and a color +- make the cam show up as a room camera in the room camera panel + - snapshot mode only +- make the ptz queue and join button show up as one roverqueuespanel style rover row below the links panel + - only show it open for verified users, for non-verified users overlay it with a message and dont let them click on it + - for mobile layouts, show it below the roverqueuepanel. +- dont worry about not being invasive, just dont break anything + +## ui flow should be: +1. you are verified +2. you see the ptz camera queue in the ui, it has 2 people in it +3. you click on it, the fullscreen UI opens + - other people will see you in the queue in the little panel +4. its not your turn yet. the fullscreen UI replaces the page. + - you see snapshots, you see the "not your turn" hud, same as driving a rover +5. its now your turn. the overlay shows up just as it does in rover hud +6. you control the camera like usual, you want to close it +7. you hit the close button, you get removed from the queue +8. the page returns to normal \ No newline at end of file diff --git a/plans/toggleable bandwidth savings.md b/plans/toggleable bandwidth savings.md new file mode 100644 index 00000000..11e03dba --- /dev/null +++ b/plans/toggleable bandwidth savings.md @@ -0,0 +1,16 @@ +# make all bandwidth saving options toggleable in one centralized server config +- multitab protection mode + - allowed + - verified only + - not allowed +- snapshots + - non-turn snapshots + - on (you see snapshots when its not your turn) + - off (everyone gets full video all the time) + - non-local spectator snapshots + - on (external spectators are only allowed snapshots) + - off (all spectators get full video) +- external spectator access (new) + - off (no one can access the spectate page externally) + - on (everyone can access the spectate page externally) +- anything else related to bandwidth savings should also get config \ No newline at end of file diff --git a/server/bin/chromegtts-wav.py b/server/bin/chromegtts-wav.py new file mode 100755 index 00000000..59545d39 --- /dev/null +++ b/server/bin/chromegtts-wav.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +""" +Chrome Google TTS WAV renderer. + +Purpose: Converts the same local ChromeOS Google TTS assets used by rovers into +server-side WAV files that can be handed to another playback transport. +Scope: This script only renders one utterance to a file; device playback and +camera delivery stay owned by Node services. +""" +import argparse +import ctypes +import os +import struct +import sys +import wave + + +ASSET_ROOT = "/opt/roverd/googletts" +LIB_PATH = os.path.join(ASSET_ROOT, "libchrometts.so") +VOICE_DIR = os.path.join(ASSET_ROOT, "en-us-x-multi-r30") +PIPELINE = "pipeline.pb" +SAMPLE_RATE = 24000 +MAX_TEXT_CHARS = 512 + +VOICES = { + "sfg": "female", + "iob": "female", + "iog": "female", + "iol": "male", + "iom": "male", + "tpc": "female", + "tpd": "male", + "tpf": "female", +} +DEFAULT_VOICE = "tpf" +DEFAULT_PITCH = 1.0 +DEFAULT_SPEED = 1.0 +MIN_PITCH = 0.5 +MAX_PITCH = 2.0 +MIN_SPEED = 0.5 +MAX_SPEED = 2.0 + + +def varint(value): + out = bytearray() + while value >= 0x80: + out.append((value & 0x7F) | 0x80) + value >>= 7 + out.append(value) + return bytes(out) + + +def field_bytes(number, payload): + return varint((number << 3) | 2) + varint(len(payload)) + payload + + +def field_float(number, value): + return varint((number << 3) | 5) + struct.pack(" maximum: + return maximum + return value + + +def float_to_s16le(samples): + pcm = bytearray() + for sample in samples: + clipped = max(-1.0, min(1.0, float(sample))) + pcm.extend(struct.pack(" 0: + count = int(frames_written.value) + if count > 0: + wav.writeframes(float_to_s16le(self.buffer[:count])) + + def shutdown(self): + self.lib.GoogleTtsShutdown() + + +def main(): + parser = argparse.ArgumentParser(description="Render Chrome Google TTS to a WAV file.") + parser.add_argument("--text", required=True) + parser.add_argument("--voice", default=DEFAULT_VOICE) + parser.add_argument("--pitch", type=float, default=DEFAULT_PITCH) + parser.add_argument("--speed", type=float, default=DEFAULT_SPEED) + parser.add_argument("--output", required=True) + args = parser.parse_args() + + tts = ChromeTTS() + try: + tts.render_wav(args.text, args.output, args.voice, args.pitch, args.speed) + finally: + tts.shutdown() + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as exc: + sys.stderr.write(f"chromegtts-wav failed: {exc}\n") + raise SystemExit(1) diff --git a/server/config.example.yaml b/server/config.example.yaml index fefc2631..744a3d70 100644 --- a/server/config.example.yaml +++ b/server/config.example.yaml @@ -128,6 +128,22 @@ roomCameras: url: "http://192.168.0.51/snapshot.jpg" streamUrl: "http://192.168.0.51/stream.mjpg" +ptzCamera: + enabled: false + name: "PTZ Camera" + host: "192.168.0.8" + onvifPort: 8000 + username: "admin" + password: "REPLACE_WITH_CAMERA_PASSWORD" + # The Reolink TrackMix autotrack profile was token 003 during commissioning. + # Keeping this configurable lets firmware/profile resets be fixed without code + # changes while the integration still remains a single-camera feature. + profileToken: "003" + turnDurationMs: 300000 + # PTZ replay capture needs a known-good replay encoder on the server. Keep it + # off by default so adding live PTZ does not start a broken replay worker loop. + replayEnabled: false + kinect: enabled: false # Capture requests are global across 3d/color so one person cannot spam room diff --git a/server/index.js b/server/index.js index e2f8a7ec..a3926ce2 100644 --- a/server/index.js +++ b/server/index.js @@ -26,6 +26,7 @@ require('./src/services/overseerControlService'); require('./src/services/globalObjectiveService'); require('./src/services/serverControlService'); require('./src/services/videoSessions'); +require('./src/services/ptzCameraService'); require('./src/services/videoAuthService'); require('./src/services/videoSocketService'); require('./src/services/roomCameraService'); diff --git a/server/install_server.sh b/server/install_server.sh index 002079cd..650abfa7 100755 --- a/server/install_server.sh +++ b/server/install_server.sh @@ -2,8 +2,12 @@ set -euo pipefail MEDIAMTX_VERSION="1.15.3" +NEOLINK_VERSION="0.6.2" MEDIAMTX_BASE_URL="https://github.com/bluenviron/mediamtx/releases/download/v${MEDIAMTX_VERSION}" +NEOLINK_BASE_URL="https://github.com/QuantumEntangledAndy/neolink/releases/download/v${NEOLINK_VERSION}" MEDIAMTX_BIN="/usr/local/bin/mediamtx" +NEOLINK_BIN="/usr/local/bin/neolink" +CHROMEGTTS_WAV_BIN="/usr/local/bin/chromegtts-wav" MEDIAMTX_CONF_DIR="/etc/mediamtx" MEDIAMTX_CONFIG="$MEDIAMTX_CONF_DIR/mediamtx.yml" ROVER_SNAPSHOT_WRITER_BIN="/usr/local/bin/rover-snapshot-writer.sh" @@ -29,6 +33,79 @@ SERVER_DIR="$SCRIPT_DIR" CONFIG_PATH="$SERVER_DIR/config.yaml" MEDIAMTX_TEMPLATE="$SERVER_DIR/mediamtx/mediamtx.yml" ROVER_SNAPSHOT_WRITER_TEMPLATE="$SERVER_DIR/mediamtx/rover-snapshot-writer.sh" +CHROMEGTTS_WAV_TEMPLATE="$SERVER_DIR/bin/chromegtts-wav.py" + +install_google_tts_assets() { + local asset_dir="/opt/roverd/googletts" + local voice_dir="${asset_dir}/en-us-x-multi-r30" + local dist_url="https://storage.googleapis.com/chromeos-localmirror/distfiles/googletts-26.5.tar.xz" + local lib_member="" + local arch_name + arch_name=$(uname -m) + + # The PTZ camera is not a rover, so Google speech must be synthesized on the + # server before neolink sends a WAV to the camera. These assets are the same + # offline ChromeOS local TTS assets that rover installers already use; keeping + # the layout identical lets the server helper and rover daemon share loader + # assumptions. + if [[ -f "${asset_dir}/libchrometts.so" && -f "${voice_dir}/pipeline.pb" ]]; then + echo " Google TTS assets already installed" + return + fi + + case "$arch_name" in + x86_64|amd64) + lib_member="libchrometts_x86_64.so" + ;; + aarch64) + lib_member="libchrometts_arm64.so" + ;; + armv7l|armv6l) + lib_member="libchrometts_armv7.so" + ;; + *) + echo "Unsupported Google TTS architecture: $arch_name" >&2 + exit 1 + ;; + esac + + echo " Installing Google TTS assets -> $asset_dir" + curl -L -o "$tmpdir/googletts-26.5.tar.xz" "$dist_url" + tar -xf "$tmpdir/googletts-26.5.tar.xz" -C "$tmpdir" en-us-x-multi.zvoice "$lib_member" + install -d -o root -g root -m 0755 "$asset_dir" + install -o root -g root -m 0644 "$tmpdir/$lib_member" "${asset_dir}/libchrometts.so" + rm -rf "$voice_dir" + install -d -o root -g root -m 0755 "$voice_dir" + # The .zvoice member is a zip archive inside the outer tar.xz. Match the + # rover installers here; trying to untar it fails after the large download. + unzip -q "$tmpdir/en-us-x-multi.zvoice" -d "$voice_dir" + chown -R root:root "$asset_dir" + find "$asset_dir" -type d -exec chmod 0755 {} + + find "$asset_dir" -type f -exec chmod 0644 {} + +} + +verify_google_tts_helper() { + local smoke_wav="$tmpdir/chromegtts-smoke.wav" + + echo " Verifying Chrome Google TTS helper" + # libchrometts is a native ChromeOS library. Rendering one tiny WAV during + # install catches missing shared-library dependencies, bad asset extraction, + # and helper path mistakes before multirover.service starts accepting PTZ TTS + # requests that would fail later in logs. + if ! "$CHROMEGTTS_WAV_BIN" \ + --text "test" \ + --voice tpf \ + --pitch 1 \ + --speed 1 \ + --output "$smoke_wav"; then + echo "Chrome Google TTS helper smoke render failed." >&2 + return 1 + fi + if [[ ! -s "$smoke_wav" ]]; then + echo "Chrome Google TTS helper did not create a WAV file." >&2 + return 1 + fi +} echo "[1/6] Installing dependencies..." # The Kinect tooling uses a native libfreenect worker/probe rather than a @@ -40,9 +117,21 @@ dnf install -y \ npm \ curl \ tar \ + unzip \ + xz \ gcc-c++ \ make \ pkgconf-pkg-config \ + flite \ + espeak \ + python3 \ + libcxx \ + libcxxabi \ + gstreamer1 \ + gstreamer1-plugins-base \ + gstreamer1-plugins-good \ + gstreamer1-plugins-bad-free \ + gstreamer1-rtsp-server \ libfreenect \ libfreenect-devel \ libusb1-devel >/dev/null @@ -62,6 +151,13 @@ EOF chmod 644 "$KINECT_UDEV_RULE" udevadm control --reload-rules +if [[ ! -f "$CHROMEGTTS_WAV_TEMPLATE" ]]; then + echo "Chrome Google TTS WAV helper missing at $CHROMEGTTS_WAV_TEMPLATE" >&2 + exit 1 +fi +echo " Installing Chrome Google TTS WAV helper -> $CHROMEGTTS_WAV_BIN" +install -m 0755 "$CHROMEGTTS_WAV_TEMPLATE" "$CHROMEGTTS_WAV_BIN" + echo "[2/6] Installing Node production deps..." runuser -u "$TARGET_USER" -- bash -c "cd '$SERVER_DIR' && npm install --production" @@ -83,12 +179,15 @@ arch=$(uname -m) case "$arch" in x86_64|amd64) mediamtx_pkg="mediamtx_v${MEDIAMTX_VERSION}_linux_amd64.tar.gz" + neolink_pkg="neolink_linux_x86_64_ubuntu.zip" ;; aarch64) mediamtx_pkg="mediamtx_v${MEDIAMTX_VERSION}_linux_arm64.tar.gz" + neolink_pkg="neolink_linux_arm64.zip" ;; armv7l) mediamtx_pkg="mediamtx_v${MEDIAMTX_VERSION}_linux_armv7.tar.gz" + neolink_pkg="neolink_linux_armhf.zip" ;; *) echo "Unsupported architecture: $arch" >&2 @@ -101,6 +200,26 @@ curl -L "$MEDIAMTX_BASE_URL/$mediamtx_pkg" -o "$tmpdir/mediamtx.tgz" tar -xzf "$tmpdir/mediamtx.tgz" -C "$tmpdir" mediamtx install -m 0755 "$tmpdir/mediamtx" "$MEDIAMTX_BIN" +echo " Installing neolink ${NEOLINK_VERSION} -> $NEOLINK_BIN" +curl -L "$NEOLINK_BASE_URL/$neolink_pkg" -o "$tmpdir/neolink.zip" +unzip -q "$tmpdir/neolink.zip" -d "$tmpdir/neolink" +neolink_extracted=$(find "$tmpdir/neolink" -type f -name neolink -perm /111 | head -n 1) +if [[ -z "$neolink_extracted" ]]; then + neolink_extracted=$(find "$tmpdir/neolink" -type f -name neolink | head -n 1) +fi +if [[ -z "$neolink_extracted" ]]; then + echo "neolink binary missing from $neolink_pkg" >&2 + exit 1 +fi +install -m 0755 "$neolink_extracted" "$NEOLINK_BIN" +install_google_tts_assets +if ! verify_google_tts_helper; then + echo " Reinstalling Google TTS assets after failed verification" + rm -rf /opt/roverd/googletts + install_google_tts_assets + verify_google_tts_helper +fi + mkdir -p "$MEDIAMTX_CONF_DIR" if [[ ! -f "$MEDIAMTX_TEMPLATE" ]]; then echo "mediaMTX template missing at $MEDIAMTX_TEMPLATE" >&2 diff --git a/server/mediamtx/rover-snapshot-writer.sh b/server/mediamtx/rover-snapshot-writer.sh index a6b2dc96..7dbe6d7f 100644 --- a/server/mediamtx/rover-snapshot-writer.sh +++ b/server/mediamtx/rover-snapshot-writer.sh @@ -16,10 +16,24 @@ esac mkdir -p "$SNAP_DIR" +FILTER="fps=1" +QUALITY="6" + +case "$PATH_NAME" in + ptz-camera) + # PTZ snapshots are shown to non-operators specifically to avoid sending the + # full live video stream. The PTZ publisher is full-resolution 16:9 video, + # so resize the JPEGs at the snapshot writer boundary before Node ever reads + # and fans them out over Socket.IO. + FILTER="fps=1,scale=480:-2" + QUALITY="10" + ;; +esac + exec ffmpeg -hide_banner -loglevel warning -nostdin -y \ -i "srt://127.0.0.1:9000?streamid=read:${PATH_NAME}" \ -an \ - -vf fps=1 \ - -q:v 6 \ + -vf "$FILTER" \ + -q:v "$QUALITY" \ -update 1 \ "${SNAP_DIR}/${PATH_NAME}.jpg" diff --git a/server/package.json b/server/package.json index 77f129c9..056c60fd 100644 --- a/server/package.json +++ b/server/package.json @@ -19,6 +19,8 @@ "morgan": "^1.10.0", "obscenity": "^0.4.6", "ollama": "^0.6.3", + "onvif": "^0.8.1", + "reolink-nvr-api": "^0.3.0", "sharp": "^0.33.5", "socket.io": "^4.7.5", "uuid": "^9.0.1", diff --git a/server/public/assets/index-BN3kEVFL.css b/server/public/assets/index-BN3kEVFL.css new file mode 100644 index 00000000..a2c4915a --- /dev/null +++ b/server/public/assets/index-BN3kEVFL.css @@ -0,0 +1 @@ +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media(min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media(min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media(min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media(min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media(min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.pride-page-bg{background-color:#050505;background-image:linear-gradient(#0000,#0000),repeating-linear-gradient(45deg,#5bcefa 0 1.818%,#f5a9b8 1.818% 3.636%,#fff 3.636% 5.455%,#f5a9b8 5.455% 7.273%,#5bcefa 7.273% 9.091%,#e40303 9.091% 10.909%,#ff8c00 10.909% 12.727%,#ffed00 12.727% 14.545%,#00a651 14.545% 16.364%,#06f 16.364% 18.182%,#9b2fae 18.182% 20%);background-attachment:fixed}.\!panel,.panel{border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity));padding:0;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.panel-section{border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(23 23 23 / var(--tw-bg-opacity));padding:0;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.panel-muted{border-radius:.375rem;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.surface{border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity));padding:.125rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.surface-muted{border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity));padding:.125rem;--tw-text-opacity: 1;color:rgb(245 245 245 / var(--tw-text-opacity))}.field-input{border-radius:.375rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity));padding:.125rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.field-input::-moz-placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.field-input::placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.field-input:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-opacity: 1;--tw-ring-color: rgb(14 165 233 / var(--tw-ring-opacity))}.button-dark{border-radius:.375rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(125 211 252 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(2 132 199 / var(--tw-bg-opacity));padding:.125rem;font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.button-dark:hover{--tw-border-opacity: 1;border-color:rgb(14 165 233 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(14 165 233 / var(--tw-bg-opacity))}.button-danger{border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(225 29 72 / var(--tw-bg-opacity));padding:.125rem;font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.button-danger:hover{--tw-bg-opacity: 1;background-color:rgb(244 63 94 / var(--tw-bg-opacity))}.chat-composer{container-type:inline-size;display:flex;flex-wrap:wrap;align-items:stretch;gap:.125rem;min-width:0;overflow:hidden}.chat-composer-nickname{flex:0 0 5rem;min-width:0;order:1}.chat-composer-input{flex:1 1 0%;min-width:0;order:2}.chat-composer-send{flex:0 0 auto;align-self:stretch;order:3}.chat-composer-tts{display:flex;flex:1 1 100%;align-items:center;gap:.125rem;min-width:0;overflow:hidden;order:4}@container (min-width: 44rem){.chat-composer{flex-wrap:nowrap}.chat-composer-nickname{flex-basis:7rem;order:1}.chat-composer-input{order:2}.chat-composer-tts{flex:0 1 auto;order:3}.chat-composer-send{order:4}}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.\!visible{visibility:visible!important}.visible{visibility:visible}.collapse{visibility:collapse}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-x-0{left:0;right:0}.inset-x-1{left:.25rem;right:.25rem}.bottom-0{bottom:0}.bottom-0\.5{bottom:.125rem}.bottom-1{bottom:.25rem}.bottom-2{bottom:.5rem}.bottom-4{bottom:1rem}.bottom-\[calc\(100\%\+0\.125rem\)\]{bottom:calc(100% + .125rem)}.left-0{left:0}.left-0\.5{left:.125rem}.left-1{left:.25rem}.left-1\/2{left:50%}.left-2{left:.5rem}.left-4{left:1rem}.right-0{right:0}.right-0\.5{right:.125rem}.right-1{right:.25rem}.right-2{right:.5rem}.top-0{top:0}.top-0\.5{top:.125rem}.top-1{top:.25rem}.top-1\/2{top:50%}.top-10{top:2.5rem}.top-6{top:1.5rem}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[1000\]{z-index:1000}.z-\[110\]{z-index:110}.z-\[120\]{z-index:120}.z-\[130\]{z-index:130}.z-\[2000\]{z-index:2000}.z-\[70\]{z-index:70}.order-1{order:1}.order-2{order:2}.col-span-2{grid-column:span 2 / span 2}.col-span-full{grid-column:1 / -1}.m-0{margin:0}.-mx-0\.5{margin-left:-.125rem;margin-right:-.125rem}.mx-auto{margin-left:auto;margin-right:auto}.my-\[-0\.125rem\]{margin-top:-.125rem;margin-bottom:-.125rem}.-mt-0\.5{margin-top:-.125rem}.mb-0\.5{margin-bottom:.125rem}.ml-0\.5{margin-left:.125rem}.ml-1{margin-left:.25rem}.mr-0{margin-right:0}.mt-0{margin-top:0}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-\[0\.35vh\]{margin-top:.35vh}.mt-auto{margin-top:auto}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.\!block{display:block!important}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.aspect-\[4\/3\]{aspect-ratio:4/3}.aspect-square{aspect-ratio:1 / 1}.aspect-video{aspect-ratio:16 / 9}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-28{height:7rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-40{height:10rem}.h-44{height:11rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-64{height:16rem}.h-8{height:2rem}.h-\[100dvh\]{height:100dvh}.h-\[2px\]{height:2px}.h-\[3\.5rem\]{height:3.5rem}.h-\[48dvh\]{height:48dvh}.h-\[70\%\]{height:70%}.h-\[7rem\]{height:7rem}.h-\[8vh\]{height:8vh}.h-\[calc\(1rem\+0\.25rem\)\]{height:1.25rem}.h-\[min\(100svh\,32rem\)\]{height:min(100svh,32rem)}.h-\[min\(60svh\,24rem\)\]{height:min(60svh,24rem)}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-24{max-height:6rem}.max-h-32{max-height:8rem}.max-h-36{max-height:9rem}.max-h-40{max-height:10rem}.max-h-52{max-height:13rem}.max-h-72{max-height:18rem}.max-h-80{max-height:20rem}.max-h-\[14rem\]{max-height:14rem}.max-h-\[36rem\]{max-height:36rem}.max-h-\[70vh\]{max-height:70vh}.max-h-\[72vh\]{max-height:72vh}.max-h-\[82vh\]{max-height:82vh}.max-h-\[85vh\]{max-height:85vh}.max-h-\[86vh\]{max-height:86vh}.max-h-\[90vh\]{max-height:90vh}.max-h-full{max-height:100%}.max-h-screen{max-height:100vh}.min-h-0{min-height:0px}.min-h-10{min-height:2.5rem}.min-h-14{min-height:3.5rem}.min-h-48{min-height:12rem}.min-h-5{min-height:1.25rem}.min-h-7{min-height:1.75rem}.min-h-9{min-height:2.25rem}.min-h-\[10rem\]{min-height:10rem}.min-h-\[12rem\]{min-height:12rem}.min-h-\[14rem\]{min-height:14rem}.min-h-\[16rem\]{min-height:16rem}.min-h-\[18rem\]{min-height:18rem}.min-h-\[3\.5rem\]{min-height:3.5rem}.min-h-\[4rem\]{min-height:4rem}.min-h-\[5rem\]{min-height:5rem}.min-h-\[7rem\]{min-height:7rem}.min-h-\[calc\(100dvh-0\.25rem\)\]{min-height:calc(100dvh - .25rem)}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-1{width:.25rem}.w-1\.5{width:.375rem}.w-1\/2{width:50%}.w-10{width:2.5rem}.w-2{width:.5rem}.w-20{width:5rem}.w-28{width:7rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-80{width:20rem}.w-\[100vw\]{width:100vw}.w-\[12\.5rem\]{width:12.5rem}.w-\[12rem\]{width:12rem}.w-\[20rem\]{width:20rem}.w-\[2px\]{width:2px}.w-\[3\.25rem\]{width:3.25rem}.w-\[4\.5rem\]{width:4.5rem}.w-\[5\.5rem\]{width:5.5rem}.w-\[5rem\]{width:5rem}.w-\[9rem\]{width:9rem}.w-\[calc\(1rem\+0\.25rem\)\]{width:1.25rem}.w-\[min\(20rem\,calc\(100vw-1rem\)\)\]{width:min(20rem,calc(100vw - 1rem))}.w-auto{width:auto}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.w-px{width:1px}.w-screen{width:100vw}.min-w-0{min-width:0px}.min-w-28{min-width:7rem}.min-w-56{min-width:14rem}.min-w-\[0\]{min-width:0}.min-w-\[3\.7rem\]{min-width:3.7rem}.min-w-\[9\.5rem\]{min-width:9.5rem}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.max-w-\[24rem\]{max-width:24rem}.max-w-\[40vw\]{max-width:40vw}.max-w-\[70vw\]{max-width:70vw}.max-w-\[80vw\]{max-width:80vw}.max-w-\[92\%\]{max-width:92%}.max-w-\[92vw\]{max-width:92vw}.max-w-\[94\%\]{max-width:94%}.max-w-\[calc\(100vw-0\.5rem\)\]{max-width:calc(100vw - .5rem)}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-\[0\.72\]{flex:.72}.flex-\[1\.1\]{flex:1.1}.flex-\[1\.22\]{flex:1.22}.flex-\[1\.28\]{flex:1.28}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow-\[0\.75\]{flex-grow:.75}.grow-\[0\.9\]{flex-grow:.9}.grow-\[1\]{flex-grow:1}.basis-0{flex-basis:0px}.origin-top-left{transform-origin:top left}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.auto-rows-fr{grid-auto-rows:minmax(0,1fr)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-\[1\.5rem_minmax\(0\,1fr\)_3rem\]{grid-template-columns:1.5rem minmax(0,1fr) 3rem}.grid-cols-\[auto_minmax\(0\,1fr\)\]{grid-template-columns:auto minmax(0,1fr)}.grid-cols-\[minmax\(0\,0\.7fr\)_minmax\(0\,2\.1fr\)_minmax\(0\,0\.7fr\)\]{grid-template-columns:minmax(0,.7fr) minmax(0,2.1fr) minmax(0,.7fr)}.grid-cols-\[minmax\(0\,1\.2fr\)_repeat\(4\,minmax\(0\,1fr\)\)\]{grid-template-columns:minmax(0,1.2fr) repeat(4,minmax(0,1fr))}.grid-cols-\[minmax\(0\,1\.35fr\)_minmax\(0\,0\.95fr\)\]{grid-template-columns:minmax(0,1.35fr) minmax(0,.95fr)}.grid-cols-\[minmax\(0\,1\.3fr\)_minmax\(0\,0\.22fr\)\]{grid-template-columns:minmax(0,1.3fr) minmax(0,.22fr)}.grid-cols-\[minmax\(0\,1\.45fr\)_minmax\(0\,0\.85fr\)\]{grid-template-columns:minmax(0,1.45fr) minmax(0,.85fr)}.grid-cols-\[minmax\(0\,1\.6fr\)_minmax\(16rem\,0\.7fr\)\]{grid-template-columns:minmax(0,1.6fr) minmax(16rem,.7fr)}.grid-cols-\[minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,1fr)}.grid-cols-\[minmax\(0\,1fr\)_11rem\]{grid-template-columns:minmax(0,1fr) 11rem}.grid-cols-\[minmax\(0\,1fr\)_13rem\]{grid-template-columns:minmax(0,1fr) 13rem}.grid-cols-\[minmax\(0\,1fr\)_14rem\]{grid-template-columns:minmax(0,1fr) 14rem}.grid-cols-\[minmax\(0\,1fr\)_2\.5rem\]{grid-template-columns:minmax(0,1fr) 2.5rem}.grid-cols-\[minmax\(0\,1fr\)_20rem\]{grid-template-columns:minmax(0,1fr) 20rem}.grid-cols-\[minmax\(0\,1fr\)_4\.75rem\]{grid-template-columns:minmax(0,1fr) 4.75rem}.grid-cols-\[minmax\(0\,1fr\)_auto\]{grid-template-columns:minmax(0,1fr) auto}.grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,0\.7fr\)_minmax\(0\,1\.9fr\)_minmax\(0\,0\.7fr\)\]{grid-template-columns:minmax(0,1fr) minmax(0,.7fr) minmax(0,1.9fr) minmax(0,.7fr)}.grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1\.4fr\)\]{grid-template-columns:minmax(0,1fr) minmax(0,1.4fr)}.grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.grid-rows-\[auto_1fr\]{grid-template-rows:auto 1fr}.grid-rows-\[auto_auto_1fr\]{grid-template-rows:auto auto 1fr}.grid-rows-\[auto_auto_1fr_auto\]{grid-template-rows:auto auto 1fr auto}.grid-rows-\[auto_auto_auto_1fr\]{grid-template-rows:auto auto auto 1fr}.grid-rows-\[auto_minmax\(0\,1fr\)\]{grid-template-rows:auto minmax(0,1fr)}.grid-rows-\[auto_minmax\(0\,1fr\)_auto\]{grid-template-rows:auto minmax(0,1fr) auto}.grid-rows-\[minmax\(0\,1fr\)\]{grid-template-rows:minmax(0,1fr)}.grid-rows-\[minmax\(0\,1fr\)_auto\]{grid-template-rows:minmax(0,1fr) auto}.grid-rows-\[minmax\(0\,1fr\)_minmax\(0\,1fr\)_minmax\(0\,1fr\)\]{grid-template-rows:minmax(0,1fr) minmax(0,1fr) minmax(0,1fr)}.grid-rows-\[minmax\(0\,1fr\)_minmax\(7rem\,0\.22fr\)\]{grid-template-rows:minmax(0,1fr) minmax(7rem,.22fr)}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.content-start{align-content:flex-start}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-stretch{justify-content:stretch}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-4{gap:1rem}.gap-\[0\.4vh\]{gap:.4vh}.gap-\[0\.55vh\]{gap:.55vh}.gap-\[0\.8vw\]{gap:.8vw}.gap-\[1vh\]{gap:1vh}.gap-\[1vw\]{gap:1vw}.gap-\[3vh\]{gap:3vh}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.self-start{align-self:flex-start}.self-stretch{align-self:stretch}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-none{border-radius:0}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.border{border-width:1px}.border-0{border-width:0px}.border-2{border-width:2px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-l-4{border-left-width:4px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-amber-200{--tw-border-opacity: 1;border-color:rgb(253 230 138 / var(--tw-border-opacity))}.border-amber-200\/30{border-color:#fde68a4d}.border-amber-200\/70{border-color:#fde68ab3}.border-amber-200\/90{border-color:#fde68ae6}.border-amber-300\/60{border-color:#fcd34d99}.border-amber-300\/70{border-color:#fcd34db3}.border-amber-300\/80{border-color:#fcd34dcc}.border-amber-400\/30{border-color:#fbbf244d}.border-amber-400\/60{border-color:#fbbf2499}.border-amber-500\/50{border-color:#f59e0b80}.border-amber-600\/60{border-color:#d9770699}.border-amber-700\/60{border-color:#b4530999}.border-blue-600{--tw-border-opacity: 1;border-color:rgb(37 99 235 / var(--tw-border-opacity))}.border-cyan-200{--tw-border-opacity: 1;border-color:rgb(165 243 252 / var(--tw-border-opacity))}.border-cyan-200\/90{border-color:#a5f3fce6}.border-cyan-300{--tw-border-opacity: 1;border-color:rgb(103 232 249 / var(--tw-border-opacity))}.border-cyan-300\/60{border-color:#67e8f999}.border-cyan-300\/70{border-color:#67e8f9b3}.border-cyan-300\/80{border-color:#67e8f9cc}.border-cyan-500\/40{border-color:#06b6d466}.border-emerald-100\/80{border-color:#d1fae5cc}.border-emerald-200{--tw-border-opacity: 1;border-color:rgb(167 243 208 / var(--tw-border-opacity))}.border-emerald-200\/70{border-color:#a7f3d0b3}.border-emerald-200\/90{border-color:#a7f3d0e6}.border-emerald-300{--tw-border-opacity: 1;border-color:rgb(110 231 183 / var(--tw-border-opacity))}.border-emerald-300\/70{border-color:#6ee7b7b3}.border-emerald-400\/60{border-color:#34d39999}.border-emerald-500\/40{border-color:#10b98166}.border-emerald-700\/60{border-color:#04785799}.border-emerald-700\/70{border-color:#047857b3}.border-emerald-950{--tw-border-opacity: 1;border-color:rgb(2 44 34 / var(--tw-border-opacity))}.border-fuchsia-300\/70{border-color:#f0abfcb3}.border-green-400{--tw-border-opacity: 1;border-color:rgb(74 222 128 / var(--tw-border-opacity))}.border-indigo-200\/70{border-color:#c7d2feb3}.border-indigo-200\/95{border-color:#c7d2fef2}.border-indigo-300\/70{border-color:#a5b4fcb3}.border-indigo-400\/30{border-color:#818cf84d}.border-indigo-700{--tw-border-opacity: 1;border-color:rgb(67 56 202 / var(--tw-border-opacity))}.border-indigo-700\/60{border-color:#4338ca99}.border-neutral-500\/50{border-color:#73737380}.border-neutral-500\/60{border-color:#73737399}.border-neutral-600{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity))}.border-neutral-700{--tw-border-opacity: 1;border-color:rgb(64 64 64 / var(--tw-border-opacity))}.border-neutral-700\/70{border-color:#404040b3}.border-pink-200{--tw-border-opacity: 1;border-color:rgb(251 207 232 / var(--tw-border-opacity))}.border-red-100{--tw-border-opacity: 1;border-color:rgb(254 226 226 / var(--tw-border-opacity))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity))}.border-red-300\/90{border-color:#fca5a5e6}.border-red-400{--tw-border-opacity: 1;border-color:rgb(248 113 113 / var(--tw-border-opacity))}.border-red-400\/60{border-color:#f8717199}.border-rose-300{--tw-border-opacity: 1;border-color:rgb(253 164 175 / var(--tw-border-opacity))}.border-sky-300{--tw-border-opacity: 1;border-color:rgb(125 211 252 / var(--tw-border-opacity))}.border-sky-400\/50{border-color:#38bdf880}.border-sky-400\/70{border-color:#38bdf8b3}.border-slate-200\/30{border-color:#e2e8f04d}.border-slate-400{--tw-border-opacity: 1;border-color:rgb(148 163 184 / var(--tw-border-opacity))}.border-slate-400\/60{border-color:#94a3b899}.border-slate-500\/70{border-color:#64748bb3}.border-slate-600{--tw-border-opacity: 1;border-color:rgb(71 85 105 / var(--tw-border-opacity))}.border-slate-600\/40{border-color:#47556966}.border-slate-600\/60{border-color:#47556999}.border-slate-600\/70{border-color:#475569b3}.border-slate-600\/80{border-color:#475569cc}.border-slate-700{--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity))}.border-slate-700\/60{border-color:#33415599}.border-slate-700\/70{border-color:#334155b3}.border-slate-700\/80{border-color:#334155cc}.border-slate-800{--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity))}.border-slate-800\/60{border-color:#1e293b99}.border-slate-800\/80{border-color:#1e293bcc}.border-transparent{border-color:transparent}.border-white{--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity))}.border-white\/10{border-color:#ffffff1a}.border-white\/40{border-color:#fff6}.border-white\/80{border-color:#fffc}.border-l-neutral-600{--tw-border-opacity: 1;border-left-color:rgb(82 82 82 / var(--tw-border-opacity))}.\!bg-black{--tw-bg-opacity: 1 !important;background-color:rgb(0 0 0 / var(--tw-bg-opacity))!important}.bg-amber-100\/95{background-color:#fef3c7f2}.bg-amber-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity))}.bg-amber-400\/80{background-color:#fbbf24cc}.bg-amber-500{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity))}.bg-amber-500\/30{background-color:#f59e0b4d}.bg-amber-600{--tw-bg-opacity: 1;background-color:rgb(217 119 6 / var(--tw-bg-opacity))}.bg-amber-600\/70{background-color:#d97706b3}.bg-amber-600\/80{background-color:#d97706cc}.bg-amber-700\/20{background-color:#b4530933}.bg-amber-700\/30{background-color:#b453094d}.bg-amber-700\/35{background-color:#b4530959}.bg-amber-700\/80{background-color:#b45309cc}.bg-amber-800{--tw-bg-opacity: 1;background-color:rgb(146 64 14 / var(--tw-bg-opacity))}.bg-amber-900{--tw-bg-opacity: 1;background-color:rgb(120 53 15 / var(--tw-bg-opacity))}.bg-amber-900\/40{background-color:#78350f66}.bg-amber-900\/60{background-color:#78350f99}.bg-amber-900\/90{background-color:#78350fe6}.bg-amber-950\/35{background-color:#451a0359}.bg-amber-950\/40{background-color:#451a0366}.bg-amber-950\/70{background-color:#451a03b3}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity))}.bg-black\/20{background-color:#0003}.bg-black\/30{background-color:#0000004d}.bg-black\/40{background-color:#0006}.bg-black\/45{background-color:#00000073}.bg-black\/50{background-color:#00000080}.bg-black\/55{background-color:#0000008c}.bg-black\/60{background-color:#0009}.bg-black\/70{background-color:#000000b3}.bg-black\/75{background-color:#000000bf}.bg-black\/80{background-color:#000c}.bg-black\/95{background-color:#000000f2}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.bg-cyan-300{--tw-bg-opacity: 1;background-color:rgb(103 232 249 / var(--tw-bg-opacity))}.bg-cyan-300\/20{background-color:#67e8f933}.bg-cyan-500{--tw-bg-opacity: 1;background-color:rgb(6 182 212 / var(--tw-bg-opacity))}.bg-cyan-500\/80{background-color:#06b6d4cc}.bg-cyan-900{--tw-bg-opacity: 1;background-color:rgb(22 78 99 / var(--tw-bg-opacity))}.bg-cyan-900\/45{background-color:#164e6373}.bg-cyan-900\/90{background-color:#164e63e6}.bg-emerald-200{--tw-bg-opacity: 1;background-color:rgb(167 243 208 / var(--tw-bg-opacity))}.bg-emerald-400{--tw-bg-opacity: 1;background-color:rgb(52 211 153 / var(--tw-bg-opacity))}.bg-emerald-500{--tw-bg-opacity: 1;background-color:rgb(16 185 129 / var(--tw-bg-opacity))}.bg-emerald-500\/45{background-color:#10b98173}.bg-emerald-500\/80{background-color:#10b981cc}.bg-emerald-600{--tw-bg-opacity: 1;background-color:rgb(5 150 105 / var(--tw-bg-opacity))}.bg-emerald-600\/70{background-color:#059669b3}.bg-emerald-600\/80{background-color:#059669cc}.bg-emerald-700{--tw-bg-opacity: 1;background-color:rgb(4 120 87 / var(--tw-bg-opacity))}.bg-emerald-700\/20{background-color:#04785733}.bg-emerald-700\/60{background-color:#04785799}.bg-emerald-700\/70{background-color:#047857b3}.bg-emerald-800{--tw-bg-opacity: 1;background-color:rgb(6 95 70 / var(--tw-bg-opacity))}.bg-emerald-900{--tw-bg-opacity: 1;background-color:rgb(6 78 59 / var(--tw-bg-opacity))}.bg-emerald-900\/60{background-color:#064e3b99}.bg-emerald-900\/90{background-color:#064e3be6}.bg-emerald-950{--tw-bg-opacity: 1;background-color:rgb(2 44 34 / var(--tw-bg-opacity))}.bg-emerald-950\/50{background-color:#022c2280}.bg-fuchsia-200{--tw-bg-opacity: 1;background-color:rgb(245 208 254 / var(--tw-bg-opacity))}.bg-fuchsia-600{--tw-bg-opacity: 1;background-color:rgb(192 38 211 / var(--tw-bg-opacity))}.bg-fuchsia-700{--tw-bg-opacity: 1;background-color:rgb(162 28 175 / var(--tw-bg-opacity))}.bg-fuchsia-800{--tw-bg-opacity: 1;background-color:rgb(134 25 143 / var(--tw-bg-opacity))}.bg-fuchsia-900\/45{background-color:#701a7573}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity))}.bg-indigo-600{--tw-bg-opacity: 1;background-color:rgb(79 70 229 / var(--tw-bg-opacity))}.bg-indigo-600\/70{background-color:#4f46e5b3}.bg-indigo-900{--tw-bg-opacity: 1;background-color:rgb(49 46 129 / var(--tw-bg-opacity))}.bg-indigo-900\/60{background-color:#312e8199}.bg-indigo-950{--tw-bg-opacity: 1;background-color:rgb(30 27 75 / var(--tw-bg-opacity))}.bg-indigo-950\/90{background-color:#1e1b4be6}.bg-neutral-700{--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity))}.bg-neutral-800{--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity))}.bg-neutral-800\/80{background-color:#262626cc}.bg-neutral-900{--tw-bg-opacity: 1;background-color:rgb(23 23 23 / var(--tw-bg-opacity))}.bg-neutral-900\/100{background-color:#171717}.bg-neutral-900\/60{background-color:#17171799}.bg-neutral-900\/70{background-color:#171717b3}.bg-neutral-900\/95{background-color:#171717f2}.bg-neutral-950{--tw-bg-opacity: 1;background-color:rgb(10 10 10 / var(--tw-bg-opacity))}.bg-pink-500{--tw-bg-opacity: 1;background-color:rgb(236 72 153 / var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.bg-red-500\/45{background-color:#ef444473}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity))}.bg-red-600\/70{background-color:#dc2626b3}.bg-red-700{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity))}.bg-red-700\/20{background-color:#b91c1c33}.bg-red-700\/60{background-color:#b91c1c99}.bg-red-700\/80{background-color:#b91c1ccc}.bg-red-800{--tw-bg-opacity: 1;background-color:rgb(153 27 27 / var(--tw-bg-opacity))}.bg-red-900\/35{background-color:#7f1d1d59}.bg-red-900\/40{background-color:#7f1d1d66}.bg-red-900\/45{background-color:#7f1d1d73}.bg-red-900\/50{background-color:#7f1d1d80}.bg-red-900\/80{background-color:#7f1d1dcc}.bg-red-950\/70{background-color:#450a0ab3}.bg-rose-500{--tw-bg-opacity: 1;background-color:rgb(244 63 94 / var(--tw-bg-opacity))}.bg-rose-600{--tw-bg-opacity: 1;background-color:rgb(225 29 72 / var(--tw-bg-opacity))}.bg-sky-600{--tw-bg-opacity: 1;background-color:rgb(2 132 199 / var(--tw-bg-opacity))}.bg-sky-700\/20{background-color:#0369a133}.bg-sky-700\/70{background-color:#0369a1b3}.bg-slate-300{--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity))}.bg-slate-500{--tw-bg-opacity: 1;background-color:rgb(100 116 139 / var(--tw-bg-opacity))}.bg-slate-600{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}.bg-slate-600\/70{background-color:#475569b3}.bg-slate-700{--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}.bg-slate-700\/30{background-color:#3341554d}.bg-slate-700\/60{background-color:#33415599}.bg-slate-700\/70{background-color:#334155b3}.bg-slate-700\/80{background-color:#334155cc}.bg-slate-800{--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}.bg-slate-800\/60{background-color:#1e293b99}.bg-slate-800\/70{background-color:#1e293bb3}.bg-slate-800\/80{background-color:#1e293bcc}.bg-slate-900{--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity))}.bg-slate-900\/35{background-color:#0f172a59}.bg-slate-900\/40{background-color:#0f172a66}.bg-slate-900\/70{background-color:#0f172ab3}.bg-slate-950{--tw-bg-opacity: 1;background-color:rgb(2 6 23 / var(--tw-bg-opacity))}.bg-slate-950\/35{background-color:#02061759}.bg-slate-950\/80{background-color:#020617cc}.bg-slate-950\/90{background-color:#020617e6}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-white\/10{background-color:#ffffff1a}.bg-yellow-100{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity))}.bg-zinc-900{--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity))}.bg-zinc-900\/90{background-color:#18181be6}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-lime-800{--tw-gradient-from: #3f6212 var(--tw-gradient-from-position);--tw-gradient-to: rgb(63 98 18 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-neutral-800{--tw-gradient-from: #262626 var(--tw-gradient-from-position);--tw-gradient-to: rgb(38 38 38 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.via-neutral-700{--tw-gradient-to: rgb(64 64 64 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), #404040 var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-neutral-600{--tw-gradient-to: #525252 var(--tw-gradient-to-position)}.to-teal-800{--tw-gradient-to: #115e59 var(--tw-gradient-to-position)}.bg-cover{background-size:cover}.bg-center{background-position:center}.bg-no-repeat{background-repeat:no-repeat}.fill-slate-100{fill:#f1f5f9}.fill-slate-200{fill:#e2e8f0}.fill-slate-400{fill:#94a3b8}.fill-white{fill:#fff}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-4{padding:1rem}.p-\[0\.85vw\]{padding:.85vw}.px-0{padding-left:0;padding-right:0}.px-0\.5{padding-left:.125rem;padding-right:.125rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-\[1\.6vw\]{padding-left:1.6vw;padding-right:1.6vw}.px-\[1vw\]{padding-left:1vw;padding-right:1vw}.px-\[3vw\]{padding-left:3vw;padding-right:3vw}.px-\[4vw\]{padding-left:4vw;padding-right:4vw}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-\[0\.55vh\]{padding-top:.55vh;padding-bottom:.55vh}.py-\[1px\]{padding-top:1px;padding-bottom:1px}.py-\[2\.5vh\]{padding-top:2.5vh;padding-bottom:2.5vh}.py-\[2px\]{padding-top:2px;padding-bottom:2px}.py-\[4vh\]{padding-top:4vh;padding-bottom:4vh}.pb-0{padding-bottom:0}.pb-0\.5{padding-bottom:.125rem}.pb-1{padding-bottom:.25rem}.pb-3{padding-bottom:.75rem}.pr-0{padding-right:0}.pr-1{padding-right:.25rem}.pt-1{padding-top:.25rem}.pt-5{padding-top:1.25rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[0\.45rem\]{font-size:.45rem}.text-\[0\.48rem\]{font-size:.48rem}.text-\[0\.52rem\]{font-size:.52rem}.text-\[0\.55em\]{font-size:.55em}.text-\[0\.55rem\]{font-size:.55rem}.text-\[0\.58rem\]{font-size:.58rem}.text-\[0\.5rem\]{font-size:.5rem}.text-\[0\.62rem\]{font-size:.62rem}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.68rem\]{font-size:.68rem}.text-\[0\.6rem\]{font-size:.6rem}.text-\[0\.72rem\]{font-size:.72rem}.text-\[0\.75rem\]{font-size:.75rem}.text-\[0\.78rem\]{font-size:.78rem}.text-\[0\.7rem\]{font-size:.7rem}.text-\[0\.82rem\]{font-size:.82rem}.text-\[0\.85rem\]{font-size:.85rem}.text-\[0\.8rem\]{font-size:.8rem}.text-\[1\.1rem\]{font-size:1.1rem}.text-\[1\.5rem\]{font-size:1.5rem}.text-\[17vw\]{font-size:17vw}.text-\[1rem\]{font-size:1rem}.text-\[2\.5vw\]{font-size:2.5vw}.text-\[2\.6vw\]{font-size:2.6vw}.text-\[3\.1vw\]{font-size:3.1vw}.text-\[3vw\]{font-size:3vw}.text-\[4vw\]{font-size:4vw}.text-\[6vw\]{font-size:6vw}.text-\[7vw\]{font-size:7vw}.text-\[clamp\(1\.25rem\,2\.5vh\,2\.6rem\)\]{font-size:clamp(1.25rem,2.5vh,2.6rem)}.text-\[clamp\(1\.2rem\,2\.9vh\,3rem\)\]{font-size:clamp(1.2rem,2.9vh,3rem)}.text-\[clamp\(2\.5rem\,5\.3vh\,5\.8rem\)\]{font-size:clamp(2.5rem,5.3vh,5.8rem)}.text-\[clamp\(2\.5rem\,7vh\,7rem\)\]{font-size:clamp(2.5rem,7vh,7rem)}.text-\[clamp\(2\.5rem\,8vh\,8rem\)\]{font-size:clamp(2.5rem,8vh,8rem)}.text-\[clamp\(2rem\,5vh\,5\.2rem\)\]{font-size:clamp(2rem,5vh,5.2rem)}.text-\[clamp\(3\.2rem\,11vh\,11rem\)\]{font-size:clamp(3.2rem,11vh,11rem)}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-black{font-weight:900}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.leading-\[0\.95\]{line-height:.95}.leading-none{line-height:1}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-normal{letter-spacing:0em}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.text-amber-100{--tw-text-opacity: 1;color:rgb(254 243 199 / var(--tw-text-opacity))}.text-amber-200{--tw-text-opacity: 1;color:rgb(253 230 138 / var(--tw-text-opacity))}.text-amber-200\/80{color:#fde68acc}.text-amber-200\/85{color:#fde68ad9}.text-amber-300{--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity))}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity))}.text-amber-50{--tw-text-opacity: 1;color:rgb(255 251 235 / var(--tw-text-opacity))}.text-amber-50\/90{color:#fffbebe6}.text-amber-950{--tw-text-opacity: 1;color:rgb(69 26 3 / var(--tw-text-opacity))}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.text-cyan-100{--tw-text-opacity: 1;color:rgb(207 250 254 / var(--tw-text-opacity))}.text-cyan-200{--tw-text-opacity: 1;color:rgb(165 243 252 / var(--tw-text-opacity))}.text-cyan-300{--tw-text-opacity: 1;color:rgb(103 232 249 / var(--tw-text-opacity))}.text-cyan-50{--tw-text-opacity: 1;color:rgb(236 254 255 / var(--tw-text-opacity))}.text-cyan-950{--tw-text-opacity: 1;color:rgb(8 51 68 / var(--tw-text-opacity))}.text-emerald-100{--tw-text-opacity: 1;color:rgb(209 250 229 / var(--tw-text-opacity))}.text-emerald-100\/80{color:#d1fae5cc}.text-emerald-200{--tw-text-opacity: 1;color:rgb(167 243 208 / var(--tw-text-opacity))}.text-emerald-300{--tw-text-opacity: 1;color:rgb(110 231 183 / var(--tw-text-opacity))}.text-emerald-400{--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity))}.text-emerald-50{--tw-text-opacity: 1;color:rgb(236 253 245 / var(--tw-text-opacity))}.text-emerald-50\/90{color:#ecfdf5e6}.text-emerald-950{--tw-text-opacity: 1;color:rgb(2 44 34 / var(--tw-text-opacity))}.text-fuchsia-50{--tw-text-opacity: 1;color:rgb(253 244 255 / var(--tw-text-opacity))}.text-fuchsia-900{--tw-text-opacity: 1;color:rgb(112 26 117 / var(--tw-text-opacity))}.text-indigo-200{--tw-text-opacity: 1;color:rgb(199 210 254 / var(--tw-text-opacity))}.text-indigo-50{--tw-text-opacity: 1;color:rgb(238 242 255 / var(--tw-text-opacity))}.text-indigo-50\/90{color:#eef2ffe6}.text-lime-300{--tw-text-opacity: 1;color:rgb(190 242 100 / var(--tw-text-opacity))}.text-lime-400{--tw-text-opacity: 1;color:rgb(163 230 53 / var(--tw-text-opacity))}.text-neutral-100{--tw-text-opacity: 1;color:rgb(245 245 245 / var(--tw-text-opacity))}.text-neutral-200{--tw-text-opacity: 1;color:rgb(229 229 229 / var(--tw-text-opacity))}.text-neutral-300{--tw-text-opacity: 1;color:rgb(212 212 212 / var(--tw-text-opacity))}.text-neutral-400{--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity))}.text-neutral-50{--tw-text-opacity: 1;color:rgb(250 250 250 / var(--tw-text-opacity))}.text-neutral-500{--tw-text-opacity: 1;color:rgb(115 115 115 / var(--tw-text-opacity))}.text-red-100{--tw-text-opacity: 1;color:rgb(254 226 226 / var(--tw-text-opacity))}.text-red-100\/90{color:#fee2e2e6}.text-red-100\/95{color:#fee2e2f2}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}.text-red-50{--tw-text-opacity: 1;color:rgb(254 242 242 / var(--tw-text-opacity))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity))}.text-rose-200{--tw-text-opacity: 1;color:rgb(254 205 211 / var(--tw-text-opacity))}.text-rose-300{--tw-text-opacity: 1;color:rgb(253 164 175 / var(--tw-text-opacity))}.text-sky-200{--tw-text-opacity: 1;color:rgb(186 230 253 / var(--tw-text-opacity))}.text-sky-300{--tw-text-opacity: 1;color:rgb(125 211 252 / var(--tw-text-opacity))}.text-sky-50{--tw-text-opacity: 1;color:rgb(240 249 255 / var(--tw-text-opacity))}.text-slate-100{--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}.text-slate-200{--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity))}.text-slate-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.text-slate-400\/60{color:#94a3b899}.text-slate-50{--tw-text-opacity: 1;color:rgb(248 250 252 / var(--tw-text-opacity))}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}.text-slate-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity))}.text-slate-950{--tw-text-opacity: 1;color:rgb(2 6 23 / var(--tw-text-opacity))}.text-teal-400{--tw-text-opacity: 1;color:rgb(45 212 191 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-white\/90{color:#ffffffe6}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity))}.accent-cyan-500{accent-color:#06b6d4}.accent-emerald-400{accent-color:#34d399}.accent-emerald-500{accent-color:#10b981}.accent-red-500{accent-color:#ef4444}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-40{opacity:.4}.opacity-45{opacity:.45}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.opacity-85{opacity:.85}.opacity-90{opacity:.9}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_1px_0_rgba\(255\,255\,255\,0\.05\)_inset\,0_10px_24px_rgba\(0\,0\,0\,0\.28\)\]{--tw-shadow: 0 1px 0 rgba(255,255,255,.05) inset,0 10px 24px rgba(0,0,0,.28);--tw-shadow-colored: inset 0 1px 0 var(--tw-shadow-color), 0 10px 24px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / .05);--tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-black\/40{--tw-shadow-color: rgb(0 0 0 / .4);--tw-shadow: var(--tw-shadow-colored)}.shadow-cyan-950\/50{--tw-shadow-color: rgb(8 51 68 / .5);--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-4{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-amber-200\/60{--tw-ring-color: rgb(253 230 138 / .6)}.ring-amber-300{--tw-ring-opacity: 1;--tw-ring-color: rgb(252 211 77 / var(--tw-ring-opacity))}.ring-amber-300\/80{--tw-ring-color: rgb(252 211 77 / .8)}.ring-emerald-300\/50{--tw-ring-color: rgb(110 231 183 / .5)}.ring-emerald-300\/70{--tw-ring-color: rgb(110 231 183 / .7)}.ring-red-300\/60{--tw-ring-color: rgb(252 165 165 / .6)}.ring-red-500\/70{--tw-ring-color: rgb(239 68 68 / .7)}.ring-red-500\/80{--tw-ring-color: rgb(239 68 68 / .8)}.ring-red-500\/90{--tw-ring-color: rgb(239 68 68 / .9)}.ring-sky-300\/50{--tw-ring-color: rgb(125 211 252 / .5)}.ring-sky-400\/80{--tw-ring-color: rgb(56 189 248 / .8)}.ring-slate-400\/40{--tw-ring-color: rgb(148 163 184 / .4)}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.grayscale{--tw-grayscale: grayscale(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert: invert(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.sepia{--tw-sepia: sepia(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[width\,height\]{transition-property:width,height;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-500{transition-duration:.5s}.duration-700{transition-duration:.7s}@keyframes batteryTickWarn{0%,49%{background-color:#fffffff2}50%,to{background-color:#ef4444f2}}.battery-tick-warn{animation:batteryTickWarn .4s steps(2,end) infinite}@keyframes batteryUrgentFlash{0%,49%{opacity:1}50%,to{opacity:.25}}.battery-urgent-flash{animation:batteryUrgentFlash .4s steps(2,end) infinite}.no-touch-select{-moz-user-select:none;user-select:none;-webkit-user-select:none;-ms-user-select:none;-webkit-touch-callout:none}.mobile-touch-control{user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;touch-action:manipulation;overscroll-behavior:contain}.mobile-drag-control{touch-action:none}.mobile-text-entry{font-size:16px;-webkit-text-size-adjust:100%;touch-action:manipulation}.\[writing-mode\:vertical-rl\]{writing-mode:vertical-rl}:root{font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}body{margin:0;min-height:100vh;--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}html,body,*{scrollbar-width:thin;scrollbar-color:rgba(148,163,184,.55) transparent}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}*::-webkit-scrollbar{width:2px;height:2px}*::-webkit-scrollbar-track{background:transparent}*::-webkit-scrollbar-thumb{background-color:#94a3b88c;border-radius:9999px}@supports (scrollbar-width: thin){@media(pointer:coarse){html,body,*{scrollbar-width:none}}}.placeholder\:text-slate-400::-moz-placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.placeholder\:text-slate-400::placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.last\:mb-0:last-child{margin-bottom:0}.hover\:-translate-y-0\.5:hover{--tw-translate-y: -.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-110:hover{--tw-scale-x: 1.1;--tw-scale-y: 1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-green-100:hover{--tw-border-opacity: 1;border-color:rgb(220 252 231 / var(--tw-border-opacity))}.hover\:border-rose-500:hover{--tw-border-opacity: 1;border-color:rgb(244 63 94 / var(--tw-border-opacity))}.hover\:border-sky-500:hover{--tw-border-opacity: 1;border-color:rgb(14 165 233 / var(--tw-border-opacity))}.hover\:border-white:hover{--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity))}.hover\:border-white\/60:hover{border-color:#fff9}.hover\:bg-amber-400:hover{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity))}.hover\:bg-amber-600:hover{--tw-bg-opacity: 1;background-color:rgb(217 119 6 / var(--tw-bg-opacity))}.hover\:bg-amber-800:hover{--tw-bg-opacity: 1;background-color:rgb(146 64 14 / var(--tw-bg-opacity))}.hover\:bg-cyan-400:hover{--tw-bg-opacity: 1;background-color:rgb(34 211 238 / var(--tw-bg-opacity))}.hover\:bg-cyan-800:hover{--tw-bg-opacity: 1;background-color:rgb(21 94 117 / var(--tw-bg-opacity))}.hover\:bg-emerald-400:hover{--tw-bg-opacity: 1;background-color:rgb(52 211 153 / var(--tw-bg-opacity))}.hover\:bg-emerald-500:hover{--tw-bg-opacity: 1;background-color:rgb(16 185 129 / var(--tw-bg-opacity))}.hover\:bg-emerald-700:hover{--tw-bg-opacity: 1;background-color:rgb(4 120 87 / var(--tw-bg-opacity))}.hover\:bg-fuchsia-500:hover{--tw-bg-opacity: 1;background-color:rgb(217 70 239 / var(--tw-bg-opacity))}.hover\:bg-green-400:hover{--tw-bg-opacity: 1;background-color:rgb(74 222 128 / var(--tw-bg-opacity))}.hover\:bg-indigo-500:hover{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity))}.hover\:bg-indigo-800:hover{--tw-bg-opacity: 1;background-color:rgb(55 48 163 / var(--tw-bg-opacity))}.hover\:bg-neutral-700:hover{--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity))}.hover\:bg-pink-400:hover{--tw-bg-opacity: 1;background-color:rgb(244 114 182 / var(--tw-bg-opacity))}.hover\:bg-red-600:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity))}.hover\:bg-rose-500:hover{--tw-bg-opacity: 1;background-color:rgb(244 63 94 / var(--tw-bg-opacity))}.hover\:bg-sky-500:hover{--tw-bg-opacity: 1;background-color:rgb(14 165 233 / var(--tw-bg-opacity))}.hover\:bg-slate-600:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}.hover\:bg-slate-700\/70:hover{background-color:#334155b3}.hover\:bg-white\/10:hover{background-color:#ffffff1a}.hover\:bg-zinc-900:hover{--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.hover\:opacity-90:hover{opacity:.9}.hover\:shadow-xl:hover{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:brightness-110:hover{--tw-brightness: brightness(1.1);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.focus\:border-cyan-300:focus{--tw-border-opacity: 1;border-color:rgb(103 232 249 / var(--tw-border-opacity))}.focus\:bg-white\/10:focus{background-color:#ffffff1a}.focus\:text-white:focus{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-emerald-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(16 185 129 / var(--tw-ring-opacity))}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:outline:focus-visible{outline-style:solid}.focus-visible\:outline-1:focus-visible{outline-width:1px}.focus-visible\:outline-offset-1:focus-visible{outline-offset:1px}.focus-visible\:outline-slate-500:focus-visible{outline-color:#64748b}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-amber-300:focus-visible{--tw-ring-opacity: 1;--tw-ring-color: rgb(252 211 77 / var(--tw-ring-opacity))}.focus-visible\:ring-cyan-300:focus-visible{--tw-ring-opacity: 1;--tw-ring-color: rgb(103 232 249 / var(--tw-ring-opacity))}.focus-visible\:ring-emerald-200:focus-visible{--tw-ring-opacity: 1;--tw-ring-color: rgb(167 243 208 / var(--tw-ring-opacity))}.focus-visible\:ring-emerald-300:focus-visible{--tw-ring-opacity: 1;--tw-ring-color: rgb(110 231 183 / var(--tw-ring-opacity))}.focus-visible\:ring-indigo-300:focus-visible{--tw-ring-opacity: 1;--tw-ring-color: rgb(165 180 252 / var(--tw-ring-opacity))}.active\:scale-95:active{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:scale-\[0\.99\]:active{--tw-scale-x: .99;--tw-scale-y: .99;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:brightness-125:active{--tw-brightness: brightness(1.25);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-60:disabled{opacity:.6}.disabled\:opacity-70:disabled{opacity:.7}@media(max-width:520px){.max-\[520px\]\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.max-\[520px\]\:justify-start{justify-content:flex-start}}@media(max-width:420px){.max-\[420px\]\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.max-\[420px\]\:grid-cols-\[auto_minmax\(0\,1fr\)\]{grid-template-columns:auto minmax(0,1fr)}.max-\[420px\]\:justify-start{justify-content:flex-start}}@media(min-width:640px){.sm\:block{display:block}.sm\:min-h-\[18rem\]{min-height:18rem}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:grid-cols-\[minmax\(0\,0\.8fr\)_minmax\(0\,1\.2fr\)\]{grid-template-columns:minmax(0,.8fr) minmax(0,1.2fr)}.sm\:text-2xl{font-size:1.5rem;line-height:2rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}}@media(min-width:768px){.md\:h-full{height:100%}.md\:h-screen{height:100vh}.md\:min-h-0{min-height:0px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-\[0\.9fr_1fr_1\.3fr\]{grid-template-columns:.9fr 1fr 1.3fr}.md\:grid-cols-\[minmax\(0\,1\.4fr\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,1.4fr) minmax(0,1fr)}.md\:grid-cols-\[minmax\(0\,1\.5fr\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,1.5fr) minmax(0,1fr)}.md\:grid-cols-\[minmax\(0\,1fr\)_10rem\]{grid-template-columns:minmax(0,1fr) 10rem}.md\:grid-cols-\[minmax\(0\,1fr\)_18rem\]{grid-template-columns:minmax(0,1fr) 18rem}.md\:grid-cols-\[minmax\(0\,1fr\)_auto\]{grid-template-columns:minmax(0,1fr) auto}.md\:grid-cols-\[minmax\(0\,1fr\)_auto_auto\]{grid-template-columns:minmax(0,1fr) auto auto}.md\:grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,0\.7fr\)\]{grid-template-columns:minmax(0,1fr) minmax(0,.7fr)}.md\:grid-cols-\[minmax\(16rem\,0\.8fr\)_minmax\(0\,1\.2fr\)\]{grid-template-columns:minmax(16rem,.8fr) minmax(0,1.2fr)}.md\:items-start{align-items:flex-start}.md\:overflow-hidden{overflow:hidden}.md\:overflow-y-auto{overflow-y:auto}.md\:text-lg{font-size:1.125rem;line-height:1.75rem}.md\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media(min-width:1024px){.lg\:col-span-2{grid-column:span 2 / span 2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:grid-cols-\[24rem_minmax\(0\,1fr\)\]{grid-template-columns:24rem minmax(0,1fr)}.lg\:grid-cols-\[minmax\(0\,1\.25fr\)_minmax\(0\,0\.9fr\)\]{grid-template-columns:minmax(0,1.25fr) minmax(0,.9fr)}.lg\:grid-cols-\[minmax\(0\,1fr\)_20rem\]{grid-template-columns:minmax(0,1fr) 20rem}.lg\:grid-cols-\[minmax\(0\,1fr\)_9rem_10rem\]{grid-template-columns:minmax(0,1fr) 9rem 10rem}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:text-right{text-align:right}}@media(min-width:1280px){.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}} diff --git a/server/public/assets/index-BfZ_TWsb.css b/server/public/assets/index-BfZ_TWsb.css deleted file mode 100644 index 43388ff8..00000000 --- a/server/public/assets/index-BfZ_TWsb.css +++ /dev/null @@ -1 +0,0 @@ -*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media(min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media(min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media(min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media(min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media(min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.pride-page-bg{background-color:#050505;background-image:linear-gradient(#0000,#0000),repeating-linear-gradient(45deg,#5bcefa 0 1.818%,#f5a9b8 1.818% 3.636%,#fff 3.636% 5.455%,#f5a9b8 5.455% 7.273%,#5bcefa 7.273% 9.091%,#e40303 9.091% 10.909%,#ff8c00 10.909% 12.727%,#ffed00 12.727% 14.545%,#00a651 14.545% 16.364%,#06f 16.364% 18.182%,#9b2fae 18.182% 20%);background-attachment:fixed}.\!panel,.panel{border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity));padding:0;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.panel-section{border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(23 23 23 / var(--tw-bg-opacity));padding:0;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.panel-muted{border-radius:.375rem;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.surface{border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity));padding:.125rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.surface-muted{border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity));padding:.125rem;--tw-text-opacity: 1;color:rgb(245 245 245 / var(--tw-text-opacity))}.field-input{border-radius:.375rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity));padding:.125rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.field-input::-moz-placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.field-input::placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.field-input:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-opacity: 1;--tw-ring-color: rgb(14 165 233 / var(--tw-ring-opacity))}.button-dark{border-radius:.375rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(125 211 252 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(2 132 199 / var(--tw-bg-opacity));padding:.125rem;font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.button-dark:hover{--tw-border-opacity: 1;border-color:rgb(14 165 233 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(14 165 233 / var(--tw-bg-opacity))}.button-danger{border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(225 29 72 / var(--tw-bg-opacity));padding:.125rem;font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.button-danger:hover{--tw-bg-opacity: 1;background-color:rgb(244 63 94 / var(--tw-bg-opacity))}.chat-composer{container-type:inline-size;display:flex;flex-wrap:wrap;align-items:stretch;gap:.125rem;min-width:0;overflow:hidden}.chat-composer-nickname{flex:0 0 5rem;min-width:0;order:1}.chat-composer-input{flex:1 1 0%;min-width:0;order:2}.chat-composer-send{flex:0 0 auto;align-self:stretch;order:3}.chat-composer-tts{display:flex;flex:1 1 100%;align-items:center;gap:.125rem;min-width:0;overflow:hidden;order:4}@container (min-width: 44rem){.chat-composer{flex-wrap:nowrap}.chat-composer-nickname{flex-basis:7rem;order:1}.chat-composer-input{order:2}.chat-composer-tts{flex:0 1 auto;order:3}.chat-composer-send{order:4}}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.\!visible{visibility:visible!important}.visible{visibility:visible}.collapse{visibility:collapse}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-x-0{left:0;right:0}.inset-x-1{left:.25rem;right:.25rem}.bottom-0{bottom:0}.bottom-0\.5{bottom:.125rem}.bottom-1{bottom:.25rem}.bottom-2{bottom:.5rem}.bottom-4{bottom:1rem}.bottom-\[calc\(100\%\+0\.125rem\)\]{bottom:calc(100% + .125rem)}.left-0{left:0}.left-0\.5{left:.125rem}.left-1{left:.25rem}.left-1\/2{left:50%}.left-2{left:.5rem}.left-4{left:1rem}.right-0{right:0}.right-0\.5{right:.125rem}.right-1{right:.25rem}.right-2{right:.5rem}.top-0{top:0}.top-0\.5{top:.125rem}.top-1{top:.25rem}.top-1\/2{top:50%}.top-10{top:2.5rem}.top-6{top:1.5rem}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[1000\]{z-index:1000}.z-\[110\]{z-index:110}.z-\[120\]{z-index:120}.z-\[130\]{z-index:130}.z-\[2000\]{z-index:2000}.z-\[70\]{z-index:70}.order-1{order:1}.order-2{order:2}.col-span-full{grid-column:1 / -1}.m-0{margin:0}.-mx-0\.5{margin-left:-.125rem;margin-right:-.125rem}.mx-auto{margin-left:auto;margin-right:auto}.my-\[-0\.125rem\]{margin-top:-.125rem;margin-bottom:-.125rem}.-mt-0\.5{margin-top:-.125rem}.mb-0\.5{margin-bottom:.125rem}.ml-0\.5{margin-left:.125rem}.ml-1{margin-left:.25rem}.mr-0{margin-right:0}.mt-0{margin-top:0}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-\[0\.35vh\]{margin-top:.35vh}.mt-auto{margin-top:auto}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.\!block{display:block!important}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.aspect-\[4\/3\]{aspect-ratio:4/3}.aspect-square{aspect-ratio:1 / 1}.aspect-video{aspect-ratio:16 / 9}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-28{height:7rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-40{height:10rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-64{height:16rem}.h-8{height:2rem}.h-\[2px\]{height:2px}.h-\[3\.5rem\]{height:3.5rem}.h-\[70\%\]{height:70%}.h-\[7rem\]{height:7rem}.h-\[8vh\]{height:8vh}.h-\[calc\(1rem\+0\.25rem\)\]{height:1.25rem}.h-\[min\(100svh\,32rem\)\]{height:min(100svh,32rem)}.h-\[min\(60svh\,24rem\)\]{height:min(60svh,24rem)}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-32{max-height:8rem}.max-h-36{max-height:9rem}.max-h-40{max-height:10rem}.max-h-52{max-height:13rem}.max-h-72{max-height:18rem}.max-h-80{max-height:20rem}.max-h-\[14rem\]{max-height:14rem}.max-h-\[36rem\]{max-height:36rem}.max-h-\[70vh\]{max-height:70vh}.max-h-\[72vh\]{max-height:72vh}.max-h-\[82vh\]{max-height:82vh}.max-h-\[85vh\]{max-height:85vh}.max-h-\[86vh\]{max-height:86vh}.max-h-\[90vh\]{max-height:90vh}.max-h-screen{max-height:100vh}.min-h-0{min-height:0px}.min-h-5{min-height:1.25rem}.min-h-9{min-height:2.25rem}.min-h-\[10rem\]{min-height:10rem}.min-h-\[14rem\]{min-height:14rem}.min-h-\[16rem\]{min-height:16rem}.min-h-\[18rem\]{min-height:18rem}.min-h-\[3\.5rem\]{min-height:3.5rem}.min-h-\[4rem\]{min-height:4rem}.min-h-\[5rem\]{min-height:5rem}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-1{width:.25rem}.w-1\.5{width:.375rem}.w-1\/2{width:50%}.w-10{width:2.5rem}.w-2{width:.5rem}.w-20{width:5rem}.w-28{width:7rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-80{width:20rem}.w-\[12\.5rem\]{width:12.5rem}.w-\[12rem\]{width:12rem}.w-\[20rem\]{width:20rem}.w-\[2px\]{width:2px}.w-\[3\.25rem\]{width:3.25rem}.w-\[4\.5rem\]{width:4.5rem}.w-\[5\.5rem\]{width:5.5rem}.w-\[5rem\]{width:5rem}.w-\[9rem\]{width:9rem}.w-\[calc\(1rem\+0\.25rem\)\]{width:1.25rem}.w-\[min\(20rem\,calc\(100vw-1rem\)\)\]{width:min(20rem,calc(100vw - 1rem))}.w-auto{width:auto}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.w-px{width:1px}.w-screen{width:100vw}.min-w-0{min-width:0px}.min-w-28{min-width:7rem}.min-w-\[0\]{min-width:0}.min-w-\[3\.7rem\]{min-width:3.7rem}.min-w-\[9\.5rem\]{min-width:9.5rem}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.max-w-\[24rem\]{max-width:24rem}.max-w-\[40vw\]{max-width:40vw}.max-w-\[70vw\]{max-width:70vw}.max-w-\[80vw\]{max-width:80vw}.max-w-\[92\%\]{max-width:92%}.max-w-\[92vw\]{max-width:92vw}.max-w-\[94\%\]{max-width:94%}.max-w-\[calc\(100vw-0\.5rem\)\]{max-width:calc(100vw - .5rem)}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-\[0\.72\]{flex:.72}.flex-\[1\.1\]{flex:1.1}.flex-\[1\.22\]{flex:1.22}.flex-\[1\.28\]{flex:1.28}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow-\[0\.75\]{flex-grow:.75}.grow-\[0\.9\]{flex-grow:.9}.grow-\[1\]{flex-grow:1}.basis-0{flex-basis:0px}.origin-top-left{transform-origin:top left}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.auto-rows-fr{grid-auto-rows:minmax(0,1fr)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-\[1\.5rem_minmax\(0\,1fr\)_3rem\]{grid-template-columns:1.5rem minmax(0,1fr) 3rem}.grid-cols-\[auto_minmax\(0\,1fr\)\]{grid-template-columns:auto minmax(0,1fr)}.grid-cols-\[minmax\(0\,0\.7fr\)_minmax\(0\,2\.1fr\)_minmax\(0\,0\.7fr\)\]{grid-template-columns:minmax(0,.7fr) minmax(0,2.1fr) minmax(0,.7fr)}.grid-cols-\[minmax\(0\,1\.2fr\)_repeat\(4\,minmax\(0\,1fr\)\)\]{grid-template-columns:minmax(0,1.2fr) repeat(4,minmax(0,1fr))}.grid-cols-\[minmax\(0\,1\.35fr\)_minmax\(0\,0\.95fr\)\]{grid-template-columns:minmax(0,1.35fr) minmax(0,.95fr)}.grid-cols-\[minmax\(0\,1\.3fr\)_minmax\(0\,0\.22fr\)\]{grid-template-columns:minmax(0,1.3fr) minmax(0,.22fr)}.grid-cols-\[minmax\(0\,1\.45fr\)_minmax\(0\,0\.85fr\)\]{grid-template-columns:minmax(0,1.45fr) minmax(0,.85fr)}.grid-cols-\[minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,1fr)}.grid-cols-\[minmax\(0\,1fr\)_2\.5rem\]{grid-template-columns:minmax(0,1fr) 2.5rem}.grid-cols-\[minmax\(0\,1fr\)_4\.75rem\]{grid-template-columns:minmax(0,1fr) 4.75rem}.grid-cols-\[minmax\(0\,1fr\)_auto\]{grid-template-columns:minmax(0,1fr) auto}.grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,0\.7fr\)_minmax\(0\,1\.9fr\)_minmax\(0\,0\.7fr\)\]{grid-template-columns:minmax(0,1fr) minmax(0,.7fr) minmax(0,1.9fr) minmax(0,.7fr)}.grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1\.4fr\)\]{grid-template-columns:minmax(0,1fr) minmax(0,1.4fr)}.grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.grid-rows-\[auto_1fr\]{grid-template-rows:auto 1fr}.grid-rows-\[auto_auto_1fr\]{grid-template-rows:auto auto 1fr}.grid-rows-\[auto_auto_1fr_auto\]{grid-template-rows:auto auto 1fr auto}.grid-rows-\[auto_auto_auto_1fr\]{grid-template-rows:auto auto auto 1fr}.grid-rows-\[auto_minmax\(0\,1fr\)\]{grid-template-rows:auto minmax(0,1fr)}.grid-rows-\[auto_minmax\(0\,1fr\)_auto\]{grid-template-rows:auto minmax(0,1fr) auto}.grid-rows-\[minmax\(0\,1fr\)\]{grid-template-rows:minmax(0,1fr)}.grid-rows-\[minmax\(0\,1fr\)_auto\]{grid-template-rows:minmax(0,1fr) auto}.grid-rows-\[minmax\(0\,1fr\)_minmax\(0\,1fr\)_minmax\(0\,1fr\)\]{grid-template-rows:minmax(0,1fr) minmax(0,1fr) minmax(0,1fr)}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.content-start{align-content:flex-start}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-stretch{justify-content:stretch}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-4{gap:1rem}.gap-\[0\.4vh\]{gap:.4vh}.gap-\[0\.55vh\]{gap:.55vh}.gap-\[0\.8vw\]{gap:.8vw}.gap-\[1vh\]{gap:1vh}.gap-\[1vw\]{gap:1vw}.gap-\[3vh\]{gap:3vh}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.self-start{align-self:flex-start}.self-stretch{align-self:stretch}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-none{border-radius:0}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-l-4{border-left-width:4px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-amber-200{--tw-border-opacity: 1;border-color:rgb(253 230 138 / var(--tw-border-opacity))}.border-amber-200\/30{border-color:#fde68a4d}.border-amber-200\/70{border-color:#fde68ab3}.border-amber-200\/90{border-color:#fde68ae6}.border-amber-300\/60{border-color:#fcd34d99}.border-amber-300\/70{border-color:#fcd34db3}.border-amber-300\/80{border-color:#fcd34dcc}.border-amber-400\/30{border-color:#fbbf244d}.border-amber-400\/60{border-color:#fbbf2499}.border-amber-600\/60{border-color:#d9770699}.border-amber-700\/60{border-color:#b4530999}.border-blue-600{--tw-border-opacity: 1;border-color:rgb(37 99 235 / var(--tw-border-opacity))}.border-cyan-200{--tw-border-opacity: 1;border-color:rgb(165 243 252 / var(--tw-border-opacity))}.border-cyan-200\/90{border-color:#a5f3fce6}.border-cyan-300{--tw-border-opacity: 1;border-color:rgb(103 232 249 / var(--tw-border-opacity))}.border-cyan-300\/70{border-color:#67e8f9b3}.border-cyan-300\/80{border-color:#67e8f9cc}.border-cyan-500\/40{border-color:#06b6d466}.border-emerald-100\/80{border-color:#d1fae5cc}.border-emerald-200{--tw-border-opacity: 1;border-color:rgb(167 243 208 / var(--tw-border-opacity))}.border-emerald-200\/70{border-color:#a7f3d0b3}.border-emerald-200\/90{border-color:#a7f3d0e6}.border-emerald-300{--tw-border-opacity: 1;border-color:rgb(110 231 183 / var(--tw-border-opacity))}.border-emerald-300\/70{border-color:#6ee7b7b3}.border-emerald-400\/60{border-color:#34d39999}.border-emerald-500\/40{border-color:#10b98166}.border-emerald-700\/60{border-color:#04785799}.border-emerald-700\/70{border-color:#047857b3}.border-emerald-950{--tw-border-opacity: 1;border-color:rgb(2 44 34 / var(--tw-border-opacity))}.border-fuchsia-300\/70{border-color:#f0abfcb3}.border-green-400{--tw-border-opacity: 1;border-color:rgb(74 222 128 / var(--tw-border-opacity))}.border-indigo-200\/70{border-color:#c7d2feb3}.border-indigo-200\/95{border-color:#c7d2fef2}.border-indigo-300\/70{border-color:#a5b4fcb3}.border-indigo-400\/30{border-color:#818cf84d}.border-indigo-700{--tw-border-opacity: 1;border-color:rgb(67 56 202 / var(--tw-border-opacity))}.border-indigo-700\/60{border-color:#4338ca99}.border-neutral-500\/50{border-color:#73737380}.border-neutral-500\/60{border-color:#73737399}.border-neutral-600{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity))}.border-neutral-700{--tw-border-opacity: 1;border-color:rgb(64 64 64 / var(--tw-border-opacity))}.border-neutral-700\/70{border-color:#404040b3}.border-pink-200{--tw-border-opacity: 1;border-color:rgb(251 207 232 / var(--tw-border-opacity))}.border-red-100{--tw-border-opacity: 1;border-color:rgb(254 226 226 / var(--tw-border-opacity))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity))}.border-red-300\/90{border-color:#fca5a5e6}.border-red-400{--tw-border-opacity: 1;border-color:rgb(248 113 113 / var(--tw-border-opacity))}.border-red-400\/60{border-color:#f8717199}.border-rose-300{--tw-border-opacity: 1;border-color:rgb(253 164 175 / var(--tw-border-opacity))}.border-sky-300{--tw-border-opacity: 1;border-color:rgb(125 211 252 / var(--tw-border-opacity))}.border-sky-400\/50{border-color:#38bdf880}.border-sky-400\/70{border-color:#38bdf8b3}.border-slate-200\/30{border-color:#e2e8f04d}.border-slate-400{--tw-border-opacity: 1;border-color:rgb(148 163 184 / var(--tw-border-opacity))}.border-slate-400\/60{border-color:#94a3b899}.border-slate-500\/70{border-color:#64748bb3}.border-slate-600{--tw-border-opacity: 1;border-color:rgb(71 85 105 / var(--tw-border-opacity))}.border-slate-600\/40{border-color:#47556966}.border-slate-600\/60{border-color:#47556999}.border-slate-600\/70{border-color:#475569b3}.border-slate-600\/80{border-color:#475569cc}.border-slate-700{--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity))}.border-slate-700\/60{border-color:#33415599}.border-slate-700\/70{border-color:#334155b3}.border-slate-700\/80{border-color:#334155cc}.border-slate-800{--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity))}.border-slate-800\/60{border-color:#1e293b99}.border-slate-800\/80{border-color:#1e293bcc}.border-transparent{border-color:transparent}.border-white{--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity))}.border-white\/10{border-color:#ffffff1a}.border-white\/40{border-color:#fff6}.border-white\/80{border-color:#fffc}.border-l-neutral-600{--tw-border-opacity: 1;border-left-color:rgb(82 82 82 / var(--tw-border-opacity))}.bg-amber-100\/95{background-color:#fef3c7f2}.bg-amber-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity))}.bg-amber-400\/80{background-color:#fbbf24cc}.bg-amber-500{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity))}.bg-amber-500\/30{background-color:#f59e0b4d}.bg-amber-600{--tw-bg-opacity: 1;background-color:rgb(217 119 6 / var(--tw-bg-opacity))}.bg-amber-600\/70{background-color:#d97706b3}.bg-amber-600\/80{background-color:#d97706cc}.bg-amber-700\/20{background-color:#b4530933}.bg-amber-700\/30{background-color:#b453094d}.bg-amber-700\/35{background-color:#b4530959}.bg-amber-700\/80{background-color:#b45309cc}.bg-amber-800{--tw-bg-opacity: 1;background-color:rgb(146 64 14 / var(--tw-bg-opacity))}.bg-amber-900{--tw-bg-opacity: 1;background-color:rgb(120 53 15 / var(--tw-bg-opacity))}.bg-amber-900\/40{background-color:#78350f66}.bg-amber-900\/60{background-color:#78350f99}.bg-amber-900\/90{background-color:#78350fe6}.bg-amber-950\/35{background-color:#451a0359}.bg-amber-950\/70{background-color:#451a03b3}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity))}.bg-black\/20{background-color:#0003}.bg-black\/30{background-color:#0000004d}.bg-black\/40{background-color:#0006}.bg-black\/45{background-color:#00000073}.bg-black\/50{background-color:#00000080}.bg-black\/55{background-color:#0000008c}.bg-black\/60{background-color:#0009}.bg-black\/70{background-color:#000000b3}.bg-black\/75{background-color:#000000bf}.bg-black\/80{background-color:#000c}.bg-black\/95{background-color:#000000f2}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.bg-cyan-300{--tw-bg-opacity: 1;background-color:rgb(103 232 249 / var(--tw-bg-opacity))}.bg-cyan-500{--tw-bg-opacity: 1;background-color:rgb(6 182 212 / var(--tw-bg-opacity))}.bg-cyan-500\/80{background-color:#06b6d4cc}.bg-cyan-900{--tw-bg-opacity: 1;background-color:rgb(22 78 99 / var(--tw-bg-opacity))}.bg-cyan-900\/45{background-color:#164e6373}.bg-cyan-900\/90{background-color:#164e63e6}.bg-emerald-200{--tw-bg-opacity: 1;background-color:rgb(167 243 208 / var(--tw-bg-opacity))}.bg-emerald-400{--tw-bg-opacity: 1;background-color:rgb(52 211 153 / var(--tw-bg-opacity))}.bg-emerald-500{--tw-bg-opacity: 1;background-color:rgb(16 185 129 / var(--tw-bg-opacity))}.bg-emerald-500\/45{background-color:#10b98173}.bg-emerald-500\/80{background-color:#10b981cc}.bg-emerald-600{--tw-bg-opacity: 1;background-color:rgb(5 150 105 / var(--tw-bg-opacity))}.bg-emerald-600\/70{background-color:#059669b3}.bg-emerald-600\/80{background-color:#059669cc}.bg-emerald-700{--tw-bg-opacity: 1;background-color:rgb(4 120 87 / var(--tw-bg-opacity))}.bg-emerald-700\/20{background-color:#04785733}.bg-emerald-700\/60{background-color:#04785799}.bg-emerald-700\/70{background-color:#047857b3}.bg-emerald-800{--tw-bg-opacity: 1;background-color:rgb(6 95 70 / var(--tw-bg-opacity))}.bg-emerald-900{--tw-bg-opacity: 1;background-color:rgb(6 78 59 / var(--tw-bg-opacity))}.bg-emerald-900\/60{background-color:#064e3b99}.bg-emerald-900\/90{background-color:#064e3be6}.bg-emerald-950{--tw-bg-opacity: 1;background-color:rgb(2 44 34 / var(--tw-bg-opacity))}.bg-emerald-950\/50{background-color:#022c2280}.bg-fuchsia-200{--tw-bg-opacity: 1;background-color:rgb(245 208 254 / var(--tw-bg-opacity))}.bg-fuchsia-600{--tw-bg-opacity: 1;background-color:rgb(192 38 211 / var(--tw-bg-opacity))}.bg-fuchsia-700{--tw-bg-opacity: 1;background-color:rgb(162 28 175 / var(--tw-bg-opacity))}.bg-fuchsia-800{--tw-bg-opacity: 1;background-color:rgb(134 25 143 / var(--tw-bg-opacity))}.bg-fuchsia-900\/45{background-color:#701a7573}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity))}.bg-indigo-600{--tw-bg-opacity: 1;background-color:rgb(79 70 229 / var(--tw-bg-opacity))}.bg-indigo-600\/70{background-color:#4f46e5b3}.bg-indigo-900{--tw-bg-opacity: 1;background-color:rgb(49 46 129 / var(--tw-bg-opacity))}.bg-indigo-900\/60{background-color:#312e8199}.bg-indigo-950{--tw-bg-opacity: 1;background-color:rgb(30 27 75 / var(--tw-bg-opacity))}.bg-indigo-950\/90{background-color:#1e1b4be6}.bg-neutral-700{--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity))}.bg-neutral-800{--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity))}.bg-neutral-800\/80{background-color:#262626cc}.bg-neutral-900{--tw-bg-opacity: 1;background-color:rgb(23 23 23 / var(--tw-bg-opacity))}.bg-neutral-900\/100{background-color:#171717}.bg-neutral-900\/60{background-color:#17171799}.bg-neutral-900\/70{background-color:#171717b3}.bg-neutral-900\/95{background-color:#171717f2}.bg-neutral-950{--tw-bg-opacity: 1;background-color:rgb(10 10 10 / var(--tw-bg-opacity))}.bg-pink-500{--tw-bg-opacity: 1;background-color:rgb(236 72 153 / var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.bg-red-500\/45{background-color:#ef444473}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity))}.bg-red-600\/70{background-color:#dc2626b3}.bg-red-700{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity))}.bg-red-700\/20{background-color:#b91c1c33}.bg-red-700\/60{background-color:#b91c1c99}.bg-red-700\/80{background-color:#b91c1ccc}.bg-red-800{--tw-bg-opacity: 1;background-color:rgb(153 27 27 / var(--tw-bg-opacity))}.bg-red-900\/35{background-color:#7f1d1d59}.bg-red-900\/40{background-color:#7f1d1d66}.bg-red-900\/45{background-color:#7f1d1d73}.bg-red-900\/50{background-color:#7f1d1d80}.bg-red-900\/80{background-color:#7f1d1dcc}.bg-red-950\/70{background-color:#450a0ab3}.bg-rose-500{--tw-bg-opacity: 1;background-color:rgb(244 63 94 / var(--tw-bg-opacity))}.bg-rose-600{--tw-bg-opacity: 1;background-color:rgb(225 29 72 / var(--tw-bg-opacity))}.bg-sky-600{--tw-bg-opacity: 1;background-color:rgb(2 132 199 / var(--tw-bg-opacity))}.bg-sky-700\/20{background-color:#0369a133}.bg-sky-700\/70{background-color:#0369a1b3}.bg-slate-300{--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity))}.bg-slate-500{--tw-bg-opacity: 1;background-color:rgb(100 116 139 / var(--tw-bg-opacity))}.bg-slate-600{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}.bg-slate-600\/70{background-color:#475569b3}.bg-slate-700{--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}.bg-slate-700\/30{background-color:#3341554d}.bg-slate-700\/60{background-color:#33415599}.bg-slate-700\/70{background-color:#334155b3}.bg-slate-700\/80{background-color:#334155cc}.bg-slate-800{--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}.bg-slate-800\/60{background-color:#1e293b99}.bg-slate-800\/70{background-color:#1e293bb3}.bg-slate-800\/80{background-color:#1e293bcc}.bg-slate-900{--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity))}.bg-slate-900\/35{background-color:#0f172a59}.bg-slate-900\/40{background-color:#0f172a66}.bg-slate-900\/70{background-color:#0f172ab3}.bg-slate-950{--tw-bg-opacity: 1;background-color:rgb(2 6 23 / var(--tw-bg-opacity))}.bg-slate-950\/35{background-color:#02061759}.bg-slate-950\/80{background-color:#020617cc}.bg-slate-950\/90{background-color:#020617e6}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-white\/10{background-color:#ffffff1a}.bg-yellow-100{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity))}.bg-zinc-900{--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity))}.bg-zinc-900\/90{background-color:#18181be6}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-lime-800{--tw-gradient-from: #3f6212 var(--tw-gradient-from-position);--tw-gradient-to: rgb(63 98 18 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-neutral-800{--tw-gradient-from: #262626 var(--tw-gradient-from-position);--tw-gradient-to: rgb(38 38 38 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.via-neutral-700{--tw-gradient-to: rgb(64 64 64 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), #404040 var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-neutral-600{--tw-gradient-to: #525252 var(--tw-gradient-to-position)}.to-teal-800{--tw-gradient-to: #115e59 var(--tw-gradient-to-position)}.bg-cover{background-size:cover}.bg-center{background-position:center}.bg-no-repeat{background-repeat:no-repeat}.fill-slate-100{fill:#f1f5f9}.fill-slate-200{fill:#e2e8f0}.fill-slate-400{fill:#94a3b8}.fill-white{fill:#fff}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-4{padding:1rem}.p-\[0\.85vw\]{padding:.85vw}.px-0{padding-left:0;padding-right:0}.px-0\.5{padding-left:.125rem;padding-right:.125rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-\[1\.6vw\]{padding-left:1.6vw;padding-right:1.6vw}.px-\[1vw\]{padding-left:1vw;padding-right:1vw}.px-\[3vw\]{padding-left:3vw;padding-right:3vw}.px-\[4vw\]{padding-left:4vw;padding-right:4vw}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-\[0\.55vh\]{padding-top:.55vh;padding-bottom:.55vh}.py-\[1px\]{padding-top:1px;padding-bottom:1px}.py-\[2\.5vh\]{padding-top:2.5vh;padding-bottom:2.5vh}.py-\[2px\]{padding-top:2px;padding-bottom:2px}.py-\[4vh\]{padding-top:4vh;padding-bottom:4vh}.pb-0{padding-bottom:0}.pb-0\.5{padding-bottom:.125rem}.pb-1{padding-bottom:.25rem}.pb-3{padding-bottom:.75rem}.pr-0{padding-right:0}.pr-1{padding-right:.25rem}.pt-1{padding-top:.25rem}.pt-5{padding-top:1.25rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[0\.45rem\]{font-size:.45rem}.text-\[0\.48rem\]{font-size:.48rem}.text-\[0\.52rem\]{font-size:.52rem}.text-\[0\.55em\]{font-size:.55em}.text-\[0\.55rem\]{font-size:.55rem}.text-\[0\.58rem\]{font-size:.58rem}.text-\[0\.5rem\]{font-size:.5rem}.text-\[0\.62rem\]{font-size:.62rem}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.68rem\]{font-size:.68rem}.text-\[0\.6rem\]{font-size:.6rem}.text-\[0\.72rem\]{font-size:.72rem}.text-\[0\.75rem\]{font-size:.75rem}.text-\[0\.78rem\]{font-size:.78rem}.text-\[0\.7rem\]{font-size:.7rem}.text-\[0\.82rem\]{font-size:.82rem}.text-\[0\.85rem\]{font-size:.85rem}.text-\[0\.8rem\]{font-size:.8rem}.text-\[1\.1rem\]{font-size:1.1rem}.text-\[1\.5rem\]{font-size:1.5rem}.text-\[17vw\]{font-size:17vw}.text-\[1rem\]{font-size:1rem}.text-\[2\.5vw\]{font-size:2.5vw}.text-\[2\.6vw\]{font-size:2.6vw}.text-\[3\.1vw\]{font-size:3.1vw}.text-\[3vw\]{font-size:3vw}.text-\[4vw\]{font-size:4vw}.text-\[6vw\]{font-size:6vw}.text-\[7vw\]{font-size:7vw}.text-\[clamp\(1\.25rem\,2\.5vh\,2\.6rem\)\]{font-size:clamp(1.25rem,2.5vh,2.6rem)}.text-\[clamp\(1\.2rem\,2\.9vh\,3rem\)\]{font-size:clamp(1.2rem,2.9vh,3rem)}.text-\[clamp\(2\.5rem\,5\.3vh\,5\.8rem\)\]{font-size:clamp(2.5rem,5.3vh,5.8rem)}.text-\[clamp\(2\.5rem\,7vh\,7rem\)\]{font-size:clamp(2.5rem,7vh,7rem)}.text-\[clamp\(2\.5rem\,8vh\,8rem\)\]{font-size:clamp(2.5rem,8vh,8rem)}.text-\[clamp\(2rem\,5vh\,5\.2rem\)\]{font-size:clamp(2rem,5vh,5.2rem)}.text-\[clamp\(3\.2rem\,11vh\,11rem\)\]{font-size:clamp(3.2rem,11vh,11rem)}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-black{font-weight:900}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.leading-\[0\.95\]{line-height:.95}.leading-none{line-height:1}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-normal{letter-spacing:0em}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.text-amber-100{--tw-text-opacity: 1;color:rgb(254 243 199 / var(--tw-text-opacity))}.text-amber-200{--tw-text-opacity: 1;color:rgb(253 230 138 / var(--tw-text-opacity))}.text-amber-200\/80{color:#fde68acc}.text-amber-200\/85{color:#fde68ad9}.text-amber-300{--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity))}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity))}.text-amber-50{--tw-text-opacity: 1;color:rgb(255 251 235 / var(--tw-text-opacity))}.text-amber-50\/90{color:#fffbebe6}.text-amber-950{--tw-text-opacity: 1;color:rgb(69 26 3 / var(--tw-text-opacity))}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.text-cyan-100{--tw-text-opacity: 1;color:rgb(207 250 254 / var(--tw-text-opacity))}.text-cyan-200{--tw-text-opacity: 1;color:rgb(165 243 252 / var(--tw-text-opacity))}.text-cyan-300{--tw-text-opacity: 1;color:rgb(103 232 249 / var(--tw-text-opacity))}.text-cyan-50{--tw-text-opacity: 1;color:rgb(236 254 255 / var(--tw-text-opacity))}.text-emerald-100{--tw-text-opacity: 1;color:rgb(209 250 229 / var(--tw-text-opacity))}.text-emerald-100\/80{color:#d1fae5cc}.text-emerald-200{--tw-text-opacity: 1;color:rgb(167 243 208 / var(--tw-text-opacity))}.text-emerald-300{--tw-text-opacity: 1;color:rgb(110 231 183 / var(--tw-text-opacity))}.text-emerald-400{--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity))}.text-emerald-50{--tw-text-opacity: 1;color:rgb(236 253 245 / var(--tw-text-opacity))}.text-emerald-50\/90{color:#ecfdf5e6}.text-emerald-950{--tw-text-opacity: 1;color:rgb(2 44 34 / var(--tw-text-opacity))}.text-fuchsia-50{--tw-text-opacity: 1;color:rgb(253 244 255 / var(--tw-text-opacity))}.text-fuchsia-900{--tw-text-opacity: 1;color:rgb(112 26 117 / var(--tw-text-opacity))}.text-indigo-200{--tw-text-opacity: 1;color:rgb(199 210 254 / var(--tw-text-opacity))}.text-indigo-50{--tw-text-opacity: 1;color:rgb(238 242 255 / var(--tw-text-opacity))}.text-indigo-50\/90{color:#eef2ffe6}.text-lime-300{--tw-text-opacity: 1;color:rgb(190 242 100 / var(--tw-text-opacity))}.text-lime-400{--tw-text-opacity: 1;color:rgb(163 230 53 / var(--tw-text-opacity))}.text-neutral-100{--tw-text-opacity: 1;color:rgb(245 245 245 / var(--tw-text-opacity))}.text-neutral-200{--tw-text-opacity: 1;color:rgb(229 229 229 / var(--tw-text-opacity))}.text-neutral-300{--tw-text-opacity: 1;color:rgb(212 212 212 / var(--tw-text-opacity))}.text-neutral-400{--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity))}.text-neutral-50{--tw-text-opacity: 1;color:rgb(250 250 250 / var(--tw-text-opacity))}.text-neutral-500{--tw-text-opacity: 1;color:rgb(115 115 115 / var(--tw-text-opacity))}.text-red-100{--tw-text-opacity: 1;color:rgb(254 226 226 / var(--tw-text-opacity))}.text-red-100\/90{color:#fee2e2e6}.text-red-100\/95{color:#fee2e2f2}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}.text-red-50{--tw-text-opacity: 1;color:rgb(254 242 242 / var(--tw-text-opacity))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity))}.text-rose-300{--tw-text-opacity: 1;color:rgb(253 164 175 / var(--tw-text-opacity))}.text-sky-200{--tw-text-opacity: 1;color:rgb(186 230 253 / var(--tw-text-opacity))}.text-sky-300{--tw-text-opacity: 1;color:rgb(125 211 252 / var(--tw-text-opacity))}.text-sky-50{--tw-text-opacity: 1;color:rgb(240 249 255 / var(--tw-text-opacity))}.text-slate-100{--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}.text-slate-200{--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity))}.text-slate-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.text-slate-400\/60{color:#94a3b899}.text-slate-50{--tw-text-opacity: 1;color:rgb(248 250 252 / var(--tw-text-opacity))}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}.text-slate-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity))}.text-slate-950{--tw-text-opacity: 1;color:rgb(2 6 23 / var(--tw-text-opacity))}.text-teal-400{--tw-text-opacity: 1;color:rgb(45 212 191 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-white\/90{color:#ffffffe6}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity))}.accent-cyan-500{accent-color:#06b6d4}.accent-emerald-400{accent-color:#34d399}.accent-emerald-500{accent-color:#10b981}.accent-red-500{accent-color:#ef4444}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-40{opacity:.4}.opacity-45{opacity:.45}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.opacity-85{opacity:.85}.opacity-90{opacity:.9}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_1px_0_rgba\(255\,255\,255\,0\.05\)_inset\,0_10px_24px_rgba\(0\,0\,0\,0\.28\)\]{--tw-shadow: 0 1px 0 rgba(255,255,255,.05) inset,0 10px 24px rgba(0,0,0,.28);--tw-shadow-colored: inset 0 1px 0 var(--tw-shadow-color), 0 10px 24px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / .05);--tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-black\/40{--tw-shadow-color: rgb(0 0 0 / .4);--tw-shadow: var(--tw-shadow-colored)}.shadow-cyan-950\/50{--tw-shadow-color: rgb(8 51 68 / .5);--tw-shadow: var(--tw-shadow-colored)}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-4{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-amber-200\/60{--tw-ring-color: rgb(253 230 138 / .6)}.ring-amber-300{--tw-ring-opacity: 1;--tw-ring-color: rgb(252 211 77 / var(--tw-ring-opacity))}.ring-amber-300\/80{--tw-ring-color: rgb(252 211 77 / .8)}.ring-emerald-300\/50{--tw-ring-color: rgb(110 231 183 / .5)}.ring-emerald-300\/70{--tw-ring-color: rgb(110 231 183 / .7)}.ring-red-300\/60{--tw-ring-color: rgb(252 165 165 / .6)}.ring-red-500\/70{--tw-ring-color: rgb(239 68 68 / .7)}.ring-red-500\/80{--tw-ring-color: rgb(239 68 68 / .8)}.ring-red-500\/90{--tw-ring-color: rgb(239 68 68 / .9)}.ring-sky-300\/50{--tw-ring-color: rgb(125 211 252 / .5)}.ring-sky-400\/80{--tw-ring-color: rgb(56 189 248 / .8)}.ring-slate-400\/40{--tw-ring-color: rgb(148 163 184 / .4)}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.grayscale{--tw-grayscale: grayscale(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert: invert(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.sepia{--tw-sepia: sepia(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[width\,height\]{transition-property:width,height;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-500{transition-duration:.5s}.duration-700{transition-duration:.7s}@keyframes batteryTickWarn{0%,49%{background-color:#fffffff2}50%,to{background-color:#ef4444f2}}.battery-tick-warn{animation:batteryTickWarn .4s steps(2,end) infinite}@keyframes batteryUrgentFlash{0%,49%{opacity:1}50%,to{opacity:.25}}.battery-urgent-flash{animation:batteryUrgentFlash .4s steps(2,end) infinite}.no-touch-select{-moz-user-select:none;user-select:none;-webkit-user-select:none;-ms-user-select:none;-webkit-touch-callout:none}.mobile-touch-control{user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;touch-action:manipulation;overscroll-behavior:contain}.mobile-drag-control{touch-action:none}.mobile-text-entry{font-size:16px;-webkit-text-size-adjust:100%;touch-action:manipulation}.\[writing-mode\:vertical-rl\]{writing-mode:vertical-rl}:root{font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}body{margin:0;min-height:100vh;--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}html,body,*{scrollbar-width:thin;scrollbar-color:rgba(148,163,184,.55) transparent}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}*::-webkit-scrollbar{width:2px;height:2px}*::-webkit-scrollbar-track{background:transparent}*::-webkit-scrollbar-thumb{background-color:#94a3b88c;border-radius:9999px}@supports (scrollbar-width: thin){@media(pointer:coarse){html,body,*{scrollbar-width:none}}}.placeholder\:text-slate-400::-moz-placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.placeholder\:text-slate-400::placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.last\:mb-0:last-child{margin-bottom:0}.hover\:-translate-y-0\.5:hover{--tw-translate-y: -.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-110:hover{--tw-scale-x: 1.1;--tw-scale-y: 1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-green-100:hover{--tw-border-opacity: 1;border-color:rgb(220 252 231 / var(--tw-border-opacity))}.hover\:border-rose-500:hover{--tw-border-opacity: 1;border-color:rgb(244 63 94 / var(--tw-border-opacity))}.hover\:border-sky-500:hover{--tw-border-opacity: 1;border-color:rgb(14 165 233 / var(--tw-border-opacity))}.hover\:border-white:hover{--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity))}.hover\:border-white\/60:hover{border-color:#fff9}.hover\:bg-amber-400:hover{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity))}.hover\:bg-amber-600:hover{--tw-bg-opacity: 1;background-color:rgb(217 119 6 / var(--tw-bg-opacity))}.hover\:bg-amber-800:hover{--tw-bg-opacity: 1;background-color:rgb(146 64 14 / var(--tw-bg-opacity))}.hover\:bg-cyan-400:hover{--tw-bg-opacity: 1;background-color:rgb(34 211 238 / var(--tw-bg-opacity))}.hover\:bg-cyan-800:hover{--tw-bg-opacity: 1;background-color:rgb(21 94 117 / var(--tw-bg-opacity))}.hover\:bg-emerald-400:hover{--tw-bg-opacity: 1;background-color:rgb(52 211 153 / var(--tw-bg-opacity))}.hover\:bg-emerald-500:hover{--tw-bg-opacity: 1;background-color:rgb(16 185 129 / var(--tw-bg-opacity))}.hover\:bg-emerald-700:hover{--tw-bg-opacity: 1;background-color:rgb(4 120 87 / var(--tw-bg-opacity))}.hover\:bg-fuchsia-500:hover{--tw-bg-opacity: 1;background-color:rgb(217 70 239 / var(--tw-bg-opacity))}.hover\:bg-green-400:hover{--tw-bg-opacity: 1;background-color:rgb(74 222 128 / var(--tw-bg-opacity))}.hover\:bg-indigo-500:hover{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity))}.hover\:bg-indigo-800:hover{--tw-bg-opacity: 1;background-color:rgb(55 48 163 / var(--tw-bg-opacity))}.hover\:bg-neutral-700:hover{--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity))}.hover\:bg-pink-400:hover{--tw-bg-opacity: 1;background-color:rgb(244 114 182 / var(--tw-bg-opacity))}.hover\:bg-red-600:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity))}.hover\:bg-rose-500:hover{--tw-bg-opacity: 1;background-color:rgb(244 63 94 / var(--tw-bg-opacity))}.hover\:bg-sky-500:hover{--tw-bg-opacity: 1;background-color:rgb(14 165 233 / var(--tw-bg-opacity))}.hover\:bg-slate-600:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}.hover\:bg-slate-700\/70:hover{background-color:#334155b3}.hover\:bg-white\/10:hover{background-color:#ffffff1a}.hover\:bg-zinc-900:hover{--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.hover\:opacity-90:hover{opacity:.9}.hover\:shadow-xl:hover{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:brightness-110:hover{--tw-brightness: brightness(1.1);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.focus\:bg-white\/10:focus{background-color:#ffffff1a}.focus\:text-white:focus{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-emerald-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(16 185 129 / var(--tw-ring-opacity))}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:outline:focus-visible{outline-style:solid}.focus-visible\:outline-1:focus-visible{outline-width:1px}.focus-visible\:outline-offset-1:focus-visible{outline-offset:1px}.focus-visible\:outline-slate-500:focus-visible{outline-color:#64748b}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-amber-300:focus-visible{--tw-ring-opacity: 1;--tw-ring-color: rgb(252 211 77 / var(--tw-ring-opacity))}.focus-visible\:ring-cyan-300:focus-visible{--tw-ring-opacity: 1;--tw-ring-color: rgb(103 232 249 / var(--tw-ring-opacity))}.focus-visible\:ring-emerald-200:focus-visible{--tw-ring-opacity: 1;--tw-ring-color: rgb(167 243 208 / var(--tw-ring-opacity))}.focus-visible\:ring-emerald-300:focus-visible{--tw-ring-opacity: 1;--tw-ring-color: rgb(110 231 183 / var(--tw-ring-opacity))}.focus-visible\:ring-indigo-300:focus-visible{--tw-ring-opacity: 1;--tw-ring-color: rgb(165 180 252 / var(--tw-ring-opacity))}.active\:scale-95:active{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:scale-\[0\.99\]:active{--tw-scale-x: .99;--tw-scale-y: .99;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:brightness-125:active{--tw-brightness: brightness(1.25);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-60:disabled{opacity:.6}.disabled\:opacity-70:disabled{opacity:.7}@media(max-width:520px){.max-\[520px\]\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.max-\[520px\]\:justify-start{justify-content:flex-start}}@media(max-width:420px){.max-\[420px\]\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.max-\[420px\]\:grid-cols-\[auto_minmax\(0\,1fr\)\]{grid-template-columns:auto minmax(0,1fr)}.max-\[420px\]\:justify-start{justify-content:flex-start}}@media(min-width:640px){.sm\:block{display:block}.sm\:min-h-\[18rem\]{min-height:18rem}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:grid-cols-\[minmax\(0\,0\.8fr\)_minmax\(0\,1\.2fr\)\]{grid-template-columns:minmax(0,.8fr) minmax(0,1.2fr)}.sm\:text-2xl{font-size:1.5rem;line-height:2rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}}@media(min-width:768px){.md\:h-full{height:100%}.md\:h-screen{height:100vh}.md\:min-h-0{min-height:0px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-\[0\.9fr_1fr_1\.3fr\]{grid-template-columns:.9fr 1fr 1.3fr}.md\:grid-cols-\[minmax\(0\,1\.4fr\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,1.4fr) minmax(0,1fr)}.md\:grid-cols-\[minmax\(0\,1\.5fr\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,1.5fr) minmax(0,1fr)}.md\:grid-cols-\[minmax\(0\,1fr\)_10rem\]{grid-template-columns:minmax(0,1fr) 10rem}.md\:grid-cols-\[minmax\(0\,1fr\)_18rem\]{grid-template-columns:minmax(0,1fr) 18rem}.md\:grid-cols-\[minmax\(0\,1fr\)_auto\]{grid-template-columns:minmax(0,1fr) auto}.md\:grid-cols-\[minmax\(0\,1fr\)_auto_auto\]{grid-template-columns:minmax(0,1fr) auto auto}.md\:overflow-hidden{overflow:hidden}.md\:overflow-y-auto{overflow-y:auto}.md\:text-lg{font-size:1.125rem;line-height:1.75rem}.md\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media(min-width:1024px){.lg\:col-span-2{grid-column:span 2 / span 2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:grid-cols-\[24rem_minmax\(0\,1fr\)\]{grid-template-columns:24rem minmax(0,1fr)}.lg\:grid-cols-\[minmax\(0\,1\.25fr\)_minmax\(0\,0\.9fr\)\]{grid-template-columns:minmax(0,1.25fr) minmax(0,.9fr)}.lg\:grid-cols-\[minmax\(0\,1fr\)_20rem\]{grid-template-columns:minmax(0,1fr) 20rem}.lg\:grid-cols-\[minmax\(0\,1fr\)_9rem_10rem\]{grid-template-columns:minmax(0,1fr) 9rem 10rem}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:text-right{text-align:right}}@media(min-width:1280px){.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}} diff --git a/server/public/assets/index-D5vPzVhj.js b/server/public/assets/index-D5vPzVhj.js new file mode 100644 index 00000000..f7a138b8 --- /dev/null +++ b/server/public/assets/index-D5vPzVhj.js @@ -0,0 +1,148 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/OrbitControls-BJ-UXxdt.js","assets/three.module--MGUDD-H.js"])))=>i.map(i=>d[i]); +(function(){const c=document.createElement("link").relList;if(c&&c.supports&&c.supports("modulepreload"))return;for(const u of document.querySelectorAll('link[rel="modulepreload"]'))i(u);new MutationObserver(u=>{for(const h of u)if(h.type==="childList")for(const d of h.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&i(d)}).observe(document,{childList:!0,subtree:!0});function n(u){const h={};return u.integrity&&(h.integrity=u.integrity),u.referrerPolicy&&(h.referrerPolicy=u.referrerPolicy),u.crossOrigin==="use-credentials"?h.credentials="include":u.crossOrigin==="anonymous"?h.credentials="omit":h.credentials="same-origin",h}function i(u){if(u.ep)return;u.ep=!0;const h=n(u);fetch(u.href,h)}})();function Ev(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var ic={exports:{}},R8={};var gs;function Tv(){if(gs)return R8;gs=1;var t=Symbol.for("react.transitional.element"),c=Symbol.for("react.fragment");function n(i,u,h){var d=null;if(h!==void 0&&(d=""+h),u.key!==void 0&&(d=""+u.key),"key"in u){h={};for(var m in u)m!=="key"&&(h[m]=u[m])}else h=u;return u=h.ref,{$$typeof:t,type:i,key:d,ref:u!==void 0?u:null,props:h}}return R8.Fragment=c,R8.jsx=n,R8.jsxs=n,R8}var xs;function _v(){return xs||(xs=1,ic.exports=Tv()),ic.exports}var r=_v(),sc={exports:{}},Z1={};var zs;function qv(){if(zs)return Z1;zs=1;var t=Symbol.for("react.transitional.element"),c=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),i=Symbol.for("react.strict_mode"),u=Symbol.for("react.profiler"),h=Symbol.for("react.consumer"),d=Symbol.for("react.context"),m=Symbol.for("react.forward_ref"),p=Symbol.for("react.suspense"),x=Symbol.for("react.memo"),g=Symbol.for("react.lazy"),b=Symbol.for("react.activity"),y=Symbol.iterator;function M(_){return _===null||typeof _!="object"?null:(_=y&&_[y]||_["@@iterator"],typeof _=="function"?_:null)}var L={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},A=Object.assign,w={};function V(_,q,Y){this.props=_,this.context=q,this.refs=w,this.updater=Y||L}V.prototype.isReactComponent={},V.prototype.setState=function(_,q){if(typeof _!="object"&&typeof _!="function"&&_!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,_,q,"setState")},V.prototype.forceUpdate=function(_){this.updater.enqueueForceUpdate(this,_,"forceUpdate")};function H(){}H.prototype=V.prototype;function S(_,q,Y){this.props=_,this.context=q,this.refs=w,this.updater=Y||L}var j=S.prototype=new H;j.constructor=S,A(j,V.prototype),j.isPureReactComponent=!0;var T=Array.isArray;function R(){}var k={H:null,A:null,T:null,S:null},B=Object.prototype.hasOwnProperty;function N(_,q,Y){var r1=Y.ref;return{$$typeof:t,type:_,key:q,ref:r1!==void 0?r1:null,props:Y}}function D(_,q){return N(_.type,q,_.props)}function $(_){return typeof _=="object"&&_!==null&&_.$$typeof===t}function O(_){var q={"=":"=0",":":"=2"};return"$"+_.replace(/[=:]/g,function(Y){return q[Y]})}var Q=/\/+/g;function l1(_,q){return typeof _=="object"&&_!==null&&_.key!=null?O(""+_.key):q.toString(36)}function G(_){switch(_.status){case"fulfilled":return _.value;case"rejected":throw _.reason;default:switch(typeof _.status=="string"?_.then(R,R):(_.status="pending",_.then(function(q){_.status==="pending"&&(_.status="fulfilled",_.value=q)},function(q){_.status==="pending"&&(_.status="rejected",_.reason=q)})),_.status){case"fulfilled":return _.value;case"rejected":throw _.reason}}throw _}function E(_,q,Y,r1,d1){var m1=typeof _;(m1==="undefined"||m1==="boolean")&&(_=null);var M1=!1;if(_===null)M1=!0;else switch(m1){case"bigint":case"string":case"number":M1=!0;break;case"object":switch(_.$$typeof){case t:case c:M1=!0;break;case g:return M1=_._init,E(M1(_._payload),q,Y,r1,d1)}}if(M1)return d1=d1(_),M1=r1===""?"."+l1(_,0):r1,T(d1)?(Y="",M1!=null&&(Y=M1.replace(Q,"$&/")+"/"),E(d1,q,Y,"",function(U1){return U1})):d1!=null&&($(d1)&&(d1=D(d1,Y+(d1.key==null||_&&_.key===d1.key?"":(""+d1.key).replace(Q,"$&/")+"/")+M1)),q.push(d1)),1;M1=0;var O1=r1===""?".":r1+":";if(T(_))for(var k1=0;k1<_.length;k1++)r1=_[k1],m1=O1+l1(r1,k1),M1+=E(r1,q,Y,m1,d1);else if(k1=M(_),typeof k1=="function")for(_=k1.call(_),k1=0;!(r1=_.next()).done;)r1=r1.value,m1=O1+l1(r1,k1++),M1+=E(r1,q,Y,m1,d1);else if(m1==="object"){if(typeof _.then=="function")return E(G(_),q,Y,r1,d1);throw q=String(_),Error("Objects are not valid as a React child (found: "+(q==="[object Object]"?"object with keys {"+Object.keys(_).join(", ")+"}":q)+"). If you meant to render a collection of children, use an array instead.")}return M1}function I(_,q,Y){if(_==null)return _;var r1=[],d1=0;return E(_,r1,"","",function(m1){return q.call(Y,m1,d1++)}),r1}function P(_){if(_._status===-1){var q=_._result;q=q(),q.then(function(Y){(_._status===0||_._status===-1)&&(_._status=1,_._result=Y)},function(Y){(_._status===0||_._status===-1)&&(_._status=2,_._result=Y)}),_._status===-1&&(_._status=0,_._result=q)}if(_._status===1)return _._result.default;throw _._result}var W=typeof reportError=="function"?reportError:function(_){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var q=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof _=="object"&&_!==null&&typeof _.message=="string"?String(_.message):String(_),error:_});if(!window.dispatchEvent(q))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",_);return}console.error(_)},X={map:I,forEach:function(_,q,Y){I(_,function(){q.apply(this,arguments)},Y)},count:function(_){var q=0;return I(_,function(){q++}),q},toArray:function(_){return I(_,function(q){return q})||[]},only:function(_){if(!$(_))throw Error("React.Children.only expected to receive a single React element child.");return _}};return Z1.Activity=b,Z1.Children=X,Z1.Component=V,Z1.Fragment=n,Z1.Profiler=u,Z1.PureComponent=S,Z1.StrictMode=i,Z1.Suspense=p,Z1.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=k,Z1.__COMPILER_RUNTIME={__proto__:null,c:function(_){return k.H.useMemoCache(_)}},Z1.cache=function(_){return function(){return _.apply(null,arguments)}},Z1.cacheSignal=function(){return null},Z1.cloneElement=function(_,q,Y){if(_==null)throw Error("The argument must be a React element, but you passed "+_+".");var r1=A({},_.props),d1=_.key;if(q!=null)for(m1 in q.key!==void 0&&(d1=""+q.key),q)!B.call(q,m1)||m1==="key"||m1==="__self"||m1==="__source"||m1==="ref"&&q.ref===void 0||(r1[m1]=q[m1]);var m1=arguments.length-2;if(m1===1)r1.children=Y;else if(1>>1,X=E[W];if(0>>1;W<_;){var q=2*(W+1)-1,Y=E[q],r1=q+1,d1=E[r1];if(0>u(Y,P))r1u(d1,Y)?(E[W]=d1,E[r1]=P,W=r1):(E[W]=Y,E[q]=P,W=q);else if(r1u(d1,P))E[W]=d1,E[r1]=P,W=r1;else break t}}return I}function u(E,I){var P=E.sortIndex-I.sortIndex;return P!==0?P:E.id-I.id}if(t.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var h=performance;t.unstable_now=function(){return h.now()}}else{var d=Date,m=d.now();t.unstable_now=function(){return d.now()-m}}var p=[],x=[],g=1,b=null,y=3,M=!1,L=!1,A=!1,w=!1,V=typeof setTimeout=="function"?setTimeout:null,H=typeof clearTimeout=="function"?clearTimeout:null,S=typeof setImmediate<"u"?setImmediate:null;function j(E){for(var I=n(x);I!==null;){if(I.callback===null)i(x);else if(I.startTime<=E)i(x),I.sortIndex=I.expirationTime,c(p,I);else break;I=n(x)}}function T(E){if(A=!1,j(E),!L)if(n(p)!==null)L=!0,R||(R=!0,O());else{var I=n(x);I!==null&&G(T,I.startTime-E)}}var R=!1,k=-1,B=5,N=-1;function D(){return w?!0:!(t.unstable_now()-NE&&D());){var W=b.callback;if(typeof W=="function"){b.callback=null,y=b.priorityLevel;var X=W(b.expirationTime<=E);if(E=t.unstable_now(),typeof X=="function"){b.callback=X,j(E),I=!0;break e}b===n(p)&&i(p),j(E)}else i(p);b=n(p)}if(b!==null)I=!0;else{var _=n(x);_!==null&&G(T,_.startTime-E),I=!1}}break t}finally{b=null,y=P,M=!1}I=void 0}}finally{I?O():R=!1}}}var O;if(typeof S=="function")O=function(){S($)};else if(typeof MessageChannel<"u"){var Q=new MessageChannel,l1=Q.port2;Q.port1.onmessage=$,O=function(){l1.postMessage(null)}}else O=function(){V($,0)};function G(E,I){k=V(function(){E(t.unstable_now())},I)}t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(E){E.callback=null},t.unstable_forceFrameRate=function(E){0>E||125W?(E.sortIndex=P,c(x,E),n(p)===null&&E===n(x)&&(A?(H(k),k=-1):A=!0,G(T,P-W))):(E.sortIndex=X,c(p,E),L||M||(L=!0,R||(R=!0,O()))),E},t.unstable_shouldYield=D,t.unstable_wrapCallback=function(E){var I=y;return function(){var P=y;y=I;try{return E.apply(this,arguments)}finally{y=P}}}})(hc)),hc}var Ms;function Ov(){return Ms||(Ms=1,uc.exports=Dv()),uc.exports}var dc={exports:{}},W2={};var ws;function Uv(){if(ws)return W2;ws=1;var t=Ba();function c(p){var x="https://react.dev/errors/"+p;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(c){console.error(c)}}return t(),dc.exports=Uv(),dc.exports}var Ss;function Iv(){if(Ss)return E8;Ss=1;var t=Ov(),c=Ba(),n=Au();function i(e){var a="https://react.dev/errors/"+e;if(1X||(e.current=W[X],W[X]=null,X--)}function Y(e,a){X++,W[X]=e.current,e.current=a}var r1=_(null),d1=_(null),m1=_(null),M1=_(null);function O1(e,a){switch(Y(m1,a),Y(d1,e),Y(r1,null),a.nodeType){case 9:case 11:e=(e=a.documentElement)&&(e=e.namespaceURI)?Di(e):0;break;default:if(e=a.tagName,a=a.namespaceURI)a=Di(a),e=Oi(a,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}q(r1),Y(r1,e)}function k1(){q(r1),q(d1),q(m1)}function U1(e){e.memoizedState!==null&&Y(M1,e);var a=r1.current,l=Oi(a,e.type);a!==l&&(Y(d1,e),Y(r1,l))}function W1(e){d1.current===e&&(q(r1),q(d1)),M1.current===e&&(q(M1),j8._currentValue=P)}var P1,u2;function v1(e){if(P1===void 0)try{throw Error()}catch(l){var a=l.stack.trim().match(/\n( *(at )?)/);P1=a&&a[1]||"",u2=-1)":-1v||U[o]!==t1[v]){var s1=` +`+U[o].replace(" at new "," at ");return e.displayName&&s1.includes("")&&(s1=s1.replace("",e.displayName)),s1}while(1<=o&&0<=v);break}}}finally{y1=!1,Error.prepareStackTrace=l}return(l=e?e.displayName||e.name:"")?v1(l):""}function E1(e,a){switch(e.tag){case 26:case 27:case 5:return v1(e.type);case 16:return v1("Lazy");case 13:return e.child!==a&&a!==null?v1("Suspense Fallback"):v1("Suspense");case 19:return v1("SuspenseList");case 0:case 15:return j1(e.type,!1);case 11:return j1(e.type.render,!1);case 1:return j1(e.type,!0);case 31:return v1("Activity");default:return""}}function g1(e){try{var a="",l=null;do a+=E1(e,l),l=e,e=e.return;while(e);return a}catch(o){return` +Error generating stack: `+o.message+` +`+o.stack}}var S1=Object.prototype.hasOwnProperty,z1=t.unstable_scheduleCallback,A1=t.unstable_cancelCallback,x1=t.unstable_shouldYield,T1=t.unstable_requestPaint,i1=t.unstable_now,C1=t.unstable_getCurrentPriorityLevel,w1=t.unstable_ImmediatePriority,N1=t.unstable_UserBlockingPriority,Q1=t.unstable_NormalPriority,N2=t.unstable_LowPriority,r2=t.unstable_IdlePriority,f1=t.log,R1=t.unstable_setDisableYieldValue,c1=null,b1=null;function V1(e){if(typeof f1=="function"&&R1(e),b1&&typeof b1.setStrictMode=="function")try{b1.setStrictMode(c1,e)}catch{}}var p1=Math.clz32?Math.clz32:X1,q1=Math.log,D1=Math.LN2;function X1(e){return e>>>=0,e===0?32:31-(q1(e)/D1|0)|0}var e2=256,D2=262144,g4=4194304;function P2(e){var a=e&42;if(a!==0)return a;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function f2(e,a,l){var o=e.pendingLanes;if(o===0)return 0;var v=0,z=e.suspendedLanes,C=e.pingedLanes;e=e.warmLanes;var F=o&134217727;return F!==0?(o=F&~z,o!==0?v=P2(o):(C&=F,C!==0?v=P2(C):l||(l=F&~e,l!==0&&(v=P2(l))))):(F=o&~z,F!==0?v=P2(F):C!==0?v=P2(C):l||(l=o&~e,l!==0&&(v=P2(l)))),v===0?0:a!==0&&a!==v&&(a&z)===0&&(z=v&-v,l=a&-a,z>=l||z===32&&(l&4194048)!==0)?a:v}function E4(e,a){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&a)===0}function wf(e,a){switch(e){case 1:case 2:case 4:case 8:case 64:return a+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return a+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function yn(){var e=g4;return g4<<=1,(g4&62914560)===0&&(g4=4194304),e}function Ke(e){for(var a=[],l=0;31>l;l++)a.push(e);return a}function $0(e,a){e.pendingLanes|=a,a!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Cf(e,a,l,o,v,z){var C=e.pendingLanes;e.pendingLanes=l,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=l,e.entangledLanes&=l,e.errorRecoveryDisabledLanes&=l,e.shellSuspendCounter=0;var F=e.entanglements,U=e.expirationTimes,t1=e.hiddenUpdates;for(l=C&~l;0"u")return null;try{return e.activeElement||e.body}catch{return e.body}}var Bf=/[\n"\\]/g;function z4(e){return e.replace(Bf,function(a){return"\\"+a.charCodeAt(0).toString(16)+" "})}function e7(e,a,l,o,v,z,C,F){e.name="",C!=null&&typeof C!="function"&&typeof C!="symbol"&&typeof C!="boolean"?e.type=C:e.removeAttribute("type"),a!=null?C==="number"?(a===0&&e.value===""||e.value!=a)&&(e.value=""+x4(a)):e.value!==""+x4(a)&&(e.value=""+x4(a)):C!=="submit"&&C!=="reset"||e.removeAttribute("value"),a!=null?c7(e,C,x4(a)):l!=null?c7(e,C,x4(l)):o!=null&&e.removeAttribute("value"),v==null&&z!=null&&(e.defaultChecked=!!z),v!=null&&(e.checked=v&&typeof v!="function"&&typeof v!="symbol"),F!=null&&typeof F!="function"&&typeof F!="symbol"&&typeof F!="boolean"?e.name=""+x4(F):e.removeAttribute("name")}function Fn(e,a,l,o,v,z,C,F){if(z!=null&&typeof z!="function"&&typeof z!="symbol"&&typeof z!="boolean"&&(e.type=z),a!=null||l!=null){if(!(z!=="submit"&&z!=="reset"||a!=null)){t7(e);return}l=l!=null?""+x4(l):"",a=a!=null?""+x4(a):l,F||a===e.value||(e.value=a),e.defaultValue=a}o=o??v,o=typeof o!="function"&&typeof o!="symbol"&&!!o,e.checked=F?e.checked:!!o,e.defaultChecked=!!o,C!=null&&typeof C!="function"&&typeof C!="symbol"&&typeof C!="boolean"&&(e.name=C),t7(e)}function c7(e,a,l){a==="number"&&C5(e.ownerDocument)===e||e.defaultValue===""+l||(e.defaultValue=""+l)}function q6(e,a,l,o){if(e=e.options,a){a={};for(var v=0;v"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),i7=!1;if(J4)try{var K0={};Object.defineProperty(K0,"passive",{get:function(){i7=!0}}),window.addEventListener("test",K0,K0),window.removeEventListener("test",K0,K0)}catch{i7=!1}var w3=null,s7=null,H5=null;function On(){if(H5)return H5;var e,a=s7,l=a.length,o,v="value"in w3?w3.value:w3.textContent,z=v.length;for(e=0;e=W0),Gn=" ",Yn=!1;function Kn(e,a){switch(e){case"keyup":return nm.indexOf(a.keyCode)!==-1;case"keydown":return a.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Qn(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var I6=!1;function lm(e,a){switch(e){case"compositionend":return Qn(a);case"keypress":return a.which!==32?null:(Yn=!0,Gn);case"textInput":return e=a.data,e===Gn&&Yn?null:e;default:return null}}function im(e,a){if(I6)return e==="compositionend"||!f7&&Kn(e,a)?(e=On(),H5=s7=w3=null,I6=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(a.ctrlKey||a.altKey||a.metaKey)||a.ctrlKey&&a.altKey){if(a.char&&1=a)return{node:l,offset:a-e};e=o}t:{for(;l;){if(l.nextSibling){l=l.nextSibling;break t}l=l.parentNode}l=void 0}l=nr(l)}}function lr(e,a){return e&&a?e===a?!0:e&&e.nodeType===3?!1:a&&a.nodeType===3?lr(e,a.parentNode):"contains"in e?e.contains(a):e.compareDocumentPosition?!!(e.compareDocumentPosition(a)&16):!1:!1}function ir(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var a=C5(e.document);a instanceof e.HTMLIFrameElement;){try{var l=typeof a.contentWindow.location.href=="string"}catch{l=!1}if(l)e=a.contentWindow;else break;a=C5(e.document)}return a}function p7(e){var a=e&&e.nodeName&&e.nodeName.toLowerCase();return a&&(a==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||a==="textarea"||e.contentEditable==="true")}var vm=J4&&"documentMode"in document&&11>=document.documentMode,P6=null,g7=null,c8=null,x7=!1;function sr(e,a,l){var o=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;x7||P6==null||P6!==C5(o)||(o=P6,"selectionStart"in o&&p7(o)?o={start:o.selectionStart,end:o.selectionEnd}:(o=(o.ownerDocument&&o.ownerDocument.defaultView||window).getSelection(),o={anchorNode:o.anchorNode,anchorOffset:o.anchorOffset,focusNode:o.focusNode,focusOffset:o.focusOffset}),c8&&e8(c8,o)||(c8=o,o=zt(g7,"onSelect"),0>=C,v-=C,P4=1<<32-p1(a)+v|l<Y1?(a2=B1,B1=null):a2=B1.sibling;var i2=e1(K,B1,J[Y1],o1);if(i2===null){B1===null&&(B1=a2);break}e&&B1&&i2.alternate===null&&a(K,B1),Z=z(i2,Z,Y1),l2===null?F1=i2:l2.sibling=i2,l2=i2,B1=a2}if(Y1===J.length)return l(K,B1),n2&&e3(K,Y1),F1;if(B1===null){for(;Y1Y1?(a2=B1,B1=null):a2=B1.sibling;var $3=e1(K,B1,i2.value,o1);if($3===null){B1===null&&(B1=a2);break}e&&B1&&$3.alternate===null&&a(K,B1),Z=z($3,Z,Y1),l2===null?F1=$3:l2.sibling=$3,l2=$3,B1=a2}if(i2.done)return l(K,B1),n2&&e3(K,Y1),F1;if(B1===null){for(;!i2.done;Y1++,i2=J.next())i2=u1(K,i2.value,o1),i2!==null&&(Z=z(i2,Z,Y1),l2===null?F1=i2:l2.sibling=i2,l2=i2);return n2&&e3(K,Y1),F1}for(B1=o(B1);!i2.done;Y1++,i2=J.next())i2=n1(B1,K,Y1,i2.value,o1),i2!==null&&(e&&i2.alternate!==null&&B1.delete(i2.key===null?Y1:i2.key),Z=z(i2,Z,Y1),l2===null?F1=i2:l2.sibling=i2,l2=i2);return e&&B1.forEach(function(Rv){return a(K,Rv)}),n2&&e3(K,Y1),F1}function p2(K,Z,J,o1){if(typeof J=="object"&&J!==null&&J.type===A&&J.key===null&&(J=J.props.children),typeof J=="object"&&J!==null){switch(J.$$typeof){case M:t:{for(var F1=J.key;Z!==null;){if(Z.key===F1){if(F1=J.type,F1===A){if(Z.tag===7){l(K,Z.sibling),o1=v(Z,J.props.children),o1.return=K,K=o1;break t}}else if(Z.elementType===F1||typeof F1=="object"&&F1!==null&&F1.$$typeof===B&&h6(F1)===Z.type){l(K,Z.sibling),o1=v(Z,J.props),s8(o1,J),o1.return=K,K=o1;break t}l(K,Z);break}else a(K,Z);Z=Z.sibling}J.type===A?(o1=l6(J.props.children,K.mode,o1,J.key),o1.return=K,K=o1):(o1=E5(J.type,J.key,J.props,null,K.mode,o1),s8(o1,J),o1.return=K,K=o1)}return C(K);case L:t:{for(F1=J.key;Z!==null;){if(Z.key===F1)if(Z.tag===4&&Z.stateNode.containerInfo===J.containerInfo&&Z.stateNode.implementation===J.implementation){l(K,Z.sibling),o1=v(Z,J.children||[]),o1.return=K,K=o1;break t}else{l(K,Z);break}else a(K,Z);Z=Z.sibling}o1=S7(J,K.mode,o1),o1.return=K,K=o1}return C(K);case B:return J=h6(J),p2(K,Z,J,o1)}if(G(J))return L1(K,Z,J,o1);if(O(J)){if(F1=O(J),typeof F1!="function")throw Error(i(150));return J=F1.call(J),_1(K,Z,J,o1)}if(typeof J.then=="function")return p2(K,Z,I5(J),o1);if(J.$$typeof===S)return p2(K,Z,q5(K,J),o1);P5(K,J)}return typeof J=="string"&&J!==""||typeof J=="number"||typeof J=="bigint"?(J=""+J,Z!==null&&Z.tag===6?(l(K,Z.sibling),o1=v(Z,J),o1.return=K,K=o1):(l(K,Z),o1=C7(J,K.mode,o1),o1.return=K,K=o1),C(K)):l(K,Z)}return function(K,Z,J,o1){try{i8=0;var F1=p2(K,Z,J,o1);return e0=null,F1}catch(B1){if(B1===t0||B1===O5)throw B1;var l2=h4(29,B1,null,K.mode);return l2.lanes=o1,l2.return=K,l2}finally{}}}var f6=jr(!0),Nr=jr(!1),L3=!1;function T7(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function _7(e,a){e=e.updateQueue,a.updateQueue===e&&(a.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function V3(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function B3(e,a,l){var o=e.updateQueue;if(o===null)return null;if(o=o.shared,(o2&2)!==0){var v=o.pending;return v===null?a.next=a:(a.next=v.next,v.next=a),o.pending=a,a=R5(e),vr(e,null,l),a}return F5(e,o,a,l),R5(e)}function o8(e,a,l){if(a=a.updateQueue,a!==null&&(a=a.shared,(l&4194048)!==0)){var o=a.lanes;o&=e.pendingLanes,l|=o,a.lanes=l,wn(e,l)}}function q7(e,a){var l=e.updateQueue,o=e.alternate;if(o!==null&&(o=o.updateQueue,l===o)){var v=null,z=null;if(l=l.firstBaseUpdate,l!==null){do{var C={lane:l.lane,tag:l.tag,payload:l.payload,callback:null,next:null};z===null?v=z=C:z=z.next=C,l=l.next}while(l!==null);z===null?v=z=a:z=z.next=a}else v=z=a;l={baseState:o.baseState,firstBaseUpdate:v,lastBaseUpdate:z,shared:o.shared,callbacks:o.callbacks},e.updateQueue=l;return}e=l.lastBaseUpdate,e===null?l.firstBaseUpdate=a:e.next=a,l.lastBaseUpdate=a}var D7=!1;function u8(){if(D7){var e=J6;if(e!==null)throw e}}function h8(e,a,l,o){D7=!1;var v=e.updateQueue;L3=!1;var z=v.firstBaseUpdate,C=v.lastBaseUpdate,F=v.shared.pending;if(F!==null){v.shared.pending=null;var U=F,t1=U.next;U.next=null,C===null?z=t1:C.next=t1,C=U;var s1=e.alternate;s1!==null&&(s1=s1.updateQueue,F=s1.lastBaseUpdate,F!==C&&(F===null?s1.firstBaseUpdate=t1:F.next=t1,s1.lastBaseUpdate=U))}if(z!==null){var u1=v.baseState;C=0,s1=t1=U=null,F=z;do{var e1=F.lane&-536870913,n1=e1!==F.lane;if(n1?(c2&e1)===e1:(o&e1)===e1){e1!==0&&e1===W6&&(D7=!0),s1!==null&&(s1=s1.next={lane:0,tag:F.tag,payload:F.payload,callback:null,next:null});t:{var L1=e,_1=F;e1=a;var p2=l;switch(_1.tag){case 1:if(L1=_1.payload,typeof L1=="function"){u1=L1.call(p2,u1,e1);break t}u1=L1;break t;case 3:L1.flags=L1.flags&-65537|128;case 0:if(L1=_1.payload,e1=typeof L1=="function"?L1.call(p2,u1,e1):L1,e1==null)break t;u1=b({},u1,e1);break t;case 2:L3=!0}}e1=F.callback,e1!==null&&(e.flags|=64,n1&&(e.flags|=8192),n1=v.callbacks,n1===null?v.callbacks=[e1]:n1.push(e1))}else n1={lane:e1,tag:F.tag,payload:F.payload,callback:F.callback,next:null},s1===null?(t1=s1=n1,U=u1):s1=s1.next=n1,C|=e1;if(F=F.next,F===null){if(F=v.shared.pending,F===null)break;n1=F,F=n1.next,n1.next=null,v.lastBaseUpdate=n1,v.shared.pending=null}}while(!0);s1===null&&(U=u1),v.baseState=U,v.firstBaseUpdate=t1,v.lastBaseUpdate=s1,z===null&&(v.shared.lanes=0),R3|=C,e.lanes=C,e.memoizedState=u1}}function kr(e,a){if(typeof e!="function")throw Error(i(191,e));e.call(a)}function Fr(e,a){var l=e.callbacks;if(l!==null)for(e.callbacks=null,e=0;ez?z:8;var C=E.T,F={};E.T=F,r9(e,!1,a,l);try{var U=v(),t1=E.S;if(t1!==null&&t1(F,U),U!==null&&typeof U=="object"&&typeof U.then=="function"){var s1=Cm(U,o);m8(e,a,s1,p4(e))}else m8(e,a,o,p4(e))}catch(u1){m8(e,a,{then:function(){},status:"rejected",reason:u1},p4())}finally{I.p=z,C!==null&&F.types!==null&&(C.types=F.types),E.T=C}}function Bm(){}function a9(e,a,l,o){if(e.tag!==5)throw Error(i(476));var v=dl(e).queue;hl(e,v,a,P,l===null?Bm:function(){return fl(e),l(o)})}function dl(e){var a=e.memoizedState;if(a!==null)return a;a={memoizedState:P,baseState:P,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:r3,lastRenderedState:P},next:null};var l={};return a.next={memoizedState:l,baseState:l,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:r3,lastRenderedState:l},next:null},e.memoizedState=a,e=e.alternate,e!==null&&(e.memoizedState=a),a}function fl(e){var a=dl(e);a.next===null&&(a=e.alternate.memoizedState),m8(e,a.next.queue,{},p4())}function n9(){return K2(j8)}function ml(){return B2().memoizedState}function vl(){return B2().memoizedState}function jm(e){for(var a=e.return;a!==null;){switch(a.tag){case 24:case 3:var l=p4();e=V3(l);var o=B3(a,e,l);o!==null&&(s4(o,a,l),o8(o,a,l)),a={cache:k7()},e.payload=a;return}a=a.return}}function Nm(e,a,l){var o=p4();l={lane:o,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null},tt(e)?gl(a,l):(l=M7(e,a,l,o),l!==null&&(s4(l,e,o),xl(l,a,o)))}function pl(e,a,l){var o=p4();m8(e,a,l,o)}function m8(e,a,l,o){var v={lane:o,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null};if(tt(e))gl(a,v);else{var z=e.alternate;if(e.lanes===0&&(z===null||z.lanes===0)&&(z=a.lastRenderedReducer,z!==null))try{var C=a.lastRenderedState,F=z(C,l);if(v.hasEagerState=!0,v.eagerState=F,u4(F,C))return F5(e,a,v,0),g2===null&&k5(),!1}catch{}finally{}if(l=M7(e,a,v,o),l!==null)return s4(l,e,o),xl(l,a,o),!0}return!1}function r9(e,a,l,o){if(o={lane:2,revertLane:T9(),gesture:null,action:o,hasEagerState:!1,eagerState:null,next:null},tt(e)){if(a)throw Error(i(479))}else a=M7(e,l,o,2),a!==null&&s4(a,e,2)}function tt(e){var a=e.alternate;return e===G1||a!==null&&a===G1}function gl(e,a){a0=G5=!0;var l=e.pending;l===null?a.next=a:(a.next=l.next,l.next=a),e.pending=a}function xl(e,a,l){if((l&4194048)!==0){var o=a.lanes;o&=e.pendingLanes,l|=o,a.lanes=l,wn(e,l)}}var v8={readContext:K2,use:Q5,useCallback:H2,useContext:H2,useEffect:H2,useImperativeHandle:H2,useLayoutEffect:H2,useInsertionEffect:H2,useMemo:H2,useReducer:H2,useRef:H2,useState:H2,useDebugValue:H2,useDeferredValue:H2,useTransition:H2,useSyncExternalStore:H2,useId:H2,useHostTransitionStatus:H2,useFormState:H2,useActionState:H2,useOptimistic:H2,useMemoCache:H2,useCacheRefresh:H2};v8.useEffectEvent=H2;var zl={readContext:K2,use:Q5,useCallback:function(e,a){return e4().memoizedState=[e,a===void 0?null:a],e},useContext:K2,useEffect:cl,useImperativeHandle:function(e,a,l){l=l!=null?l.concat([e]):null,W5(4194308,4,ll.bind(null,a,e),l)},useLayoutEffect:function(e,a){return W5(4194308,4,e,a)},useInsertionEffect:function(e,a){W5(4,2,e,a)},useMemo:function(e,a){var l=e4();a=a===void 0?null:a;var o=e();if(m6){V1(!0);try{e()}finally{V1(!1)}}return l.memoizedState=[o,a],o},useReducer:function(e,a,l){var o=e4();if(l!==void 0){var v=l(a);if(m6){V1(!0);try{l(a)}finally{V1(!1)}}}else v=a;return o.memoizedState=o.baseState=v,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:v},o.queue=e,e=e.dispatch=Nm.bind(null,G1,e),[o.memoizedState,e]},useRef:function(e){var a=e4();return e={current:e},a.memoizedState=e},useState:function(e){e=W7(e);var a=e.queue,l=pl.bind(null,G1,a);return a.dispatch=l,[e.memoizedState,l]},useDebugValue:e9,useDeferredValue:function(e,a){var l=e4();return c9(l,e,a)},useTransition:function(){var e=W7(!1);return e=hl.bind(null,G1,e.queue,!0,!1),e4().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,a,l){var o=G1,v=e4();if(n2){if(l===void 0)throw Error(i(407));l=l()}else{if(l=a(),g2===null)throw Error(i(349));(c2&127)!==0||Dr(o,a,l)}v.memoizedState=l;var z={value:l,getSnapshot:a};return v.queue=z,cl(Ur.bind(null,o,z,e),[e]),o.flags|=2048,r0(9,{destroy:void 0},Or.bind(null,o,z,l,a),null),l},useId:function(){var e=e4(),a=g2.identifierPrefix;if(n2){var l=$4,o=P4;l=(o&~(1<<32-p1(o)-1)).toString(32)+l,a="_"+a+"R_"+l,l=Y5++,0<\/script>",z=z.removeChild(z.firstChild);break;case"select":z=typeof o.is=="string"?C.createElement("select",{is:o.is}):C.createElement("select"),o.multiple?z.multiple=!0:o.size&&(z.size=o.size);break;default:z=typeof o.is=="string"?C.createElement(v,{is:o.is}):C.createElement(v)}}z[G2]=a,z[c4]=o;t:for(C=a.child;C!==null;){if(C.tag===5||C.tag===6)z.appendChild(C.stateNode);else if(C.tag!==4&&C.tag!==27&&C.child!==null){C.child.return=C,C=C.child;continue}if(C===a)break t;for(;C.sibling===null;){if(C.return===null||C.return===a)break t;C=C.return}C.sibling.return=C.return,C=C.sibling}a.stateNode=z;t:switch(X2(z,v,o),v){case"button":case"input":case"select":case"textarea":o=!!o.autoFocus;break t;case"img":o=!0;break t;default:o=!1}o&&i3(a)}}return z2(a),z9(a,a.type,e===null?null:e.memoizedProps,a.pendingProps,l),null;case 6:if(e&&a.stateNode!=null)e.memoizedProps!==o&&i3(a);else{if(typeof o!="string"&&a.stateNode===null)throw Error(i(166));if(e=m1.current,Q6(a)){if(e=a.stateNode,l=a.memoizedProps,o=null,v=Y2,v!==null)switch(v.tag){case 27:case 5:o=v.memoizedProps}e[G2]=a,e=!!(e.nodeValue===l||o!==null&&o.suppressHydrationWarning===!0||_i(e.nodeValue,l)),e||H3(a,!0)}else e=bt(e).createTextNode(o),e[G2]=a,a.stateNode=e}return z2(a),null;case 31:if(l=a.memoizedState,e===null||e.memoizedState!==null){if(o=Q6(a),l!==null){if(e===null){if(!o)throw Error(i(318));if(e=a.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(i(557));e[G2]=a}else i6(),(a.flags&128)===0&&(a.memoizedState=null),a.flags|=4;z2(a),e=!1}else l=V7(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=l),e=!0;if(!e)return a.flags&256?(f4(a),a):(f4(a),null);if((a.flags&128)!==0)throw Error(i(558))}return z2(a),null;case 13:if(o=a.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(v=Q6(a),o!==null&&o.dehydrated!==null){if(e===null){if(!v)throw Error(i(318));if(v=a.memoizedState,v=v!==null?v.dehydrated:null,!v)throw Error(i(317));v[G2]=a}else i6(),(a.flags&128)===0&&(a.memoizedState=null),a.flags|=4;z2(a),v=!1}else v=V7(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=v),v=!0;if(!v)return a.flags&256?(f4(a),a):(f4(a),null)}return f4(a),(a.flags&128)!==0?(a.lanes=l,a):(l=o!==null,e=e!==null&&e.memoizedState!==null,l&&(o=a.child,v=null,o.alternate!==null&&o.alternate.memoizedState!==null&&o.alternate.memoizedState.cachePool!==null&&(v=o.alternate.memoizedState.cachePool.pool),z=null,o.memoizedState!==null&&o.memoizedState.cachePool!==null&&(z=o.memoizedState.cachePool.pool),z!==v&&(o.flags|=2048)),l!==e&&l&&(a.child.flags|=8192),rt(a,a.updateQueue),z2(a),null);case 4:return k1(),e===null&&O9(a.stateNode.containerInfo),z2(a),null;case 10:return a3(a.type),z2(a),null;case 19:if(q(V2),o=a.memoizedState,o===null)return z2(a),null;if(v=(a.flags&128)!==0,z=o.rendering,z===null)if(v)g8(o,!1);else{if(A2!==0||e!==null&&(e.flags&128)!==0)for(e=a.child;e!==null;){if(z=Z5(e),z!==null){for(a.flags|=128,g8(o,!1),e=z.updateQueue,a.updateQueue=e,rt(a,e),a.subtreeFlags=0,e=l,l=a.child;l!==null;)pr(l,e),l=l.sibling;return Y(V2,V2.current&1|2),n2&&e3(a,o.treeForkCount),a.child}e=e.sibling}o.tail!==null&&i1()>ut&&(a.flags|=128,v=!0,g8(o,!1),a.lanes=4194304)}else{if(!v)if(e=Z5(z),e!==null){if(a.flags|=128,v=!0,e=e.updateQueue,a.updateQueue=e,rt(a,e),g8(o,!0),o.tail===null&&o.tailMode==="hidden"&&!z.alternate&&!n2)return z2(a),null}else 2*i1()-o.renderingStartTime>ut&&l!==536870912&&(a.flags|=128,v=!0,g8(o,!1),a.lanes=4194304);o.isBackwards?(z.sibling=a.child,a.child=z):(e=o.last,e!==null?e.sibling=z:a.child=z,o.last=z)}return o.tail!==null?(e=o.tail,o.rendering=e,o.tail=e.sibling,o.renderingStartTime=i1(),e.sibling=null,l=V2.current,Y(V2,v?l&1|2:l&1),n2&&e3(a,o.treeForkCount),e):(z2(a),null);case 22:case 23:return f4(a),U7(),o=a.memoizedState!==null,e!==null?e.memoizedState!==null!==o&&(a.flags|=8192):o&&(a.flags|=8192),o?(l&536870912)!==0&&(a.flags&128)===0&&(z2(a),a.subtreeFlags&6&&(a.flags|=8192)):z2(a),l=a.updateQueue,l!==null&&rt(a,l.retryQueue),l=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(l=e.memoizedState.cachePool.pool),o=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(o=a.memoizedState.cachePool.pool),o!==l&&(a.flags|=2048),e!==null&&q(u6),null;case 24:return l=null,e!==null&&(l=e.memoizedState.cache),a.memoizedState.cache!==l&&(a.flags|=2048),a3(k2),z2(a),null;case 25:return null;case 30:return null}throw Error(i(156,a.tag))}function Tm(e,a){switch(A7(a),a.tag){case 1:return e=a.flags,e&65536?(a.flags=e&-65537|128,a):null;case 3:return a3(k2),k1(),e=a.flags,(e&65536)!==0&&(e&128)===0?(a.flags=e&-65537|128,a):null;case 26:case 27:case 5:return W1(a),null;case 31:if(a.memoizedState!==null){if(f4(a),a.alternate===null)throw Error(i(340));i6()}return e=a.flags,e&65536?(a.flags=e&-65537|128,a):null;case 13:if(f4(a),e=a.memoizedState,e!==null&&e.dehydrated!==null){if(a.alternate===null)throw Error(i(340));i6()}return e=a.flags,e&65536?(a.flags=e&-65537|128,a):null;case 19:return q(V2),null;case 4:return k1(),null;case 10:return a3(a.type),null;case 22:case 23:return f4(a),U7(),e!==null&&q(u6),e=a.flags,e&65536?(a.flags=e&-65537|128,a):null;case 24:return a3(k2),null;case 25:return null;default:return null}}function Il(e,a){switch(A7(a),a.tag){case 3:a3(k2),k1();break;case 26:case 27:case 5:W1(a);break;case 4:k1();break;case 31:a.memoizedState!==null&&f4(a);break;case 13:f4(a);break;case 19:q(V2);break;case 10:a3(a.type);break;case 22:case 23:f4(a),U7(),e!==null&&q(u6);break;case 24:a3(k2)}}function x8(e,a){try{var l=a.updateQueue,o=l!==null?l.lastEffect:null;if(o!==null){var v=o.next;l=v;do{if((l.tag&e)===e){o=void 0;var z=l.create,C=l.inst;o=z(),C.destroy=o}l=l.next}while(l!==v)}}catch(F){d2(a,a.return,F)}}function k3(e,a,l){try{var o=a.updateQueue,v=o!==null?o.lastEffect:null;if(v!==null){var z=v.next;o=z;do{if((o.tag&e)===e){var C=o.inst,F=C.destroy;if(F!==void 0){C.destroy=void 0,v=a;var U=l,t1=F;try{t1()}catch(s1){d2(v,U,s1)}}}o=o.next}while(o!==z)}}catch(s1){d2(a,a.return,s1)}}function Pl(e){var a=e.updateQueue;if(a!==null){var l=e.stateNode;try{Fr(a,l)}catch(o){d2(e,e.return,o)}}}function $l(e,a,l){l.props=v6(e.type,e.memoizedProps),l.state=e.memoizedState;try{l.componentWillUnmount()}catch(o){d2(e,a,o)}}function z8(e,a){try{var l=e.ref;if(l!==null){switch(e.tag){case 26:case 27:case 5:var o=e.stateNode;break;case 30:o=e.stateNode;break;default:o=e.stateNode}typeof l=="function"?e.refCleanup=l(o):l.current=o}}catch(v){d2(e,a,v)}}function Z4(e,a){var l=e.ref,o=e.refCleanup;if(l!==null)if(typeof o=="function")try{o()}catch(v){d2(e,a,v)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof l=="function")try{l(null)}catch(v){d2(e,a,v)}else l.current=null}function Zl(e){var a=e.type,l=e.memoizedProps,o=e.stateNode;try{t:switch(a){case"button":case"input":case"select":case"textarea":l.autoFocus&&o.focus();break t;case"img":l.src?o.src=l.src:l.srcSet&&(o.srcset=l.srcSet)}}catch(v){d2(e,e.return,v)}}function b9(e,a,l){try{var o=e.stateNode;rv(o,e.type,l,a),o[c4]=a}catch(v){d2(e,e.return,v)}}function Gl(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&D3(e.type)||e.tag===4}function y9(e){t:for(;;){for(;e.sibling===null;){if(e.return===null||Gl(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&D3(e.type)||e.flags&2||e.child===null||e.tag===4)continue t;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function M9(e,a,l){var o=e.tag;if(o===5||o===6)e=e.stateNode,a?(l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l).insertBefore(e,a):(a=l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l,a.appendChild(e),l=l._reactRootContainer,l!=null||a.onclick!==null||(a.onclick=W4));else if(o!==4&&(o===27&&D3(e.type)&&(l=e.stateNode,a=null),e=e.child,e!==null))for(M9(e,a,l),e=e.sibling;e!==null;)M9(e,a,l),e=e.sibling}function lt(e,a,l){var o=e.tag;if(o===5||o===6)e=e.stateNode,a?l.insertBefore(e,a):l.appendChild(e);else if(o!==4&&(o===27&&D3(e.type)&&(l=e.stateNode),e=e.child,e!==null))for(lt(e,a,l),e=e.sibling;e!==null;)lt(e,a,l),e=e.sibling}function Yl(e){var a=e.stateNode,l=e.memoizedProps;try{for(var o=e.type,v=a.attributes;v.length;)a.removeAttributeNode(v[0]);X2(a,o,l),a[G2]=e,a[c4]=l}catch(z){d2(e,e.return,z)}}var s3=!1,E2=!1,w9=!1,Kl=typeof WeakSet=="function"?WeakSet:Set,Z2=null;function _m(e,a){if(e=e.containerInfo,P9=At,e=ir(e),p7(e)){if("selectionStart"in e)var l={start:e.selectionStart,end:e.selectionEnd};else t:{l=(l=e.ownerDocument)&&l.defaultView||window;var o=l.getSelection&&l.getSelection();if(o&&o.rangeCount!==0){l=o.anchorNode;var v=o.anchorOffset,z=o.focusNode;o=o.focusOffset;try{l.nodeType,z.nodeType}catch{l=null;break t}var C=0,F=-1,U=-1,t1=0,s1=0,u1=e,e1=null;e:for(;;){for(var n1;u1!==l||v!==0&&u1.nodeType!==3||(F=C+v),u1!==z||o!==0&&u1.nodeType!==3||(U=C+o),u1.nodeType===3&&(C+=u1.nodeValue.length),(n1=u1.firstChild)!==null;)e1=u1,u1=n1;for(;;){if(u1===e)break e;if(e1===l&&++t1===v&&(F=C),e1===z&&++s1===o&&(U=C),(n1=u1.nextSibling)!==null)break;u1=e1,e1=u1.parentNode}u1=n1}l=F===-1||U===-1?null:{start:F,end:U}}else l=null}l=l||{start:0,end:0}}else l=null;for($9={focusedElem:e,selectionRange:l},At=!1,Z2=a;Z2!==null;)if(a=Z2,e=a.child,(a.subtreeFlags&1028)!==0&&e!==null)e.return=a,Z2=e;else for(;Z2!==null;){switch(a=Z2,z=a.alternate,e=a.flags,a.tag){case 0:if((e&4)!==0&&(e=a.updateQueue,e=e!==null?e.events:null,e!==null))for(l=0;l title"))),X2(z,o,l),z[G2]=e,$2(z),o=z;break t;case"link":var C=es("link","href",v).get(o+(l.href||""));if(C){for(var F=0;Fp2&&(C=p2,p2=_1,_1=C);var K=rr(F,_1),Z=rr(F,p2);if(K&&Z&&(n1.rangeCount!==1||n1.anchorNode!==K.node||n1.anchorOffset!==K.offset||n1.focusNode!==Z.node||n1.focusOffset!==Z.offset)){var J=u1.createRange();J.setStart(K.node,K.offset),n1.removeAllRanges(),_1>p2?(n1.addRange(J),n1.extend(Z.node,Z.offset)):(J.setEnd(Z.node,Z.offset),n1.addRange(J))}}}}for(u1=[],n1=F;n1=n1.parentNode;)n1.nodeType===1&&u1.push({element:n1,left:n1.scrollLeft,top:n1.scrollTop});for(typeof F.focus=="function"&&F.focus(),F=0;Fl?32:l,E.T=null,l=B9,B9=null;var z=T3,C=f3;if(O2=0,u0=T3=null,f3=0,(o2&6)!==0)throw Error(i(331));var F=o2;if(o2|=4,li(z.current),ai(z,z.current,C,l),o2=F,S8(0,!1),b1&&typeof b1.onPostCommitFiberRoot=="function")try{b1.onPostCommitFiberRoot(c1,z)}catch{}return!0}finally{I.p=v,E.T=o,Ci(e,a)}}function Hi(e,a,l){a=y4(l,a),a=o9(e.stateNode,a,2),e=B3(e,a,2),e!==null&&($0(e,2),G4(e))}function d2(e,a,l){if(e.tag===3)Hi(e,e,l);else for(;a!==null;){if(a.tag===3){Hi(a,e,l);break}else if(a.tag===1){var o=a.stateNode;if(typeof a.type.getDerivedStateFromError=="function"||typeof o.componentDidCatch=="function"&&(E3===null||!E3.has(o))){e=y4(l,e),l=Al(2),o=B3(a,l,2),o!==null&&(Ll(l,o,a,e),$0(o,2),G4(o));break}}a=a.return}}function F9(e,a,l){var o=e.pingCache;if(o===null){o=e.pingCache=new Om;var v=new Set;o.set(a,v)}else v=o.get(a),v===void 0&&(v=new Set,o.set(a,v));v.has(l)||(H9=!0,v.add(l),e=Zm.bind(null,e,a,l),a.then(e,e))}function Zm(e,a,l){var o=e.pingCache;o!==null&&o.delete(a),e.pingedLanes|=e.suspendedLanes&l,e.warmLanes&=~l,g2===e&&(c2&l)===l&&(A2===4||A2===3&&(c2&62914560)===c2&&300>i1()-ot?(o2&2)===0&&h0(e,0):A9|=l,o0===c2&&(o0=0)),G4(e)}function Ai(e,a){a===0&&(a=yn()),e=r6(e,a),e!==null&&($0(e,a),G4(e))}function Gm(e){var a=e.memoizedState,l=0;a!==null&&(l=a.retryLane),Ai(e,l)}function Ym(e,a){var l=0;switch(e.tag){case 31:case 13:var o=e.stateNode,v=e.memoizedState;v!==null&&(l=v.retryLane);break;case 19:o=e.stateNode;break;case 22:o=e.stateNode._retryCache;break;default:throw Error(i(314))}o!==null&&o.delete(a),Ai(e,l)}function Km(e,a){return z1(e,a)}var pt=null,f0=null,R9=!1,gt=!1,E9=!1,q3=0;function G4(e){e!==f0&&e.next===null&&(f0===null?pt=f0=e:f0=f0.next=e),gt=!0,R9||(R9=!0,Xm())}function S8(e,a){if(!E9&>){E9=!0;do for(var l=!1,o=pt;o!==null;){if(e!==0){var v=o.pendingLanes;if(v===0)var z=0;else{var C=o.suspendedLanes,F=o.pingedLanes;z=(1<<31-p1(42|e)+1)-1,z&=v&~(C&~F),z=z&201326741?z&201326741|1:z?z|2:0}z!==0&&(l=!0,ji(o,z))}else z=c2,z=f2(o,o===g2?z:0,o.cancelPendingCommit!==null||o.timeoutHandle!==-1),(z&3)===0||E4(o,z)||(l=!0,ji(o,z));o=o.next}while(l);E9=!1}}function Qm(){Li()}function Li(){gt=R9=!1;var e=0;q3!==0&&iv()&&(e=q3);for(var a=i1(),l=null,o=pt;o!==null;){var v=o.next,z=Vi(o,a);z===0?(o.next=null,l===null?pt=v:l.next=v,v===null&&(f0=l)):(l=o,(e!==0||(z&3)!==0)&&(gt=!0)),o=v}O2!==0&&O2!==5||S8(e),q3!==0&&(q3=0)}function Vi(e,a){for(var l=e.suspendedLanes,o=e.pingedLanes,v=e.expirationTimes,z=e.pendingLanes&-62914561;0F)break;var s1=U.transferSize,u1=U.initiatorType;s1&&qi(u1)&&(U=U.responseEnd,C+=s1*(U"u"?null:document;function Xi(e,a,l){var o=m0;if(o&&typeof a=="string"&&a){var v=z4(a);v='link[rel="'+e+'"][href="'+v+'"]',typeof l=="string"&&(v+='[crossorigin="'+l+'"]'),Qi.has(v)||(Qi.add(v),e={rel:e,crossOrigin:l,href:a},o.querySelector(v)===null&&(a=o.createElement("link"),X2(a,"link",e),$2(a),o.head.appendChild(a)))}}function pv(e){m3.D(e),Xi("dns-prefetch",e,null)}function gv(e,a){m3.C(e,a),Xi("preconnect",e,a)}function xv(e,a,l){m3.L(e,a,l);var o=m0;if(o&&e&&a){var v='link[rel="preload"][as="'+z4(a)+'"]';a==="image"&&l&&l.imageSrcSet?(v+='[imagesrcset="'+z4(l.imageSrcSet)+'"]',typeof l.imageSizes=="string"&&(v+='[imagesizes="'+z4(l.imageSizes)+'"]')):v+='[href="'+z4(e)+'"]';var z=v;switch(a){case"style":z=v0(e);break;case"script":z=p0(e)}A4.has(z)||(e=b({rel:"preload",href:a==="image"&&l&&l.imageSrcSet?void 0:e,as:a},l),A4.set(z,e),o.querySelector(v)!==null||a==="style"&&o.querySelector(V8(z))||a==="script"&&o.querySelector(B8(z))||(a=o.createElement("link"),X2(a,"link",e),$2(a),o.head.appendChild(a)))}}function zv(e,a){m3.m(e,a);var l=m0;if(l&&e){var o=a&&typeof a.as=="string"?a.as:"script",v='link[rel="modulepreload"][as="'+z4(o)+'"][href="'+z4(e)+'"]',z=v;switch(o){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":z=p0(e)}if(!A4.has(z)&&(e=b({rel:"modulepreload",href:e},a),A4.set(z,e),l.querySelector(v)===null)){switch(o){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(l.querySelector(B8(z)))return}o=l.createElement("link"),X2(o,"link",e),$2(o),l.head.appendChild(o)}}}function bv(e,a,l){m3.S(e,a,l);var o=m0;if(o&&e){var v=T6(o).hoistableStyles,z=v0(e);a=a||"default";var C=v.get(z);if(!C){var F={loading:0,preload:null};if(C=o.querySelector(V8(z)))F.loading=5;else{e=b({rel:"stylesheet",href:e,"data-precedence":a},l),(l=A4.get(z))&&W9(e,l);var U=C=o.createElement("link");$2(U),X2(U,"link",e),U._p=new Promise(function(t1,s1){U.onload=t1,U.onerror=s1}),U.addEventListener("load",function(){F.loading|=1}),U.addEventListener("error",function(){F.loading|=2}),F.loading|=4,Mt(C,a,o)}C={type:"stylesheet",instance:C,count:1,state:F},v.set(z,C)}}}function yv(e,a){m3.X(e,a);var l=m0;if(l&&e){var o=T6(l).hoistableScripts,v=p0(e),z=o.get(v);z||(z=l.querySelector(B8(v)),z||(e=b({src:e,async:!0},a),(a=A4.get(v))&&J9(e,a),z=l.createElement("script"),$2(z),X2(z,"link",e),l.head.appendChild(z)),z={type:"script",instance:z,count:1,state:null},o.set(v,z))}}function Mv(e,a){m3.M(e,a);var l=m0;if(l&&e){var o=T6(l).hoistableScripts,v=p0(e),z=o.get(v);z||(z=l.querySelector(B8(v)),z||(e=b({src:e,async:!0,type:"module"},a),(a=A4.get(v))&&J9(e,a),z=l.createElement("script"),$2(z),X2(z,"link",e),l.head.appendChild(z)),z={type:"script",instance:z,count:1,state:null},o.set(v,z))}}function Wi(e,a,l,o){var v=(v=m1.current)?yt(v):null;if(!v)throw Error(i(446));switch(e){case"meta":case"title":return null;case"style":return typeof l.precedence=="string"&&typeof l.href=="string"?(a=v0(l.href),l=T6(v).hoistableStyles,o=l.get(a),o||(o={type:"style",instance:null,count:0,state:null},l.set(a,o)),o):{type:"void",instance:null,count:0,state:null};case"link":if(l.rel==="stylesheet"&&typeof l.href=="string"&&typeof l.precedence=="string"){e=v0(l.href);var z=T6(v).hoistableStyles,C=z.get(e);if(C||(v=v.ownerDocument||v,C={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},z.set(e,C),(z=v.querySelector(V8(e)))&&!z._p&&(C.instance=z,C.state.loading=5),A4.has(e)||(l={rel:"preload",as:"style",href:l.href,crossOrigin:l.crossOrigin,integrity:l.integrity,media:l.media,hrefLang:l.hrefLang,referrerPolicy:l.referrerPolicy},A4.set(e,l),z||wv(v,e,l,C.state))),a&&o===null)throw Error(i(528,""));return C}if(a&&o!==null)throw Error(i(529,""));return null;case"script":return a=l.async,l=l.src,typeof l=="string"&&a&&typeof a!="function"&&typeof a!="symbol"?(a=p0(l),l=T6(v).hoistableScripts,o=l.get(a),o||(o={type:"script",instance:null,count:0,state:null},l.set(a,o)),o):{type:"void",instance:null,count:0,state:null};default:throw Error(i(444,e))}}function v0(e){return'href="'+z4(e)+'"'}function V8(e){return'link[rel="stylesheet"]['+e+"]"}function Ji(e){return b({},e,{"data-precedence":e.precedence,precedence:null})}function wv(e,a,l,o){e.querySelector('link[rel="preload"][as="style"]['+a+"]")?o.loading=1:(a=e.createElement("link"),o.preload=a,a.addEventListener("load",function(){return o.loading|=1}),a.addEventListener("error",function(){return o.loading|=2}),X2(a,"link",l),$2(a),e.head.appendChild(a))}function p0(e){return'[src="'+z4(e)+'"]'}function B8(e){return"script[async]"+e}function ts(e,a,l){if(a.count++,a.instance===null)switch(a.type){case"style":var o=e.querySelector('style[data-href~="'+z4(l.href)+'"]');if(o)return a.instance=o,$2(o),o;var v=b({},l,{"data-href":l.href,"data-precedence":l.precedence,href:null,precedence:null});return o=(e.ownerDocument||e).createElement("style"),$2(o),X2(o,"style",v),Mt(o,l.precedence,e),a.instance=o;case"stylesheet":v=v0(l.href);var z=e.querySelector(V8(v));if(z)return a.state.loading|=4,a.instance=z,$2(z),z;o=Ji(l),(v=A4.get(v))&&W9(o,v),z=(e.ownerDocument||e).createElement("link"),$2(z);var C=z;return C._p=new Promise(function(F,U){C.onload=F,C.onerror=U}),X2(z,"link",o),a.state.loading|=4,Mt(z,l.precedence,e),a.instance=z;case"script":return z=p0(l.src),(v=e.querySelector(B8(z)))?(a.instance=v,$2(v),v):(o=l,(v=A4.get(z))&&(o=b({},l),J9(o,v)),e=e.ownerDocument||e,v=e.createElement("script"),$2(v),X2(v,"link",o),e.head.appendChild(v),a.instance=v);case"void":return null;default:throw Error(i(443,a.type))}else a.type==="stylesheet"&&(a.state.loading&4)===0&&(o=a.instance,a.state.loading|=4,Mt(o,l.precedence,e));return a.instance}function Mt(e,a,l){for(var o=l.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),v=o.length?o[o.length-1]:null,z=v,C=0;C title"):null)}function Cv(e,a,l){if(l===1||a.itemProp!=null)return!1;switch(e){case"meta":case"title":return!0;case"style":if(typeof a.precedence!="string"||typeof a.href!="string"||a.href==="")break;return!0;case"link":if(typeof a.rel!="string"||typeof a.href!="string"||a.href===""||a.onLoad||a.onError)break;switch(a.rel){case"stylesheet":return e=a.disabled,typeof a.precedence=="string"&&e==null;default:return!0}case"script":if(a.async&&typeof a.async!="function"&&typeof a.async!="symbol"&&!a.onLoad&&!a.onError&&a.src&&typeof a.src=="string")return!0}return!1}function as(e){return!(e.type==="stylesheet"&&(e.state.loading&3)===0)}function Sv(e,a,l,o){if(l.type==="stylesheet"&&(typeof o.media!="string"||matchMedia(o.media).matches!==!1)&&(l.state.loading&4)===0){if(l.instance===null){var v=v0(o.href),z=a.querySelector(V8(v));if(z){a=z._p,a!==null&&typeof a=="object"&&typeof a.then=="function"&&(e.count++,e=Ct.bind(e),a.then(e,e)),l.state.loading|=4,l.instance=z,$2(z);return}z=a.ownerDocument||a,o=Ji(o),(v=A4.get(v))&&W9(o,v),z=z.createElement("link"),$2(z);var C=z;C._p=new Promise(function(F,U){C.onload=F,C.onerror=U}),X2(z,"link",o),l.instance=z}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(l,a),(a=l.state.preload)&&(l.state.loading&3)===0&&(e.count++,l=Ct.bind(e),a.addEventListener("load",l),a.addEventListener("error",l))}}var tc=0;function Hv(e,a){return e.stylesheets&&e.count===0&&Ht(e,e.stylesheets),0tc?50:800)+a);return e.unsuspend=l,function(){e.unsuspend=null,clearTimeout(o),clearTimeout(v)}}:null}function Ct(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Ht(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var St=null;function Ht(e,a){e.stylesheets=null,e.unsuspend!==null&&(e.count++,St=new Map,a.forEach(Av,e),St=null,Ct.call(e))}function Av(e,a){if(!(a.state.loading&4)){var l=St.get(e);if(l)var o=l.get(null);else{l=new Map,St.set(e,l);for(var v=e.querySelectorAll("link[data-precedence],style[data-precedence]"),z=0;z"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(c){console.error(c)}}return t(),oc.exports=Iv(),oc.exports}var $v=Pv();var As="popstate";function Zv(t={}){function c(i,u){let{pathname:h,search:d,hash:m}=i.location;return Qc("",{pathname:h,search:d,hash:m},u.state&&u.state.usr||null,u.state&&u.state.key||"default")}function n(i,u){return typeof u=="string"?u:c5(u)}return Yv(c,n,null,t)}function w2(t,c){if(t===!1||t===null||typeof t>"u")throw new Error(c)}function I4(t,c){if(!t){typeof console<"u"&&console.warn(c);try{throw new Error(c)}catch{}}}function Gv(){return Math.random().toString(36).substring(2,10)}function Ls(t,c){return{usr:t.state,key:t.key,idx:c}}function Qc(t,c,n=null,i){return{pathname:typeof t=="string"?t:t.pathname,search:"",hash:"",...typeof c=="string"?T0(c):c,state:n,key:c&&c.key||i||Gv()}}function c5({pathname:t="/",search:c="",hash:n=""}){return c&&c!=="?"&&(t+=c.charAt(0)==="?"?c:"?"+c),n&&n!=="#"&&(t+=n.charAt(0)==="#"?n:"#"+n),t}function T0(t){let c={};if(t){let n=t.indexOf("#");n>=0&&(c.hash=t.substring(n),t=t.substring(0,n));let i=t.indexOf("?");i>=0&&(c.search=t.substring(i),t=t.substring(0,i)),t&&(c.pathname=t)}return c}function Yv(t,c,n,i={}){let{window:u=document.defaultView,v5Compat:h=!1}=i,d=u.history,m="POP",p=null,x=g();x==null&&(x=0,d.replaceState({...d.state,idx:x},""));function g(){return(d.state||{idx:null}).idx}function b(){m="POP";let w=g(),V=w==null?null:w-x;x=w,p&&p({action:m,location:A.location,delta:V})}function y(w,V){m="PUSH";let H=Qc(A.location,w,V);x=g()+1;let S=Ls(H,x),j=A.createHref(H);try{d.pushState(S,"",j)}catch(T){if(T instanceof DOMException&&T.name==="DataCloneError")throw T;u.location.assign(j)}h&&p&&p({action:m,location:A.location,delta:1})}function M(w,V){m="REPLACE";let H=Qc(A.location,w,V);x=g();let S=Ls(H,x),j=A.createHref(H);d.replaceState(S,"",j),h&&p&&p({action:m,location:A.location,delta:0})}function L(w){return Kv(w)}let A={get action(){return m},get location(){return t(u,d)},listen(w){if(p)throw new Error("A history only accepts one active listener");return u.addEventListener(As,b),p=w,()=>{u.removeEventListener(As,b),p=null}},createHref(w){return c(u,w)},createURL:L,encodeLocation(w){let V=L(w);return{pathname:V.pathname,search:V.search,hash:V.hash}},push:y,replace:M,go(w){return d.go(w)}};return A}function Kv(t,c=!1){let n="http://localhost";typeof window<"u"&&(n=window.location.origin!=="null"?window.location.origin:window.location.href),w2(n,"No window.location.(origin|href) available to create URL");let i=typeof t=="string"?t:c5(t);return i=i.replace(/ $/,"%20"),!c&&i.startsWith("//")&&(i=n+i),new URL(i,n)}function Lu(t,c,n="/"){return Qv(t,c,n,!1)}function Qv(t,c,n,i){let u=typeof c=="string"?T0(c):c,h=b3(u.pathname||"/",n);if(h==null)return null;let d=Vu(t);Xv(d);let m=null;for(let p=0;m==null&&p{let g={relativePath:x===void 0?d.path||"":x,caseSensitive:d.caseSensitive===!0,childrenIndex:m,route:d};if(g.relativePath.startsWith("/")){if(!g.relativePath.startsWith(i)&&p)return;w2(g.relativePath.startsWith(i),`Absolute route path "${g.relativePath}" nested under path "${i}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),g.relativePath=g.relativePath.slice(i.length)}let b=x3([i,g.relativePath]),y=n.concat(g);d.children&&d.children.length>0&&(w2(d.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${b}".`),Vu(d.children,c,y,b,p)),!(d.path==null&&!d.index)&&c.push({path:b,score:np(b,d.index),routesMeta:y})};return t.forEach((d,m)=>{if(d.path===""||!d.path?.includes("?"))h(d,m);else for(let p of Bu(d.path))h(d,m,!0,p)}),c}function Bu(t){let c=t.split("/");if(c.length===0)return[];let[n,...i]=c,u=n.endsWith("?"),h=n.replace(/\?$/,"");if(i.length===0)return u?[h,""]:[h];let d=Bu(i.join("/")),m=[];return m.push(...d.map(p=>p===""?h:[h,p].join("/"))),u&&m.push(...d),m.map(p=>t.startsWith("/")&&p===""?"/":p)}function Xv(t){t.sort((c,n)=>c.score!==n.score?n.score-c.score:rp(c.routesMeta.map(i=>i.childrenIndex),n.routesMeta.map(i=>i.childrenIndex)))}var Wv=/^:[\w-]+$/,Jv=3,tp=2,ep=1,cp=10,ap=-2,Vs=t=>t==="*";function np(t,c){let n=t.split("/"),i=n.length;return n.some(Vs)&&(i+=ap),c&&(i+=tp),n.filter(u=>!Vs(u)).reduce((u,h)=>u+(Wv.test(h)?Jv:h===""?ep:cp),i)}function rp(t,c){return t.length===c.length&&t.slice(0,-1).every((i,u)=>i===c[u])?t[t.length-1]-c[c.length-1]:0}function lp(t,c,n=!1){let{routesMeta:i}=t,u={},h="/",d=[];for(let m=0;m{if(g==="*"){let L=m[y]||"";d=h.slice(0,h.length-L.length).replace(/(.)\/+$/,"$1")}const M=m[y];return b&&!M?x[g]=void 0:x[g]=(M||"").replace(/%2F/g,"/"),x},{}),pathname:h,pathnameBase:d,pattern:t}}function ip(t,c=!1,n=!0){I4(t==="*"||!t.endsWith("*")||t.endsWith("/*"),`Route path "${t}" will be treated as if it were "${t.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${t.replace(/\*$/,"/*")}".`);let i=[],u="^"+t.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(d,m,p)=>(i.push({paramName:m,isOptional:p!=null}),p?"/?([^\\/]+)?":"/([^\\/]+)")).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return t.endsWith("*")?(i.push({paramName:"*"}),u+=t==="*"||t==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):n?u+="\\/*$":t!==""&&t!=="/"&&(u+="(?:(?=\\/|$))"),[new RegExp(u,c?void 0:"i"),i]}function sp(t){try{return t.split("/").map(c=>decodeURIComponent(c).replace(/\//g,"%2F")).join("/")}catch(c){return I4(!1,`The URL path "${t}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${c}).`),t}}function b3(t,c){if(c==="/")return t;if(!t.toLowerCase().startsWith(c.toLowerCase()))return null;let n=c.endsWith("/")?c.length-1:c.length,i=t.charAt(n);return i&&i!=="/"?null:t.slice(n)||"/"}var op=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,up=t=>op.test(t);function hp(t,c="/"){let{pathname:n,search:i="",hash:u=""}=typeof t=="string"?T0(t):t,h;if(n)if(up(n))h=n;else{if(n.includes("//")){let d=n;n=n.replace(/\/\/+/g,"/"),I4(!1,`Pathnames cannot have embedded double slashes - normalizing ${d} -> ${n}`)}n.startsWith("/")?h=Bs(n.substring(1),"/"):h=Bs(n,c)}else h=c;return{pathname:h,search:mp(i),hash:vp(u)}}function Bs(t,c){let n=c.replace(/\/+$/,"").split("/");return t.split("/").forEach(u=>{u===".."?n.length>1&&n.pop():u!=="."&&n.push(u)}),n.length>1?n.join("/"):"/"}function fc(t,c,n,i){return`Cannot include a '${t}' character in a manually specified \`to.${c}\` field [${JSON.stringify(i)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function dp(t){return t.filter((c,n)=>n===0||c.route.path&&c.route.path.length>0)}function ju(t){let c=dp(t);return c.map((n,i)=>i===c.length-1?n.pathname:n.pathnameBase)}function Nu(t,c,n,i=!1){let u;typeof t=="string"?u=T0(t):(u={...t},w2(!u.pathname||!u.pathname.includes("?"),fc("?","pathname","search",u)),w2(!u.pathname||!u.pathname.includes("#"),fc("#","pathname","hash",u)),w2(!u.search||!u.search.includes("#"),fc("#","search","hash",u)));let h=t===""||u.pathname==="",d=h?"/":u.pathname,m;if(d==null)m=n;else{let b=c.length-1;if(!i&&d.startsWith("..")){let y=d.split("/");for(;y[0]==="..";)y.shift(),b-=1;u.pathname=y.join("/")}m=b>=0?c[b]:"/"}let p=hp(u,m),x=d&&d!=="/"&&d.endsWith("/"),g=(h||d===".")&&n.endsWith("/");return!p.pathname.endsWith("/")&&(x||g)&&(p.pathname+="/"),p}var x3=t=>t.join("/").replace(/\/\/+/g,"/"),fp=t=>t.replace(/\/+$/,"").replace(/^\/*/,"/"),mp=t=>!t||t==="?"?"":t.startsWith("?")?t:"?"+t,vp=t=>!t||t==="#"?"":t.startsWith("#")?t:"#"+t;function pp(t){return t!=null&&typeof t.status=="number"&&typeof t.statusText=="string"&&typeof t.internal=="boolean"&&"data"in t}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var ku=["POST","PUT","PATCH","DELETE"];new Set(ku);var gp=["GET",...ku];new Set(gp);var _0=f.createContext(null);_0.displayName="DataRouter";var ke=f.createContext(null);ke.displayName="DataRouterState";f.createContext(!1);var Fu=f.createContext({isTransitioning:!1});Fu.displayName="ViewTransition";var xp=f.createContext(new Map);xp.displayName="Fetchers";var zp=f.createContext(null);zp.displayName="Await";var Q4=f.createContext(null);Q4.displayName="Navigation";var m5=f.createContext(null);m5.displayName="Location";var y3=f.createContext({outlet:null,matches:[],isDataRoute:!1});y3.displayName="Route";var ja=f.createContext(null);ja.displayName="RouteError";function bp(t,{relative:c}={}){w2(v5(),"useHref() may be used only in the context of a component.");let{basename:n,navigator:i}=f.useContext(Q4),{hash:u,pathname:h,search:d}=p5(t,{relative:c}),m=h;return n!=="/"&&(m=h==="/"?n:x3([n,h])),i.createHref({pathname:m,search:d,hash:u})}function v5(){return f.useContext(m5)!=null}function X3(){return w2(v5(),"useLocation() may be used only in the context of a component."),f.useContext(m5).location}var Ru="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function Eu(t){f.useContext(Q4).static||f.useLayoutEffect(t)}function yp(){let{isDataRoute:t}=f.useContext(y3);return t?Fp():Mp()}function Mp(){w2(v5(),"useNavigate() may be used only in the context of a component.");let t=f.useContext(_0),{basename:c,navigator:n}=f.useContext(Q4),{matches:i}=f.useContext(y3),{pathname:u}=X3(),h=JSON.stringify(ju(i)),d=f.useRef(!1);return Eu(()=>{d.current=!0}),f.useCallback((p,x={})=>{if(I4(d.current,Ru),!d.current)return;if(typeof p=="number"){n.go(p);return}let g=Nu(p,JSON.parse(h),u,x.relative==="path");t==null&&c!=="/"&&(g.pathname=g.pathname==="/"?c:x3([c,g.pathname])),(x.replace?n.replace:n.push)(g,x.state,x)},[c,n,h,u,t])}f.createContext(null);function p5(t,{relative:c}={}){let{matches:n}=f.useContext(y3),{pathname:i}=X3(),u=JSON.stringify(ju(n));return f.useMemo(()=>Nu(t,JSON.parse(u),i,c==="path"),[t,u,i,c])}function wp(t,c){return Tu(t,c)}function Tu(t,c,n,i,u){w2(v5(),"useRoutes() may be used only in the context of a component.");let{navigator:h}=f.useContext(Q4),{matches:d}=f.useContext(y3),m=d[d.length-1],p=m?m.params:{},x=m?m.pathname:"/",g=m?m.pathnameBase:"/",b=m&&m.route;{let H=b&&b.path||"";_u(x,!b||H.endsWith("*")||H.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${x}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let y=X3(),M;if(c){let H=typeof c=="string"?T0(c):c;w2(g==="/"||H.pathname?.startsWith(g),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${g}" but pathname "${H.pathname}" was given in the \`location\` prop.`),M=H}else M=y;let L=M.pathname||"/",A=L;if(g!=="/"){let H=g.replace(/^\//,"").split("/");A="/"+L.replace(/^\//,"").split("/").slice(H.length).join("/")}let w=Lu(t,{pathname:A});I4(b||w!=null,`No routes matched location "${M.pathname}${M.search}${M.hash}" `),I4(w==null||w[w.length-1].route.element!==void 0||w[w.length-1].route.Component!==void 0||w[w.length-1].route.lazy!==void 0,`Matched leaf route at location "${M.pathname}${M.search}${M.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let V=Lp(w&&w.map(H=>Object.assign({},H,{params:Object.assign({},p,H.params),pathname:x3([g,h.encodeLocation?h.encodeLocation(H.pathname.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:H.pathname]),pathnameBase:H.pathnameBase==="/"?g:x3([g,h.encodeLocation?h.encodeLocation(H.pathnameBase.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:H.pathnameBase])})),d,n,i,u);return c&&V?f.createElement(m5.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",...M},navigationType:"POP"}},V):V}function Cp(){let t=kp(),c=pp(t)?`${t.status} ${t.statusText}`:t instanceof Error?t.message:JSON.stringify(t),n=t instanceof Error?t.stack:null,i="rgba(200,200,200, 0.5)",u={padding:"0.5rem",backgroundColor:i},h={padding:"2px 4px",backgroundColor:i},d=null;return console.error("Error handled by React Router default ErrorBoundary:",t),d=f.createElement(f.Fragment,null,f.createElement("p",null,"💿 Hey developer 👋"),f.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",f.createElement("code",{style:h},"ErrorBoundary")," or"," ",f.createElement("code",{style:h},"errorElement")," prop on your route.")),f.createElement(f.Fragment,null,f.createElement("h2",null,"Unexpected Application Error!"),f.createElement("h3",{style:{fontStyle:"italic"}},c),n?f.createElement("pre",{style:u},n):null,d)}var Sp=f.createElement(Cp,null),Hp=class extends f.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,c){return c.location!==t.location||c.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:c.error,location:c.location,revalidation:t.revalidation||c.revalidation}}componentDidCatch(t,c){this.props.onError?this.props.onError(t,c):console.error("React Router caught the following error during render",t)}render(){return this.state.error!==void 0?f.createElement(y3.Provider,{value:this.props.routeContext},f.createElement(ja.Provider,{value:this.state.error,children:this.props.component})):this.props.children}};function Ap({routeContext:t,match:c,children:n}){let i=f.useContext(_0);return i&&i.static&&i.staticContext&&(c.route.errorElement||c.route.ErrorBoundary)&&(i.staticContext._deepestRenderedBoundaryId=c.route.id),f.createElement(y3.Provider,{value:t},n)}function Lp(t,c=[],n=null,i=null,u=null){if(t==null){if(!n)return null;if(n.errors)t=n.matches;else if(c.length===0&&!n.initialized&&n.matches.length>0)t=n.matches;else return null}let h=t,d=n?.errors;if(d!=null){let g=h.findIndex(b=>b.route.id&&d?.[b.route.id]!==void 0);w2(g>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(d).join(",")}`),h=h.slice(0,Math.min(h.length,g+1))}let m=!1,p=-1;if(n)for(let g=0;g=0?h=h.slice(0,p+1):h=[h[0]];break}}}let x=n&&i?(g,b)=>{i(g,{location:n.location,params:n.matches?.[0]?.params??{},errorInfo:b})}:void 0;return h.reduceRight((g,b,y)=>{let M,L=!1,A=null,w=null;n&&(M=d&&b.route.id?d[b.route.id]:void 0,A=b.route.errorElement||Sp,m&&(p<0&&y===0?(_u("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),L=!0,w=null):p===y&&(L=!0,w=b.route.hydrateFallbackElement||null)));let V=c.concat(h.slice(0,y+1)),H=()=>{let S;return M?S=A:L?S=w:b.route.Component?S=f.createElement(b.route.Component,null):b.route.element?S=b.route.element:S=g,f.createElement(Ap,{match:b,routeContext:{outlet:g,matches:V,isDataRoute:n!=null},children:S})};return n&&(b.route.ErrorBoundary||b.route.errorElement||y===0)?f.createElement(Hp,{location:n.location,revalidation:n.revalidation,component:A,error:M,children:H(),routeContext:{outlet:null,matches:V,isDataRoute:!0},onError:x}):H()},null)}function Na(t){return`${t} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Vp(t){let c=f.useContext(_0);return w2(c,Na(t)),c}function Bp(t){let c=f.useContext(ke);return w2(c,Na(t)),c}function jp(t){let c=f.useContext(y3);return w2(c,Na(t)),c}function ka(t){let c=jp(t),n=c.matches[c.matches.length-1];return w2(n.route.id,`${t} can only be used on routes that contain a unique "id"`),n.route.id}function Np(){return ka("useRouteId")}function kp(){let t=f.useContext(ja),c=Bp("useRouteError"),n=ka("useRouteError");return t!==void 0?t:c.errors?.[n]}function Fp(){let{router:t}=Vp("useNavigate"),c=ka("useNavigate"),n=f.useRef(!1);return Eu(()=>{n.current=!0}),f.useCallback(async(u,h={})=>{I4(n.current,Ru),n.current&&(typeof u=="number"?t.navigate(u):await t.navigate(u,{fromRouteId:c,...h}))},[t,c])}var js={};function _u(t,c,n){!c&&!js[t]&&(js[t]=!0,I4(!1,n))}f.memo(Rp);function Rp({routes:t,future:c,state:n,unstable_onError:i}){return Tu(t,void 0,n,i,c)}function S6(t){w2(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function Ep({basename:t="/",children:c=null,location:n,navigationType:i="POP",navigator:u,static:h=!1}){w2(!v5(),"You cannot render a inside another . You should never have more than one in your app.");let d=t.replace(/^\/*/,"/"),m=f.useMemo(()=>({basename:d,navigator:u,static:h,future:{}}),[d,u,h]);typeof n=="string"&&(n=T0(n));let{pathname:p="/",search:x="",hash:g="",state:b=null,key:y="default"}=n,M=f.useMemo(()=>{let L=b3(p,d);return L==null?null:{location:{pathname:L,search:x,hash:g,state:b,key:y},navigationType:i}},[d,p,x,g,b,y,i]);return I4(M!=null,` is not able to match the URL "${p}${x}${g}" because it does not start with the basename, so the won't render anything.`),M==null?null:f.createElement(Q4.Provider,{value:m},f.createElement(m5.Provider,{children:c,value:M}))}function Tp({children:t,location:c}){return wp(Xc(t),c)}function Xc(t,c=[]){let n=[];return f.Children.forEach(t,(i,u)=>{if(!f.isValidElement(i))return;let h=[...c,u];if(i.type===f.Fragment){n.push.apply(n,Xc(i.props.children,h));return}w2(i.type===S6,`[${typeof i.type=="string"?i.type:i.type.name}] is not a component. All component children of must be a or `),w2(!i.props.index||!i.props.children,"An index route cannot have child routes.");let d={id:i.props.id||h.join("-"),caseSensitive:i.props.caseSensitive,element:i.props.element,Component:i.props.Component,index:i.props.index,path:i.props.path,middleware:i.props.middleware,loader:i.props.loader,action:i.props.action,hydrateFallbackElement:i.props.hydrateFallbackElement,HydrateFallback:i.props.HydrateFallback,errorElement:i.props.errorElement,ErrorBoundary:i.props.ErrorBoundary,hasErrorBoundary:i.props.hasErrorBoundary===!0||i.props.ErrorBoundary!=null||i.props.errorElement!=null,shouldRevalidate:i.props.shouldRevalidate,handle:i.props.handle,lazy:i.props.lazy};i.props.children&&(d.children=Xc(i.props.children,h)),n.push(d)}),n}var ee="get",ce="application/x-www-form-urlencoded";function Fe(t){return t!=null&&typeof t.tagName=="string"}function _p(t){return Fe(t)&&t.tagName.toLowerCase()==="button"}function qp(t){return Fe(t)&&t.tagName.toLowerCase()==="form"}function Dp(t){return Fe(t)&&t.tagName.toLowerCase()==="input"}function Op(t){return!!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey)}function Up(t,c){return t.button===0&&(!c||c==="_self")&&!Op(t)}var Ft=null;function Ip(){if(Ft===null)try{new FormData(document.createElement("form"),0),Ft=!1}catch{Ft=!0}return Ft}var Pp=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function mc(t){return t!=null&&!Pp.has(t)?(I4(!1,`"${t}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${ce}"`),null):t}function $p(t,c){let n,i,u,h,d;if(qp(t)){let m=t.getAttribute("action");i=m?b3(m,c):null,n=t.getAttribute("method")||ee,u=mc(t.getAttribute("enctype"))||ce,h=new FormData(t)}else if(_p(t)||Dp(t)&&(t.type==="submit"||t.type==="image")){let m=t.form;if(m==null)throw new Error('Cannot submit a ); })} @@ -194,6 +194,7 @@ export default function ControlPadPanel({ disabled = false }) {
stopDrivePad('stop')} diff --git a/webui/src/components/MobileControls/FloatingJoystick.jsx b/webui/src/components/MobileControls/FloatingJoystick.jsx index 87401d65..d1a13bb5 100644 --- a/webui/src/components/MobileControls/FloatingJoystick.jsx +++ b/webui/src/components/MobileControls/FloatingJoystick.jsx @@ -119,7 +119,13 @@ function FloatingPadOverlay({ center, size, activeCellId }) { ); } -export default function FloatingJoystick({ activeInputLabel = 'stop', disabled, onCellChange, onStop }) { +export default function FloatingJoystick({ + activeInputLabel = 'stop', + compact = false, + disabled, + onCellChange, + onStop, +}) { const containerRef = useRef(null); const pointerIdRef = useRef(null); const activePadRef = useRef(null); @@ -209,7 +215,7 @@ export default function FloatingJoystick({ activeInputLabel = 'stop', disabled,
event.preventDefault()} > -
- {/* The readout lives inside the pointer target instead of above it, so the - visual indicator does not consume any non-drivable space on small phones. */} - - {activeInputLabel} - -
-
- drive pad - hold and drag -
+ {!compact ? ( +
+ {/* The readout lives inside the pointer target instead of above it, so the + visual indicator does not consume any non-drivable space on small phones. */} + + {activeInputLabel} + +
+ ) : null} + {!compact ? ( +
+ drive pad + hold and drag +
+ ) : ( +
+ )}
{activePad ? ( entry?.socketId || entry).filter(Boolean) + : []; + const queue = currentId ? [currentId, ...waiting.filter((id) => id !== currentId)] : waiting; + const nextId = currentId ? waiting[0] || null : queue[0] || null; + return { queue, currentId, nextId }; +} + +function usePtzQueueLookup(ptz = null) { + const users = useSessionSelector((state) => state.session?.users ?? []); + return useCallback( + (socketId) => { + const fromUsers = users.find((entry) => entry.socketId === socketId); + if (fromUsers) return fromUsers; + if (ptz?.operatorSocketId === socketId) { + return { socketId, nickname: ptz?.operatorLabel || null, role: null }; + } + const fromQueue = Array.isArray(ptz?.queue) + ? ptz.queue.find((entry) => (entry?.socketId || entry) === socketId) + : null; + return { + socketId, + nickname: fromQueue?.label || null, + role: null, + }; + }, + [ptz, users], + ); +} + +function PtzSnapshotPreview({ feed, label = 'PTZ Camera', className = 'h-full w-full' }) { + return ( +
+ {feed?.objectUrl ? ( + {label} + ) : ( +
Waiting for snapshot...
+ )} + {/* + Snapshot mode should look like the regular rover video player: the + camera name belongs to the surrounding card/menu, while the media pane + only exposes stream health in the small top-left diagnostic overlay. + */} +
+
+ Status: {feed?.error ? `Error: ${feed.error}` : feed?.status || 'connecting'} +
+
+
+ ); +} + +function StatusRow({ label, value, tone = '' }) { + return ( +
+ {label} + {value} +
+ ); +} + +function PtzStatePanel({ ptz, compact = false }) { + const now = useSharedClock(1000, Boolean(ptz?.deadline)); + const spotlightOn = isSpotlightOn(ptz?.light); + const irMode = normalizeIrMode(ptz?.ir?.state); + const publisher = ptz?.publisher || {}; + const publisherStatus = publisher.running + ? 'running' + : publisher.restartAt + ? 'restarting' + : publisher.lastEvent || 'stopped'; + const mode = ptz?.isOperator ? 'operator' : ptz?.queuedPosition ? `queued ${ptz.queuedPosition}` : 'spectator'; + + return ( + + + + + + + + {!compact ? : null} + {ptz?.blocked?.message ? ( +
+ {ptz.blocked.message} +
+ ) : null} +
+ ); +} + +function PtzQueueSummary({ ptz, title = 'PTZ queue' }) { + const selfId = useSessionSelector((state) => state.session?.socketId || null); + const lookupUser = usePtzQueueLookup(ptz); + const { queue, currentId, nextId } = normalizePtzQueue(ptz); + + return ( + + + + ); +} + +function PtzLightingControls({ ptz, disabled = false }) { + const { ptzSpotlight, ptzIr } = useSessionActions(); + const [busy, setBusy] = useState(''); + const spotlightOn = isSpotlightOn(ptz?.light); + const irMode = normalizeIrMode(ptz?.ir?.state); + + const toggleSpotlight = async (nextOn) => { + if (disabled) return; + setBusy('spotlight'); + try { + await ptzSpotlight({ state: nextOn ? 1 : 0 }); + } finally { + setBusy(''); + } + }; + + const cycleIr = async () => { + if (disabled) return; + setBusy('ir'); + try { + await ptzIr({ state: nextIrMode(irMode) }); + } finally { + setBusy(''); + } + }; + + return ( +
+ + +
+ ); +} + +function PtzMobileZoomButtons({ disabled = false }) { + const { nudgeServo, stopAllMotion } = useControlActions(); + const repeatTimerRef = useRef(null); + + const stopZoom = useCallback(() => { + /* + Mobile zoom is intentionally routed through the normal camera-up/down + control action instead of emitting PTZ socket commands directly. That + keeps the zoom buttons on the same path as keyboard/gamepad camera tilt, + and the PTZ adapter remains the one place that translates "camera nudge" + into Reolink zoom pulses. + */ + if (repeatTimerRef.current) { + clearInterval(repeatTimerRef.current); + repeatTimerRef.current = null; + } + stopAllMotion(); + }, [stopAllMotion]); + + const startZoom = useCallback( + (direction) => (event) => { + /* + Send an immediate nudge and then repeat while held. The adapter turns + each nudge into a short zoom pulse, so repeating the standard action is + the simplest way to get continuous hold-to-zoom without adding another + PTZ-specific command loop. + */ + event.preventDefault(); + if (disabled) return; + stopZoom(); + nudgeServo(direction); + repeatTimerRef.current = setInterval(() => { + nudgeServo(direction); + }, 120); + }, + [disabled, nudgeServo, stopZoom], + ); + const stopFromPointer = useCallback( + (event) => { + event?.preventDefault?.(); + if (disabled) return; + stopZoom(); + }, + [disabled, stopZoom], + ); + + useEffect( + () => () => { + /* + A touch surface can unmount during orientation changes or fullscreen + close while a pointer is still down. Clear the repeat timer here so a + held zoom button cannot keep firing camera-up/down actions after the + mobile controls have disappeared. + */ + if (repeatTimerRef.current) { + clearInterval(repeatTimerRef.current); + repeatTimerRef.current = null; + } + }, + [], + ); + + return ( +
+ + +
+ ); +} + +function PtzMobileControlsPanel({ ptz, disabled = false }) { + return ( +
+ +
+ {/* + Reuse the rover control pad so touch intent still enters the normal + control system. The PTZ adapter translates that same drive vector into + pan/tilt commands only while this user is the PTZ operator. + */} + +
+ +
+ ); +} + +function keyLabelFor(keymap, actionId) { + return formatKeyLabel(keymap?.[actionId]?.[0]); +} + +function PtzControlReference() { + const keymap = useControlSelector((control) => control.state.keymap); + const rows = [ + ['Tilt up', 'driveForward'], + ['Tilt down', 'driveBackward'], + ['Pan left', 'driveLeft'], + ['Pan right', 'driveRight'], + ['Zoom in', 'cameraUp'], + ['Zoom out', 'cameraDown'], + ['Spotlight', 'headlightToggle'], + ['Infrared mode', 'laserToggle'], + ]; + + return ( + + {rows.map(([label, actionId]) => ( +
+ {label} + +
+ ))} +
+ ); +} + +function PtzPresetPanel({ ptz }) { + const role = useSessionSelector((state) => state.session?.role || null); + const { + ptzListPresets, + ptzGotoPreset, + ptzCreatePreset, + ptzRemovePreset, + } = useSessionActions(); + const [name, setName] = useState(''); + const [busy, setBusy] = useState(''); + const presets = Array.isArray(ptz?.presets) ? ptz.presets : []; + const isPresetAdmin = role === 'admin' || role === 'lockdown'; + const canMoveToPreset = Boolean(ptz?.isOperator); + + const refreshPresets = async () => { + if (busy) return; + setBusy('refresh'); + try { + /* + Presets live on the camera, not in browser state. A manual refresh gives + admins a simple recovery path if another admin or the camera's native + app changes preset storage while this UI is already open. + */ + await ptzListPresets(); + } catch (err) { + alert(err.message || 'Failed to refresh PTZ presets.'); + } finally { + setBusy(''); + } + }; + + const goToPreset = async (preset) => { + if (!canMoveToPreset || busy || !preset?.token) return; + setBusy(`goto:${preset.token}`); + try { + /* + Moving to a preset is a physical camera move, so the server still checks + that this browser owns the active PTZ turn before accepting the command. + */ + await ptzGotoPreset({ token: preset.token }); + } catch (err) { + alert(err.message || 'Failed to move to PTZ preset.'); + } finally { + setBusy(''); + } + }; + + const createPreset = async (event) => { + event.preventDefault(); + const trimmed = name.trim(); + if (!isPresetAdmin || busy || !trimmed) return; + setBusy('create'); + try { + /* + ONVIF setPreset stores the camera's current physical position. The UI + only sends the admin's label; the server supplies the active profile + token so browser code does not need to know camera profile internals. + */ + await ptzCreatePreset({ name: trimmed }); + setName(''); + } catch (err) { + alert(err.message || 'Failed to create PTZ preset.'); + } finally { + setBusy(''); + } + }; + + const removePreset = async (preset) => { + if (!isPresetAdmin || busy || !preset?.token) return; + const confirmed = window.confirm(`Remove preset "${preset.name}"?`); + if (!confirmed) return; + setBusy(`remove:${preset.token}`); + try { + /* + The token is the camera's durable preset identifier. Names are only UI + labels and may not be unique, so deletion always targets the token. + */ + await ptzRemovePreset({ token: preset.token }); + } catch (err) { + alert(err.message || 'Failed to remove PTZ preset.'); + } finally { + setBusy(''); + } + }; + + return ( + + Refresh + + )} + bodyClassName="flex min-h-0 flex-col gap-1 p-1 text-xs" + > + {ptz?.presetsError ? ( +
+ {ptz.presetsError} +
+ ) : null} +
+ {presets.length ? presets.map((preset) => { + const gotoBusy = busy === `goto:${preset.token}`; + const removeBusy = busy === `remove:${preset.token}`; + return ( +
+ + {isPresetAdmin ? ( + + ) : null} +
+ ); + }) : ( +
+ No presets saved. +
+ )} +
+ {isPresetAdmin ? ( + + setName(event.target.value)} + placeholder="Preset name" + /> + + + ) : null} +
+ ); +} + +function buildPtzTurnModel(ptz, selfId) { + const { queue, currentId, nextId } = normalizePtzQueue(ptz); + const isActive = Boolean(ptz?.isOperator); + const isQueued = Boolean(ptz?.queuedPosition); + return { + enabled: Boolean(ptz && (isActive || isQueued || currentId)), + targetId: ptz?.id || PTZ_CAMERA_ID, + activeId: currentId, + nextId, + isActive, + isNext: Boolean(selfId && nextId === selfId), + deadline: ptz?.deadline || null, + idleDeadline: null, + showNotTurnNotice: Boolean(!isActive && (isQueued || currentId || queue.length)), + showPreviewReason: false, + }; +} + +function PtzMediaPane({ ptz, open, framed = true }) { + const isOperator = Boolean(ptz?.isOperator); + const selfId = useSessionSelector((state) => state.session?.socketId || null); + const snapshotFeeds = usePtzCameraSnapshots([PTZ_CAMERA_ID], { enabled: open && !isOperator }); + const snapshot = snapshotFeeds[PTZ_CAMERA_ID] || null; + const turnModel = useMemo(() => buildPtzTurnModel(ptz, selfId), [ptz, selfId]); + const media = ( + <> + {isOperator ? ( + + ) : ( + + )} + + + ); + + if (!framed) { + return
{media}
; + } + + return ( +
+
+ {media} +
+
+ ); +} + +function PtzDesktopFullscreen({ ptz, releasePending }) { + return ( +
+
+
+ +
+ +
+
+ + +
+ {releasePending ? ( +
+ Closing... +
+ ) : null} +
+ ); +} + +function PtzMobileFullscreen({ ptz, layout, onClose, releasePending = false }) { + const landscape = layout === 'mobile-landscape'; + const topHeightClass = landscape ? 'h-full min-h-[calc(100dvh-0.25rem)]' : 'h-[48dvh]'; + const topGridClass = landscape + ? 'grid-cols-[minmax(0,1fr)_13rem]' + : 'grid-cols-[minmax(0,1fr)_11rem]'; + + return ( +
+
+
+ + +
+ +
+
+ +
+ + + + +
+
+
+ ); +} + +export function PtzFullscreenController({ open, onClose, layout = 'desktop' }) { + const ptz = useSessionSelector((state) => state.session?.ptzCamera || null); + const { ptzRelease } = useSessionActions(); + const { stopAllMotion } = useControlActions(); + const [releasePending, setReleasePending] = useState(false); + const isMobile = layout === 'mobile-portrait' || layout === 'mobile-landscape'; + + const releaseAndClose = useCallback(async () => { + if (releasePending) return; + setReleasePending(true); + try { + /* + Stop first so a held key/pointer cannot leave ONVIF continuous movement + running while the server removes this socket from the PTZ queue. + */ + stopAllMotion?.(); + await ptzRelease(); + onClose?.(); + } finally { + setReleasePending(false); + } + }, [onClose, ptzRelease, releasePending, stopAllMotion]); + + if (!open) return null; + + const controller = ( + /* + The PTZ controller needs to cover the driver page, but it must not become + the top-most application layer. Global fullscreen overlays like help, + quickstart, mode gates, and connection warnings are still part of the + active app state while PTZ is open, so this portal intentionally sits + below their z-30+ overlay stack instead of hiding them. + */ +
+ + Close + + )} + hideHeader={isMobile} + fillHeight + clipOverflow={false} + className="h-[100dvh] w-[100vw] rounded-none border-0 !bg-black" + bodyClassName="relative min-h-0 flex-1" + > + {isMobile ? ( + + ) : ( + + )} + +
+ ); + + return createPortal(controller, document.body); +} + +export default function PtzQueueCard({ layout = 'desktop' }) { + const featureEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'ptzCamera')); + const ptz = useSessionSelector((state) => state.session?.ptzCamera || null); + const isVerified = useSessionSelector((state) => Boolean(state.session?.isVerified)); + const role = useSessionSelector((state) => state.session?.role || null); + const selfId = useSessionSelector((state) => state.session?.socketId || null); + const { ptzClaim, ptzRelease } = useSessionActions(); + const lookupUser = usePtzQueueLookup(ptz); + const { queue, currentId, nextId } = normalizePtzQueue(ptz); + const now = useSharedClock(1000, Boolean(ptz?.deadline)); + const [controllerOpen, setControllerOpen] = useState(false); + const [pending, setPending] = useState(false); + const canUse = Boolean(ptz?.canUse || isVerified || role === 'admin' || role === 'lockdown'); + const isParticipant = Boolean(ptz?.isOperator || ptz?.queuedPosition); + const timerLabel = ptz?.isOperator && ptz?.deadline ? `${formatRemaining(ptz.deadline, now)} left` : ''; + + if (!featureEnabled) return null; + + const handleRequest = async () => { + if (!canUse || pending) return; + if (isParticipant) { + setControllerOpen(true); + return; + } + setPending(true); + trackAnalyticsEvent('ptz_queue_join', { layout }); + try { + const response = await ptzClaim(); + /* + The server is authoritative for whether the click became an active turn + or a queued wait. Open only after it confirms one of those states so a + dock-guard rejection does not strand the user in fullscreen. + */ + if (response?.state?.isOperator || response?.state?.queuedPosition) { + setControllerOpen(true); + } + trackAnalyticsEvent('ptz_queue_join_result', { layout, status: 'accepted' }); + } catch (err) { + trackAnalyticsEvent('ptz_queue_join_result', { + layout, + status: 'failed', + reason: err?.message || 'unknown', + }); + alert(err.message || 'PTZ request failed.'); + } finally { + setPending(false); + } + }; + + const handleLeave = async () => { + if (pending) return; + setPending(true); + try { + await ptzRelease(); + } catch (err) { + alert(err.message || 'Failed to leave PTZ camera.'); + } finally { + setPending(false); + } + }; + + const actionLabel = pending + ? '...' + : ptz?.isOperator + ? 'Open' + : ptz?.queuedPosition + ? 'Open' + : 'request'; + + return ( + <> + +
    + +
+ {isParticipant ? ( + + ) : null} + {!canUse ? ( +
+ Verify your account to use the PTZ camera. +
+ ) : null} +
+ setControllerOpen(false)} layout={layout} /> + + ); +} diff --git a/webui/src/components/PtzLiveVideo/index.jsx b/webui/src/components/PtzLiveVideo/index.jsx new file mode 100644 index 00000000..1b1ee5d6 --- /dev/null +++ b/webui/src/components/PtzLiveVideo/index.jsx @@ -0,0 +1,213 @@ +// PTZ Live Video +// Purpose: Plays the single PTZ camera WHEP stream with the same fresh-session retry loop used by rover video. +// Scope: Owns browser-side WHEP playback/retry only; server authorization and snapshot fallback policy stay outside. +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useVideoRequests } from '../../hooks/useVideoRequests.js'; +import { WhepPlayer } from '../../lib/whepPlayer.js'; +import { RESTART_DELAY_MS } from '../RoverMediaPlayer/constants.js'; + +export const PTZ_CAMERA_ID = 'ptz-camera'; + +const PTZ_AUDIO_RETRY_MS = 1000; +const TERMINAL_WHEP_STATES = new Set(['error', 'failed', 'disconnected', 'closed']); +const AUTHORIZATION_ERROR_RE = /not authorized/i; + +function isAuthorizationError(error) { + return AUTHORIZATION_ERROR_RE.test(String(error || '')); +} + +export default function PtzLiveVideo({ + enabled = true, + startMuted = true, + className = 'relative h-full w-full bg-black', + videoClassName = 'h-full w-full object-contain', + statusClassName = 'pointer-events-none absolute left-1 top-1 z-20 font-medium text-slate-100 text-[0.65rem]', + fallback = null, +}) { + const videoRef = useRef(null); + const retryTimerRef = useRef(null); + const playTimerRef = useRef(null); + const enabledRef = useRef(enabled); + const fallbackRef = useRef(false); + const playerGenerationRef = useRef(0); + const [status, setStatus] = useState('idle'); + const [detail, setDetail] = useState(null); + const [restartToken, setRestartToken] = useState(0); + const sources = useVideoRequests( + [{ type: 'ptz', id: PTZ_CAMERA_ID, key: PTZ_CAMERA_ID }], + { enabled, version: restartToken }, + ); + const source = sources[PTZ_CAMERA_ID] || null; + const shouldUseFallback = Boolean(source?.error && isAuthorizationError(source.error)); + + useEffect(() => { + enabledRef.current = enabled; + }, [enabled]); + + useEffect(() => { + fallbackRef.current = shouldUseFallback; + }, [shouldUseFallback]); + + useEffect(() => { + if (enabled && !shouldUseFallback) return undefined; + /* + A retry that was scheduled before the server denied live access should not + keep firing in snapshot mode. Clear it when live playback is no longer the + active display policy. + */ + if (retryTimerRef.current) { + clearTimeout(retryTimerRef.current); + retryTimerRef.current = null; + } + return undefined; + }, [enabled, shouldUseFallback]); + + const scheduleRestart = useCallback(() => { + if (!enabledRef.current || fallbackRef.current) return; + /* + WHEP sessions are one-shot browser/server negotiations. When the camera + reboots, the old PeerConnection and token can look alive enough to keep a + black element on screen, but they are not useful anymore. Bumping this + token forces useVideoRequests to ask the server for a new MediaMTX auth + session before creating the next WhepPlayer. + */ + clearTimeout(retryTimerRef.current); + retryTimerRef.current = setTimeout(() => { + retryTimerRef.current = null; + setRestartToken(Date.now()); + }, RESTART_DELAY_MS); + }, []); + + useEffect(() => { + if (!enabled || shouldUseFallback || !source?.url || !videoRef.current) return undefined; + let active = true; + const generation = playerGenerationRef.current + 1; + playerGenerationRef.current = generation; + const player = new WhepPlayer({ + url: source.url, + token: source.token, + video: videoRef.current, + startMuted, + onStatus: (nextStatus, info) => { + /* + Old PeerConnection callbacks can arrive after React has already + cleaned up this effect for a newer token. Only the currently-owned + generation is allowed to update status or schedule another restart. + */ + if (!active || playerGenerationRef.current !== generation) return; + const normalized = String(nextStatus || '').toLowerCase(); + setStatus(nextStatus || 'unknown'); + setDetail(info || null); + if (TERMINAL_WHEP_STATES.has(normalized)) { + scheduleRestart(); + } + }, + }); + + player.start().catch((err) => { + if (!active || playerGenerationRef.current !== generation) return; + setStatus('error'); + setDetail(err.message || 'WHEP start failed'); + scheduleRestart(); + }); + + return () => { + /* + Mark inactive before stop() because WhepPlayer reports "stopped" during + normal cleanup. Cleanup-driven stops should not immediately schedule the + next retry; only the replacement effect should own the new connection. + */ + active = false; + player.stop(); + }; + }, [enabled, scheduleRestart, shouldUseFallback, source?.token, source?.url, startMuted]); + + useEffect(() => { + const video = videoRef.current; + if (!enabled || shouldUseFallback || !source?.url || !video) return undefined; + const generation = playerGenerationRef.current; + + const handleEnded = () => { + if (playerGenerationRef.current !== generation) return; + setStatus('stopped'); + setDetail('ended'); + scheduleRestart(); + }; + const handleError = () => { + if (playerGenerationRef.current !== generation) return; + setStatus('error'); + setDetail(video.error?.message || 'video element error'); + scheduleRestart(); + }; + + video.addEventListener('ended', handleEnded); + video.addEventListener('error', handleError); + return () => { + video.removeEventListener('ended', handleEnded); + video.removeEventListener('error', handleError); + }; + }, [enabled, scheduleRestart, shouldUseFallback, source?.url]); + + useEffect(() => { + const video = videoRef.current; + if (!enabled || shouldUseFallback || startMuted || !source?.url || !video) { + if (playTimerRef.current) clearInterval(playTimerRef.current); + playTimerRef.current = null; + return undefined; + } + + /* + PTZ carries inline Opus audio. When the operator opened the camera from a + user gesture, keep retrying audible playback so browser autoplay timing + does not leave the element permanently muted after a reconnect. + */ + const attemptPlay = () => { + const target = videoRef.current; + if (!target) return; + target.muted = false; + if (!target.paused && !target.ended) return; + target.play().catch(() => {}); + }; + + attemptPlay(); + playTimerRef.current = setInterval(attemptPlay, PTZ_AUDIO_RETRY_MS); + return () => { + if (playTimerRef.current) clearInterval(playTimerRef.current); + playTimerRef.current = null; + }; + }, [enabled, shouldUseFallback, source?.url, startMuted, status]); + + useEffect(() => () => { + if (retryTimerRef.current) clearTimeout(retryTimerRef.current); + if (playTimerRef.current) clearInterval(playTimerRef.current); + }, []); + + if (shouldUseFallback && typeof fallback === 'function') { + return fallback({ source, status, detail }); + } + + const displayStatus = source?.error || detail || status; + + return ( +
+ {source?.url ? ( +
+ ); +} diff --git a/webui/src/components/QueueTargetRow/index.jsx b/webui/src/components/QueueTargetRow/index.jsx new file mode 100644 index 00000000..4b72d48c --- /dev/null +++ b/webui/src/components/QueueTargetRow/index.jsx @@ -0,0 +1,196 @@ +// Queue Target Row +// Purpose: Renders the shared queue-row visual language used by rover queues and PTZ. +// Scope: Owns row chrome, queue chips, timer labels, and row/button event plumbing; +// callers still own target-specific permission checks and request actions. +import RoverLabel from '../RoverLabel/index.jsx'; + +function classNames(...values) { + return values.filter(Boolean).join(' '); +} + +function roleColors(role) { + switch (role) { + case 'admin': + case 'lockdown': + return 'text-amber-300'; + case 'spectator': + return 'text-slate-400'; + default: + return 'text-sky-300'; + } +} + +function formatQueueUserLabel(user, selfId) { + /* + Queue chips need to be readable even when a socket has no nickname yet. + Keeping the socket-prefix fallback here means rover queues and PTZ queues + degrade identically instead of each target inventing its own anonymous label. + */ + if (!user) return ''; + const base = user.nickname || user.label || user.socketId?.slice(0, 6) || 'unknown'; + if (user.socketId && user.socketId === selfId) { + return `${base} (you)`; + } + return base; +} + +export function QueueUserChips({ + targetId, + queue = [], + currentId = null, + nextId = null, + selfId = null, + lookupUser, +}) { + if (!queue.length) { + return

No queue.

; + } + + return ( +
+ {queue.map((socketId, idx) => { + const user = lookupUser?.(socketId) || { socketId, nickname: null, role: null }; + const isCurrent = socketId === currentId; + const isNext = Boolean(nextId && socketId === nextId && !isCurrent); + /* + These classes intentionally mirror the original rover queue styling. + PTZ feeds the same current/next model into this component, so the user + does not have to learn a different visual vocabulary for camera turns. + */ + const highlightClass = isCurrent + ? 'bg-sky-600 text-white ring-2 ring-amber-300' + : isNext + ? 'bg-emerald-700/60 text-emerald-100 ring-1 ring-emerald-300/70' + : 'bg-slate-800 text-slate-200'; + return ( + + + {formatQueueUserLabel(user, selfId)} + + {isCurrent && now} + {isNext && next} + + ); + })} +
+ ); +} + +export default function QueueTargetRow({ + target, + queue = [], + currentId = null, + nextId = null, + selfId = null, + lookupUser, + canClick = false, + pending = false, + locked = false, + lockedBlocked = false, + privateOpen = false, + buttonLabel = '', + batteryLabel = '', + batteryClassName = 'text-slate-400', + timerLabel = '', + thumbnailUrl = '', + onRequest, + showAction = true, +}) { + const targetId = String(target?.id || ''); + const targetLabel = target?.label || target?.name || targetId; + + return ( +
  • { + /* + The whole row is a large target because queue selection is one of the + main touch/click actions on the page. The caller still decides whether + clicking is currently allowed, so disabled PTZ and locked rover states + cannot accidentally request control through the shared renderer. + */ + if (!canClick) return; + onRequest?.(targetId); + }} + > + {thumbnailUrl ? ( + + ) : null} +
    +
    +
    +

    + + {target?.description ? ( + + {target.description} + + ) : null} +

    + {timerLabel ? ( + + {timerLabel} + + ) : null} +
    + {batteryLabel ? ( + + {batteryLabel} + + ) : null} +
    + +
    + {showAction ? ( + + ) : null} +
  • + ); +} diff --git a/webui/src/components/ReplaySourcesPanel/index.jsx b/webui/src/components/ReplaySourcesPanel/index.jsx index aefb34ba..60eed24b 100644 --- a/webui/src/components/ReplaySourcesPanel/index.jsx +++ b/webui/src/components/ReplaySourcesPanel/index.jsx @@ -134,7 +134,9 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei const grouped = useMemo(() => { return { rovers: sources.filter((source) => source.type === 'rover'), - rooms: sources.filter((source) => source.type === 'room'), + // PTZ is presented with room cameras because there is only one fixed room + // PTZ camera and it should not create a separate source category. + rooms: sources.filter((source) => source.type === 'room' || source.type === 'ptz'), }; }, [sources]); diff --git a/webui/src/components/RightPaneTabs/index.jsx b/webui/src/components/RightPaneTabs/index.jsx index 0e61cae8..a9439acc 100644 --- a/webui/src/components/RightPaneTabs/index.jsx +++ b/webui/src/components/RightPaneTabs/index.jsx @@ -9,6 +9,7 @@ import HelpPanel from '../HelpPanel/index.jsx'; import ChatPanel from '../ChatPanel/index.jsx'; import { LinkButtonsPanel } from '../UserListPanel/index.jsx'; import ReplaySourcesPanel from '../ReplaySourcesPanel/index.jsx'; +import PtzQueueCard from '../PtzCamera/index.jsx'; import Tabs, { Tab, TabList, TabPanel, TabPanels } from '../Tabs/index.jsx'; import TopDownMap from '../TopDownMap/index.jsx'; import DriveDockAction from '../DriveDockAction/index.jsx'; @@ -201,7 +202,10 @@ function QueueReplayLinksRow() {
    - +
    + + +
    ); } @@ -425,7 +429,7 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) { {/* VIP tab */} - + {/* help tab */} diff --git a/webui/src/components/RoomCameraPanel/index.jsx b/webui/src/components/RoomCameraPanel/index.jsx index 10a41cad..4ff6b3ab 100644 --- a/webui/src/components/RoomCameraPanel/index.jsx +++ b/webui/src/components/RoomCameraPanel/index.jsx @@ -5,6 +5,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { useSessionSelector } from '../../context/SessionContext.jsx'; import { useSettingsNamespace } from '../../settings/index.js'; import { useRoomCameraSnapshots } from '../../hooks/useRoomCameraSnapshots.js'; +import { PTZ_CAMERA_ID, usePtzCameraSnapshots } from '../../hooks/usePtzCameraSnapshot.js'; import RoomCameraFeed from '../RoomCameraFeed/index.jsx'; import CardFrame from '../CardFrame/index.jsx'; import { isFeatureEnabled } from '../../lib/features.js'; @@ -105,11 +106,14 @@ function useCameraPanelSubscriptionGate() { } export default function RoomCameraPanel(props) { - const enabled = useSessionSelector((state) => isFeatureEnabled(state, 'roomCameras')); + const enabled = useSessionSelector((state) => + isFeatureEnabled(state, 'roomCameras') || isFeatureEnabled(state, 'ptzCamera'), + ); /* - Room-camera visibility belongs with the room-camera panel. This keeps every - route free to mount the panel without duplicating the server feature rule. + Camera-panel visibility belongs with the panel. PTZ is included here because + the user-facing request is "show it as a room camera"; the rendering path + still uses the same RoomCameraFeed tile as ordinary room cameras. */ if (!enabled) return null; @@ -123,13 +127,35 @@ function RoomCameraPanelContent({ hideHeader = false, panelId = null, }) { + const roomCamerasEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'roomCameras')); + const ptzEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'ptzCamera')); + const ptz = useSessionSelector((state) => state.session?.ptzCamera || null); const cameras = useSessionSelector((state) => state.session?.roomCameras || []); + const cameraSources = useMemo(() => { + const base = roomCamerasEnabled ? cameras : []; + if (!ptzEnabled || !ptz) return base; + /* + PTZ snapshots use a different socket namespace from room cameras, but the + display model is intentionally the same: an id, a label, and a feed object. + Marking the type lets the subscription layer stay separate while the tile + renderer remains shared. + */ + return [ + ...base, + { + id: PTZ_CAMERA_ID, + name: ptz.name || 'PTZ Camera', + type: 'ptz', + }, + ]; + }, [cameras, ptz, ptzEnabled, roomCamerasEnabled]); const cameraIds = useMemo( - () => cameras.map((camera) => camera.id), - [cameras], + () => cameraSources.filter((camera) => camera.type !== 'ptz').map((camera) => camera.id), + [cameraSources], ); const { panelRef, isPanelVisible } = useCameraPanelSubscriptionGate(); const feedMap = useRoomCameraSnapshots(cameraIds, { enabled: isPanelVisible }); + const ptzFeeds = usePtzCameraSnapshots([PTZ_CAMERA_ID], { enabled: isPanelVisible && ptzEnabled }); const { value: orientationSettings, save: saveOrientationSettings } = useSettingsNamespace('roomCameraPanels', {}); const [orientation, setOrientation] = useState(() => normalizeOrientation( @@ -152,7 +178,7 @@ function RoomCameraPanelContent({ ); const containerClass = effectiveOrientation === 'vertical' ? 'flex flex-col gap-0.5' : 'grid gap-0.5 md:grid-cols-2'; - const showLayoutToggle = !hideLayoutToggle && !forcedOrientation && cameras.length > 0; + const showLayoutToggle = !hideLayoutToggle && !forcedOrientation && cameraSources.length > 0; const applyOrientation = (next) => { setOrientation(next); if (panelId) { @@ -160,7 +186,7 @@ function RoomCameraPanelContent({ } }; - if (cameras.length === 0) { + if (cameraSources.length === 0) { return (
    @@ -195,8 +221,8 @@ function RoomCameraPanelContent({ bodyClassName="space-y-0.5 text-base" >
    - {cameras.map((camera) => { - const feed = feedMap[camera.id] || null; + {cameraSources.map((camera) => { + const feed = camera.type === 'ptz' ? ptzFeeds[camera.id] || null : feedMap[camera.id] || null; return (
    {/*
    diff --git a/webui/src/components/RoverQueuesPanel/index.jsx b/webui/src/components/RoverQueuesPanel/index.jsx index c0528a1d..3d7fe074 100644 --- a/webui/src/components/RoverQueuesPanel/index.jsx +++ b/webui/src/components/RoverQueuesPanel/index.jsx @@ -5,17 +5,13 @@ import { useMemo, useState } from 'react'; import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx'; import { useSharedClock } from '../../hooks/useSharedClock.js'; import CardFrame from '../CardFrame/index.jsx'; -import RoverLabel from '../RoverLabel/index.jsx'; +import QueueTargetRow from '../QueueTargetRow/index.jsx'; import { trackAnalyticsEvent } from '../../analytics/index.js'; import { openExternalRover } from '../../lib/interInstanceTransfer.js'; import { ExternalInstancesCompact } from '../InterInstancePanel/index.jsx'; import { isFeatureEnabled } from '../../lib/features.js'; import { useSettingsNamespace } from '../../settings/index.js'; -function classNames(...values) { - return values.filter(Boolean).join(' '); -} - function formatBattery(rover) { const percent = rover?.batteryState?.percentDisplay; if (percent == null) return '--'; @@ -29,27 +25,6 @@ function batteryClass(rover) { return 'text-emerald-300'; } -function roleColors(role) { - switch (role) { - case 'admin': - case 'lockdown': - return 'text-amber-300'; - case 'spectator': - return 'text-slate-400'; - default: - return 'text-sky-300'; - } -} - -function formatLabel(user, selfId) { - if (!user) return ''; - const base = user.nickname || user.socketId?.slice(0, 6) || 'unknown'; - if (user.socketId && user.socketId === selfId) { - return `${base} (you)`; - } - return base; -} - export default function RoverQueuesPanel({ title = 'Rovers', roster: rosterOverride = null, @@ -192,129 +167,59 @@ export default function RoverQueuesPanel({ ) : (
      {rosterItems.map((rover) => { - const roverId = String(rover.id); - const info = turnQueues?.[roverId] || null; - const queue = info?.queue || []; - const deadline = info?.idleDeadline || info?.deadline || null; - const remainingSeconds = - deadline && deadline > now ? Math.ceil((deadline - now) / 1000) : deadline ? 0 : null; - const currentId = info?.current || null; - const currentIdx = currentId ? queue.findIndex((id) => id === currentId) : -1; - const nextId = - queue.length > 1 - ? currentIdx >= 0 - ? queue[(currentIdx + 1) % queue.length] - : queue[0] - : null; - const isSelfCurrent = Boolean(selfId && currentId && currentId === selfId); - const isSelfNext = Boolean(selfId && nextId && nextId === selfId); - const showTimer = remainingSeconds != null && (isSelfCurrent || isSelfNext); - const isPrivateOpen = Boolean(rover?.private?.enabled && rover?.private?.open); - const isGrantedClosedPrivate = Boolean(rover?.private?.enabled && !rover?.private?.open); - const locked = Boolean(rover.locked); - const lockedBlocked = locked && (externalMode || (!adminCapable && !isGrantedClosedPrivate)); - const lockLabel = rover.lockReason ? `locked: ${rover.lockReason}` : 'locked'; - const buttonLabel = pending[roverId] - ? '...' - : lockedBlocked - ? lockLabel - : externalMode - ? 'Open' - : 'request'; - const canClickRow = canRequest && !lockedBlocked && !pending[roverId]; - return ( -
    • { - if (!canClickRow) return; - handleRequest(rover.id); - }} - > - {externalMode && rover?.snapshots?.latestUrl ? ( - - ) : null} -
      -
      -
      -

      - - {rover.description ? ( - - {rover.description} - - ) : null} -

      - {showTimer ? ( - - {isSelfCurrent ? `${remainingSeconds}s left` : `Your turn in ${remainingSeconds}s`} - - ) : null} -
      - - {formatBattery(rover)} - -
      - {queue.length === 0 ? ( -

      No queue.

      - ) : ( -
      - {queue.map((socketId, idx) => { - const user = lookupUser(socketId); - const isCurrent = socketId === currentId; - const isNext = Boolean(nextId && socketId === nextId && !isCurrent); - const highlightClass = isCurrent - ? 'bg-sky-600 text-white ring-2 ring-amber-300' - : isNext - ? 'bg-emerald-700/60 text-emerald-100 ring-1 ring-emerald-300/70' - : 'bg-slate-800 text-slate-200'; - return ( - - - {formatLabel(user, selfId)} - - {isCurrent && now} - {isNext && next} - - ); - })} -
      - )} -
      - {canRequest ? ( - - ) : null} -
    • - ); + const roverId = String(rover.id); + const info = turnQueues?.[roverId] || null; + const queue = info?.queue || []; + const deadline = info?.idleDeadline || info?.deadline || null; + const remainingSeconds = + deadline && deadline > now ? Math.ceil((deadline - now) / 1000) : deadline ? 0 : null; + const currentId = info?.current || null; + const currentIdx = currentId ? queue.findIndex((id) => id === currentId) : -1; + const nextId = + queue.length > 1 + ? currentIdx >= 0 + ? queue[(currentIdx + 1) % queue.length] + : queue[0] + : null; + const isSelfCurrent = Boolean(selfId && currentId && currentId === selfId); + const isSelfNext = Boolean(selfId && nextId && nextId === selfId); + const showTimer = remainingSeconds != null && (isSelfCurrent || isSelfNext); + const isPrivateOpen = Boolean(rover?.private?.enabled && rover?.private?.open); + const isGrantedClosedPrivate = Boolean(rover?.private?.enabled && !rover?.private?.open); + const locked = Boolean(rover.locked); + const lockedBlocked = locked && (externalMode || (!adminCapable && !isGrantedClosedPrivate)); + const lockLabel = rover.lockReason ? `locked: ${rover.lockReason}` : 'locked'; + const buttonLabel = pending[roverId] + ? '...' + : lockedBlocked + ? lockLabel + : externalMode + ? 'Open' + : 'request'; + const canClickRow = canRequest && !lockedBlocked && !pending[roverId]; + return ( + + ); })}
    )} diff --git a/webui/src/components/UserListPanel/index.jsx b/webui/src/components/UserListPanel/index.jsx index 5e328c1f..67481acf 100644 --- a/webui/src/components/UserListPanel/index.jsx +++ b/webui/src/components/UserListPanel/index.jsx @@ -19,7 +19,7 @@ export function NicknameEntryPanel({ compact = false }) { ); } -export function LinkButtonsPanel({ className = '' }) { +export function LinkButtonsPanel({ className = '', fillHeight = true }) { const enabled = useSessionSelector((state) => isFeatureEnabled(state, 'socials')); /* @@ -32,7 +32,7 @@ export function LinkButtonsPanel({ className = '' }) { return ( diff --git a/webui/src/components/vip/VipPtzCameraCard.jsx b/webui/src/components/vip/VipPtzCameraCard.jsx new file mode 100644 index 00000000..e2a0dd89 --- /dev/null +++ b/webui/src/components/vip/VipPtzCameraCard.jsx @@ -0,0 +1,574 @@ +// Vip PTZ Camera Card +// Purpose: Provides the verified-user entry point and fullscreen controller for the single Reolink PTZ camera. +// Scope: Owns PTZ UI state only; server-side PTZ ownership, rover handoff, and command authorization remain authoritative. +import { useCallback, useMemo, useState } from 'react'; +import { createPortal } from 'react-dom'; +import ChatPanel from '../ChatPanel/index.jsx'; +import CardFrame from '../CardFrame/index.jsx'; +import GPIOToggleControl from '../GPIOToggleControl/index.jsx'; +import ControlPadPanel from '../MobileControls/ControlPadPanel.jsx'; +import PtzLiveVideo from '../PtzLiveVideo/index.jsx'; +import ReplaySourcesPanel from '../ReplaySourcesPanel/index.jsx'; +import KeyPill from './VipAudioUploadCard/KeyPill.jsx'; +import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx'; +import { useControlSelector } from '../../controls/index.js'; +import { formatKeyLabel } from '../../controls/keymapUtils.js'; +import { usePtzCameraSnapshots } from '../../hooks/usePtzCameraSnapshot.js'; +import { isFeatureEnabled } from '../../lib/features.js'; + +const PTZ_CAMERA_ID = 'ptz-camera'; +const PTZ_ZOOM_SPEED = 0.55; + +function formatRemaining(deadline) { + const remaining = Math.max(0, Math.ceil((Number(deadline || 0) - Date.now()) / 1000)); + if (!remaining) return '--'; + const minutes = Math.floor(remaining / 60); + const seconds = remaining % 60; + return `${minutes}:${String(seconds).padStart(2, '0')}`; +} + +function isSpotlightOn(light = {}) { + if (typeof light?.on === 'boolean') return light.on; + const raw = light?.state; + if (typeof raw === 'string') { + const normalized = raw.trim().toLowerCase(); + return !['', '0', 'off', 'false'].includes(normalized); + } + return Boolean(Number(raw)); +} + +function normalizeIrMode(mode) { + const normalized = String(mode || '').trim().toLowerCase(); + if (normalized === 'on') return 'On'; + if (normalized === 'off') return 'Off'; + return 'Auto'; +} + +function nextIrMode(currentMode) { + const current = normalizeIrMode(currentMode); + if (current === 'Auto') return 'On'; + if (current === 'On') return 'Off'; + return 'Auto'; +} + +function PtzSnapshotPreview({ feed, label = 'PTZ Camera' }) { + return ( +
    + {feed?.objectUrl ? ( + {label} + ) : ( +
    Waiting for snapshot...
    + )} + {/* + Even on this legacy VIP surface, keep the video pane itself identical + to rover media panes: no camera title inside the frame, only the small + top-left stream status. Any PTZ name/context belongs to the card chrome + around the media, not the media player. + */} +
    +
    + Status: {feed?.error ? `Error: ${feed.error}` : feed?.status || 'connecting'} +
    +
    +
    + ); +} + +function StatusRow({ label, value, tone = '' }) { + return ( +
    + {label} + {value} +
    + ); +} + +function formatPublisherProgress(progress = null) { + /* + ffmpeg progress is intentionally shown as raw operational numbers instead + of translated prose. When the PTZ video feels delayed, fps/speed/drop/out + time make it obvious whether ffmpeg itself is keeping up or the delay is + somewhere before/after the transcoder. + */ + if (!progress) return null; + return [ + progress.fps ? `fps ${progress.fps}` : null, + progress.speed ? `speed ${progress.speed}` : null, + progress.drop_frames ? `drop ${progress.drop_frames}` : null, + progress.out_time ? `out ${progress.out_time}` : null, + ].filter(Boolean).join(' | '); +} + +function PtzQueueList({ queue = [], operatorLabel = '' }) { + const hasQueue = Array.isArray(queue) && queue.length > 0; + return ( +
    +
    Turn queue
    +
    + {operatorLabel ? ( +
    + Now + {operatorLabel} +
    + ) : null} + {hasQueue ? queue.map((entry, index) => ( +
    + {index + 1} + {entry.label || entry.socketId || 'queued user'} +
    + )) : ( +
    No one waiting
    + )} +
    +
    + ); +} + +function PtzStatePanel({ ptz, onClose, onRelease, releaseDisabled = false }) { + const spotlightOn = isSpotlightOn(ptz?.light); + const irMode = normalizeIrMode(ptz?.ir?.state); + const publisher = ptz?.publisher || {}; + const publisherStatus = publisher.running + ? `running${publisher.pid ? ` ${publisher.pid}` : ''}` + : publisher.restartAt + ? 'restarting' + : publisher.lastEvent || 'stopped'; + const publisherProgress = formatPublisherProgress(publisher.progress); + const statusTone = ptz?.error ? 'text-amber-300' : ptz?.isOperator ? 'text-emerald-300' : 'text-slate-100'; + + return ( + Close : null} + bodyClassName="space-y-0.5 p-1 text-sm" + > + + + + + + + + {publisherProgress ? : null} + {publisher.lastStderr ? ( +
    + {publisher.lastStderr} +
    + ) : null} + {ptz?.blocked?.message ? ( +
    + {ptz.blocked.message} +
    + ) : null} + {onRelease ? ( + + ) : null} +
    + ); +} + +function PtzLightingControls({ ptz, disabled = false }) { + const { ptzSpotlight, ptzIr } = useSessionActions(); + const [busy, setBusy] = useState(''); + const spotlightOn = isSpotlightOn(ptz?.light); + const irMode = normalizeIrMode(ptz?.ir?.state); + + const toggleSpotlight = async (nextOn) => { + if (disabled) return; + setBusy('spotlight'); + try { + await ptzSpotlight({ state: nextOn ? 1 : 0 }); + } finally { + setBusy(''); + } + }; + + const cycleIr = async () => { + if (disabled) return; + setBusy('ir'); + try { + await ptzIr({ state: nextIrMode(irMode) }); + } finally { + setBusy(''); + } + }; + + return ( +
    + + +
    + ); +} + +function PtzMobileZoomButtons({ disabled = false }) { + const { ptzMove, ptzStop } = useSessionActions(); + const stopZoom = useCallback(() => { + ptzStop().catch(() => {}); + }, [ptzStop]); + const startZoom = useCallback( + (direction) => (event) => { + /* + Mobile needs explicit zoom targets because the regular mobile drive pad + is already used for pan/tilt. Desktop does not render these buttons; it + uses the mapped camera up/down controls shown in the reference panel. + */ + event.preventDefault(); + if (disabled) return; + ptzMove({ pan: 0, tilt: 0, zoom: direction * PTZ_ZOOM_SPEED }).catch(() => {}); + }, + [disabled, ptzMove], + ); + const stopFromPointer = useCallback( + (event) => { + event?.preventDefault?.(); + if (disabled) return; + stopZoom(); + }, + [disabled, stopZoom], + ); + + return ( +
    + + +
    + ); +} + +function PtzMobileControlsPanel({ ptz, disabled = false }) { + return ( +
    + +
    + {/* + Reuse the rover mobile movement card instead of building a second PTZ + joystick. Its drive vector goes through the shared internal control + layer, where the PTZ adapter already converts movement plus speed mode + into camera pan/tilt commands. + */} + +
    + +
    + ); +} + +function keyLabelFor(keymap, actionId) { + return formatKeyLabel(keymap?.[actionId]?.[0]); +} + +function PtzControlReference() { + const keymap = useControlSelector((control) => control.state.keymap); + const rows = [ + ['Tilt up', 'driveForward'], + ['Tilt down', 'driveBackward'], + ['Pan left', 'driveLeft'], + ['Pan right', 'driveRight'], + ['Zoom in', 'cameraUp'], + ['Zoom out', 'cameraDown'], + ['Spotlight', 'headlightToggle'], + ['Infrared mode', 'laserToggle'], + ]; + + return ( + + {rows.map(([label, actionId]) => ( +
    + {label} + {/* Use the same key display component as the rest of the UI so PTZ + controls read as normal mapped controls instead of custom labels. */} + +
    + ))} +
    + ); +} + +function PtzController({ open, onClose, layout = 'desktop' }) { + const ptz = useSessionSelector((state) => state.session?.ptzCamera || null); + const isOperator = Boolean(ptz?.isOperator); + const isMobile = layout === 'mobile-portrait' || layout === 'mobile-landscape'; + const { ptzRelease } = useSessionActions(); + const snapshotFeeds = usePtzCameraSnapshots([PTZ_CAMERA_ID], { enabled: open && !isOperator }); + const snapshot = snapshotFeeds[PTZ_CAMERA_ID] || null; + const [releasePending, setReleasePending] = useState(false); + + if (!open) return null; + + const releaseAndClose = async () => { + setReleasePending(true); + try { + await ptzRelease(); + onClose(); + } finally { + setReleasePending(false); + } + }; + + const desktopSidebar = ( + <> + +
    + +
    + {isOperator ? ( +
    + +
    + ) : ( +
    + + Live PTZ controls unlock when your camera turn is active. + +
    + )} +
    + +
    +
    + {/* + The global chat key focuses the input registered by ChatPanel through + ChatContext. Keeping a real ChatPanel mounted inside the PTZ fullscreen + sidebar lets the normal keyboard path focus chat and lets Enter submit + through the existing chat composer form instead of adding PTZ-specific + chat handling. + */} + +
    +
    + +
    +
    + +
    + + ); + + const mobileSidebar = ( + <> +
    + +
    +
    + {/* + Mobile uses the same ChatPanel registration as desktop so the mapped + chat key and the on-screen input stay on one shared chat implementation. + */} + +
    +
    + +
    +
    + +
    +
    + +
    + + ); + + const sidebarWidthClass = isMobile + ? 'grid-cols-[minmax(0,1fr)_14rem]' + : 'grid-cols-[minmax(0,1fr)_20rem]'; + + const controller = ( +
    + +
    + {isOperator ? ( + + ) : ( + + )} +
    + +
    +
    + ); + + /* + The controller is a true fullscreen surface, so mount it directly under + document.body instead of inside the VIP tab/card tree. That keeps tab panel + spacing, mobile banners, and parent overflow rules from creating visible + gaps around a fixed-position camera interface. + */ + return createPortal(controller, document.body); +} + +export default function VipPtzCameraCard({ onMessage, fullWidth = false, layout = 'desktop' }) { + const featureEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'ptzCamera')); + const ptz = useSessionSelector((state) => state.session?.ptzCamera || null); + const isVerified = useSessionSelector((state) => Boolean(state.session?.isVerified)); + const { ptzClaim, ptzRelease } = useSessionActions(); + const snapshotFeeds = usePtzCameraSnapshots([PTZ_CAMERA_ID], { enabled: Boolean(featureEnabled) }); + const snapshot = snapshotFeeds[PTZ_CAMERA_ID] || null; + const [controllerOpen, setControllerOpen] = useState(false); + const [pending, setPending] = useState(false); + + const wrapClass = fullWidth ? 'w-full' : 'mx-auto w-full max-w-xl'; + const queueText = useMemo(() => { + if (ptz?.isOperator) return 'Your turn'; + if (ptz?.queuedPosition) return `Queue position ${ptz.queuedPosition}`; + if (ptz?.operatorLabel) return `${ptz.operatorLabel} operating`; + return 'Available'; + }, [ptz?.isOperator, ptz?.operatorLabel, ptz?.queuedPosition]); + + if (!featureEnabled) return null; + + const handleClaim = async () => { + setPending(true); + onMessage?.(''); + try { + const response = await ptzClaim(); + /* + Requesting the PTZ camera is only enough to open fullscreen after the + server confirms the user actually became the operator or entered the + queue. Dock-required failures reject before queue entry, so they must + leave the user on this card with the dock/charge message visible. + */ + if (response?.state?.isOperator || response?.state?.queuedPosition) setControllerOpen(true); + if (response?.state?.isOperator) onMessage?.('PTZ camera turn active.'); + else if (response?.state?.queuedPosition) onMessage?.(`Joined PTZ queue at position ${response.state.queuedPosition}.`); + } catch (err) { + /* + Rover-to-rover switch denial uses the browser alert popup for the + dock-and-charge message. PTZ claim denial should feel identical because + it is enforcing the same "do not abandon an undocked rover" rule. + */ + alert(err.message || 'PTZ request failed.'); + onMessage?.(err.message || 'PTZ request failed.'); + } finally { + setPending(false); + } + }; + + const handleRelease = async () => { + setPending(true); + try { + await ptzRelease(); + onMessage?.('Left PTZ camera.'); + } catch (err) { + onMessage?.(err.message || 'Failed to leave PTZ camera.'); + } finally { + setPending(false); + } + }; + + return ( + <> + +
    + +
    +
    +
    +

    State

    +

    {queueText}

    +
    +
    +

    Remaining

    +

    {formatRemaining(ptz?.deadline)}

    +
    +
    +

    Spotlight

    +

    {isSpotlightOn(ptz?.light) ? 'On' : 'Off'}

    +
    +
    +

    Infrared

    +

    {normalizeIrMode(ptz?.ir?.state)}

    +
    +
    + + {ptz?.blocked?.message ? ( +

    + {ptz.blocked.message} +

    + ) : null} +
    + {ptz?.isOperator ? ( + <> + + + + ) : ( + + )} +
    +
    +
    +
    + setControllerOpen(false)} layout={layout} /> + + ); +} diff --git a/webui/src/context/ChatContext.jsx b/webui/src/context/ChatContext.jsx index 46e3024e..6ff1b6cf 100644 --- a/webui/src/context/ChatContext.jsx +++ b/webui/src/context/ChatContext.jsx @@ -61,6 +61,7 @@ export function ChatProvider({ children }) { const [isChatFocused, setIsChatFocused] = useState(false); const panelInputRef = useRef(null); const hudInputRef = useRef(null); + const overlayInputRef = useRef(null); const audioRef = useRef(null); const typingRef = useRef(new Map()); const typingAlertRef = useRef(new Map()); @@ -236,7 +237,17 @@ export function ChatProvider({ children }) { ); const registerInputRef = useCallback((el, options = {}) => { - const target = options?.target === 'hud' ? 'hud' : 'panel'; + const target = options?.target === 'overlay' ? 'overlay' : options?.target === 'hud' ? 'hud' : 'panel'; + /* + Fullscreen surfaces such as PTZ need chat-key focus while they are mounted + without stealing the normal HUD ref permanently. Keep overlay chat as a + separate highest-priority slot so unmounting the portal only clears the + overlay target and leaves the driver HUD/panel refs intact. + */ + if (target === 'overlay') { + overlayInputRef.current = el; + return; + } if (target === 'hud') { hudInputRef.current = el; } else { @@ -246,11 +257,12 @@ export function ChatProvider({ children }) { const focusChat = useCallback(() => { setIsChatFocused(true); - (hudInputRef.current || panelInputRef.current)?.focus(); + (overlayInputRef.current || hudInputRef.current || panelInputRef.current)?.focus(); }, []); const blurChat = useCallback(() => { setIsChatFocused(false); + overlayInputRef.current?.blur(); hudInputRef.current?.blur(); panelInputRef.current?.blur(); }, []); diff --git a/webui/src/context/SessionContext.jsx b/webui/src/context/SessionContext.jsx index c4821484..9c462ea1 100644 --- a/webui/src/context/SessionContext.jsx +++ b/webui/src/context/SessionContext.jsx @@ -420,6 +420,16 @@ export function SessionProvider({ children }) { setAudioLevels: (levels = {}) => emitWithAck('audioLevels:set', levels), setPrivateSafety: (roverId, safety = {}) => emitWithAck('session:privateSafety:set', { roverId, safety }), + ptzClaim: () => emitWithAck('ptzCamera:claim'), + ptzRelease: () => emitWithAck('ptzCamera:release'), + ptzMove: (payload = {}) => emitWithAck('ptzCamera:move', payload), + ptzStop: () => emitWithAck('ptzCamera:stop'), + ptzSpotlight: (payload = {}) => emitWithAck('ptzCamera:spotlight', payload), + ptzIr: (payload = {}) => emitWithAck('ptzCamera:ir', payload), + ptzListPresets: () => emitWithAck('ptzCamera:presets:list'), + ptzGotoPreset: (payload = {}) => emitWithAck('ptzCamera:preset:goto', payload), + ptzCreatePreset: (payload = {}) => emitWithAck('ptzCamera:preset:create', payload), + ptzRemovePreset: (payload = {}) => emitWithAck('ptzCamera:preset:remove', payload), llmControl: (action, controls = {}) => emitWithAck('llm:control', { controls: { action, ...controls } }), overseerControl: (action, controls = {}) => diff --git a/webui/src/controls/ControlContext.jsx b/webui/src/controls/ControlContext.jsx index 0d3f32e3..d6ac50ca 100644 --- a/webui/src/controls/ControlContext.jsx +++ b/webui/src/controls/ControlContext.jsx @@ -35,6 +35,7 @@ import { applyDriveOvercurrentScale, useOvercurrentLimiter, } from './overcurrentLimiter.js'; +import { usePtzControlAdapter } from './ptzControlAdapter.js'; const ControlSystemContext = createContext(null); @@ -179,6 +180,7 @@ export function ControlSystemProvider({ children }) { [overcurrentLimiter.adminImmune, overcurrentLimiter.scales], ); const pipeline = useCommandPipeline({ driveTransform, auxTransform }); + const ptzControls = usePtzControlAdapter(); const turnOnAllLights = useCallback(() => { const entities = homeAssistantEntities || []; @@ -327,9 +329,10 @@ export function ControlSystemProvider({ children }) { payload: { ...computed, source: meta.source ?? null }, }); recordControlIntent(); + if (ptzControls.applyDriveVector(vector, meta)) return; pipeline.sendDriveDirect(computed.speeds); }, - [pipeline, recordControlIntent, state.manualDockAssist?.active], + [pipeline, ptzControls, recordControlIntent, state.manualDockAssist?.active], ); const setAuxMotors = useCallback( @@ -372,6 +375,21 @@ export function ControlSystemProvider({ children }) { const setServoAngle = useCallback( (value, options = {}) => { + if (ptzControls.isActive) { + /* + Servo-capable rover controls converge here from keyboard, gamepad, + desktop, and mobile. When the active control target is the PTZ camera, + route the intent through the PTZ adapter instead of making the rover + command pipeline understand camera zoom semantics. + */ + const baseline = typeof servoAngleRef.current === 'number' ? servoAngleRef.current : 0; + const numeric = Number(value); + if (!Number.isFinite(numeric)) return; + ptzControls.pulseZoom(numeric - baseline); + servoAngleRef.current = numeric; + recordControlIntent(); + return; + } if (!pipeline.servoConfig) return; const force = Boolean(options?.force); if (state.manualDockAssist?.active && !force) return; @@ -381,23 +399,23 @@ export function ControlSystemProvider({ children }) { servoAngleRef.current = clamped; recordControlIntent(); }, - [pipeline, recordControlIntent, state.manualDockAssist?.active], + [pipeline, ptzControls, recordControlIntent, state.manualDockAssist?.active], ); const nudgeServo = useCallback( (delta = 0) => { const config = pipeline.servoConfig; - if (!config) return; - const step = typeof delta === 'number' && delta !== 0 ? delta : config.nudgeDegrees || 1; + if (!config && !ptzControls.isActive) return; + const step = typeof delta === 'number' && delta !== 0 ? delta : config?.nudgeDegrees || 1; const baseline = typeof servoAngleRef.current === 'number' ? servoAngleRef.current - : typeof config.homeAngle === 'number' + : typeof config?.homeAngle === 'number' ? config.homeAngle : 0; setServoAngle(baseline + step); }, - [pipeline.servoConfig, setServoAngle], + [pipeline.servoConfig, ptzControls.isActive, setServoAngle], ); const goServoHome = useCallback(() => { @@ -460,9 +478,13 @@ export function ControlSystemProvider({ children }) { source: 'system-stop', }, }); + if (ptzControls.isActive) { + ptzControls.stopMotion(); + return; + } pipeline.sendDriveDirect({ left: 0, right: 0 }); pipeline.sendAuxMotors({ main: 0, side: 0, vacuum: 0 }); - }, [pipeline]); + }, [pipeline, ptzControls]); const sendOiCommand = useCallback( (command) => { @@ -484,6 +506,10 @@ export function ControlSystemProvider({ children }) { const setHeadlight = useCallback( (headlightOn) => { + if (ptzControls.setSpotlight(headlightOn)) { + recordControlIntent(); + return; + } if (!pipeline.headlight) return; // Web controls now speak in logical device state. Any electrical // inversion needed by the actual GPIO driver is handled by roverd's @@ -492,7 +518,7 @@ export function ControlSystemProvider({ children }) { pipeline.sendHeadlight(action); recordControlIntent(); }, - [pipeline, recordControlIntent], + [pipeline, ptzControls, recordControlIntent], ); const toggleHeadlight = useCallback(() => { @@ -501,6 +527,10 @@ export function ControlSystemProvider({ children }) { const setLaser = useCallback( (laserOn) => { + if (ptzControls.setIr(laserOn)) { + recordControlIntent(); + return; + } if (!pipeline.laser) return; if (roomLightsLockedOn && laserOn !== false) return; // The laser shares the same logical toggle contract as the headlight; it @@ -509,7 +539,7 @@ export function ControlSystemProvider({ children }) { pipeline.sendLaser(action); recordControlIntent(); }, - [pipeline, recordControlIntent, roomLightsLockedOn], + [pipeline, ptzControls, recordControlIntent, roomLightsLockedOn], ); const toggleLaser = useCallback(() => { @@ -739,10 +769,11 @@ export function ControlSystemProvider({ children }) { state, dispatch, pipeline, + ptzControls, overcurrentLimiter, actions: stableActions, }), - [state, pipeline, overcurrentLimiter, stableActions], + [state, pipeline, ptzControls, overcurrentLimiter, stableActions], ); if (snapshotRef.current == null) { diff --git a/webui/src/controls/commandPipeline.js b/webui/src/controls/commandPipeline.js index d235b85e..6bf86f39 100644 --- a/webui/src/controls/commandPipeline.js +++ b/webui/src/controls/commandPipeline.js @@ -46,7 +46,6 @@ export function useCommandPipeline(options = {}) { const headlightState = useMemo(() => rosterEntry?.headlight?.state ?? null, [rosterEntry]); const laserState = useMemo(() => rosterEntry?.laser?.state ?? null, [rosterEntry]); - const emitCommand = useCallback( (payload, cb) => { if (!roverId) return; @@ -65,7 +64,6 @@ export function useCommandPipeline(options = {}) { const sendDriveDirect = useCallback( (speeds) => { - if (!roverId) return null; const rawPayload = { left: clampRange(speeds?.left ?? 0, [-500, 500]), right: clampRange(speeds?.right ?? 0, [-500, 500]), @@ -75,6 +73,7 @@ export function useCommandPipeline(options = {}) { left: clampRange(transformed?.left ?? 0, [-500, 500]), right: clampRange(transformed?.right ?? 0, [-500, 500]), }; + if (!roverId) return null; emitCommand({ type: 'drive', data: { driveDirect: payload }, diff --git a/webui/src/controls/overcurrentLimiter.js b/webui/src/controls/overcurrentLimiter.js index dafa7df5..7386a63c 100644 --- a/webui/src/controls/overcurrentLimiter.js +++ b/webui/src/controls/overcurrentLimiter.js @@ -12,8 +12,8 @@ export const OVERCURRENT_GROUPS = [ export const DEFAULT_OVERCURRENT_LIMITS = { downRatePerSec: 0.5, - upRatePerSec: 0.5, - releaseDelaySec: 2, + upRatePerSec: 0.7, + releaseDelaySec: 1, outputRateMs: 250, }; diff --git a/webui/src/controls/ptzControlAdapter.js b/webui/src/controls/ptzControlAdapter.js new file mode 100644 index 00000000..7bf0596a --- /dev/null +++ b/webui/src/controls/ptzControlAdapter.js @@ -0,0 +1,236 @@ +// PTZ Control Adapter +// Purpose: Hooks the single PTZ camera into the internal control action layer. +// Scope: Owns PTZ-specific control mixing and socket commands so keyboard, +// mobile, desktop, and gamepad inputs do not each learn camera-specific rules. +import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { useSocket } from '../context/SocketContext.jsx'; +import { useSessionSelector } from '../context/SessionContext.jsx'; + +const PTZ_STOP = { pan: 0, tilt: 0, zoom: 0 }; +const PTZ_SPEEDS = { + slow: 0.1, + medium: 0.5, + fast: 1, +}; +const ZOOM_PULSE_MS = 220; + +function clampUnit(value) { + const number = Number(value) || 0; + return Math.max(-1, Math.min(1, number)); +} + +function axisSign(value) { + const number = Number(value) || 0; + if (number > 0.05) return 1; + if (number < -0.05) return -1; + return 0; +} + +function pickPanTiltSpeed(vector = {}, meta = {}) { + /* + PTZ movement should not inherit rover wheel speeds. Inputs only choose a + speed tier here: precision/low-magnitude input is slow, normal input is + medium, and explicit boost is fast. That gives every control surface the + same camera feel without remixing differential-drive output. + */ + if (vector?.boost) return PTZ_SPEEDS.fast; + const maxAxis = Math.max(Math.abs(Number(vector?.x) || 0), Math.abs(Number(vector?.y) || 0)); + const baseSpeed = Number(meta?.speedOptions?.baseSpeed); + if (maxAxis > 0 && maxAxis <= 0.45) return PTZ_SPEEDS.slow; + if (Number.isFinite(baseSpeed) && baseSpeed > 0 && baseSpeed <= 150) return PTZ_SPEEDS.slow; + return PTZ_SPEEDS.medium; +} + +function payloadSignature(payload = PTZ_STOP) { + return [ + Number(payload.pan || 0).toFixed(3), + Number(payload.tilt || 0).toFixed(3), + Number(payload.zoom || 0).toFixed(3), + ].join(':'); +} + +function isIdlePayload(payload = PTZ_STOP) { + return !payload.pan && !payload.tilt && !payload.zoom; +} + +function buildPanTiltPayload(vector = {}, meta = {}) { + const panSign = axisSign(vector.x); + const tiltSign = axisSign(vector.y); + if (!panSign && !tiltSign) return PTZ_STOP; + + const speed = pickPanTiltSpeed(vector, meta); + const diagonalScale = panSign && tiltSign ? Math.SQRT1_2 : 1; + return { + pan: clampUnit(panSign * speed * diagonalScale), + tilt: clampUnit(tiltSign * speed * diagonalScale), + zoom: 0, + }; +} + +function isSpotlightOn(light = {}) { + if (typeof light?.on === 'boolean') return light.on; + const raw = light?.state; + if (typeof raw === 'string') { + const normalized = raw.trim().toLowerCase(); + return !['', '0', 'off', 'false'].includes(normalized); + } + return Boolean(Number(raw)); +} + +function normalizeIrMode(mode) { + const normalized = String(mode || '').trim().toLowerCase(); + if (normalized === 'on') return 'On'; + if (normalized === 'off') return 'Off'; + return 'Auto'; +} + +function nextIrMode(currentMode) { + /* + The rover laser button becomes the PTZ camera's IR mode control while the + PTZ camera is active. Cycle all three modes exposed by this Reolink camera + instead of reducing the control to a two-state toggle. + */ + const current = normalizeIrMode(currentMode); + if (current === 'Auto') return 'On'; + if (current === 'On') return 'Off'; + return 'Auto'; +} + +export function usePtzControlAdapter() { + const socket = useSocket(); + const ptz = useSessionSelector((state) => state.session?.ptzCamera || null); + const isActive = Boolean(ptz?.isOperator); + const lastMotionSignatureRef = useRef(payloadSignature(PTZ_STOP)); + const zoomStopTimerRef = useRef(null); + + const emitPtz = useCallback( + (eventName, payload = {}) => { + if (!socket || !isActive) return; + socket.emit(eventName, payload); + }, + [isActive, socket], + ); + + const stopMotion = useCallback(() => { + if (zoomStopTimerRef.current) { + clearTimeout(zoomStopTimerRef.current); + zoomStopTimerRef.current = null; + } + const stopSignature = payloadSignature(PTZ_STOP); + if (lastMotionSignatureRef.current === stopSignature) return; + lastMotionSignatureRef.current = stopSignature; + emitPtz('ptzCamera:stop'); + }, [emitPtz]); + + const sendMotion = useCallback( + (payload, options = {}) => { + if (!isActive) return false; + const nextPayload = { + pan: clampUnit(payload?.pan), + tilt: clampUnit(payload?.tilt), + zoom: clampUnit(payload?.zoom), + }; + const nextSignature = payloadSignature(nextPayload); + if (!options.force && lastMotionSignatureRef.current === nextSignature) return true; + lastMotionSignatureRef.current = nextSignature; + if (isIdlePayload(nextPayload)) { + emitPtz('ptzCamera:stop'); + } else { + emitPtz('ptzCamera:move', nextPayload); + } + return true; + }, + [emitPtz, isActive], + ); + + const applyDriveVector = useCallback( + (vector, meta = {}) => { + if (!isActive) return false; + sendMotion(buildPanTiltPayload(vector, meta)); + return true; + }, + [isActive, sendMotion], + ); + + const pulseZoom = useCallback( + (direction) => { + if (!isActive) return false; + const sign = axisSign(direction); + if (!sign) { + stopMotion(); + return true; + } + /* + Zoom is different from pan/tilt because it is driven by repeated nudge + events from existing camera controls. Force each pulse through even when + the payload is identical, otherwise holding "camera up" only sends the + first zoom command and every later nudge is de-duped away. + */ + sendMotion({ pan: 0, tilt: 0, zoom: sign * PTZ_SPEEDS.medium }, { force: true }); + if (zoomStopTimerRef.current) clearTimeout(zoomStopTimerRef.current); + /* + Existing rover camera controls are nudge/slider based, not hold-based. + Treat each nudge as a short PTZ zoom pulse, then stop from the adapter + so delayed stop behavior is owned by the camera layer only. + */ + zoomStopTimerRef.current = setTimeout(() => { + zoomStopTimerRef.current = null; + stopMotion(); + }, ZOOM_PULSE_MS); + return true; + }, + [isActive, sendMotion, stopMotion], + ); + + const setSpotlight = useCallback( + (nextOn) => { + if (!isActive) return false; + const desiredOn = typeof nextOn === 'boolean' ? nextOn : !isSpotlightOn(ptz?.light); + emitPtz('ptzCamera:spotlight', { state: desiredOn ? 1 : 0 }); + return true; + }, + [emitPtz, isActive, ptz?.light], + ); + + const setIr = useCallback( + (nextOn) => { + if (!isActive) return false; + const desiredState = typeof nextOn === 'boolean' + ? (nextOn ? 'On' : 'Off') + : nextIrMode(ptz?.ir?.state); + emitPtz('ptzCamera:ir', { state: desiredState }); + return true; + }, + [emitPtz, isActive, ptz?.ir?.state], + ); + + useEffect(() => { + if (isActive) return undefined; + lastMotionSignatureRef.current = payloadSignature(PTZ_STOP); + if (zoomStopTimerRef.current) { + clearTimeout(zoomStopTimerRef.current); + zoomStopTimerRef.current = null; + } + return undefined; + }, [isActive]); + + useEffect( + () => () => { + if (zoomStopTimerRef.current) clearTimeout(zoomStopTimerRef.current); + }, + [], + ); + + return useMemo( + () => ({ + isActive, + state: ptz, + applyDriveVector, + pulseZoom, + setSpotlight, + setIr, + stopMotion, + }), + [applyDriveVector, isActive, ptz, pulseZoom, setIr, setSpotlight, stopMotion], + ); +} diff --git a/webui/src/hooks/usePtzCameraSnapshot.js b/webui/src/hooks/usePtzCameraSnapshot.js new file mode 100644 index 00000000..ed64b03a --- /dev/null +++ b/webui/src/hooks/usePtzCameraSnapshot.js @@ -0,0 +1,136 @@ +// Hook: usePtzCameraSnapshots +// Purpose: Subscribes to PTZ snapshot streams using the same map-shaped contract as rover snapshots. +// Scope: Keeps PTZ snapshot lifecycle boring: callers pass source ids and receive feeds keyed by id. +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useSocket } from '../context/SocketContext.jsx'; + +export const PTZ_CAMERA_ID = 'ptz-camera'; + +export function usePtzCameraSnapshots(sourceList = [], options = {}) { + const socket = useSocket(); + const { enabled = true, version = null } = options; + const [feeds, setFeeds] = useState({}); + const objectUrls = useRef(new Map()); + const ids = useMemo( + () => sourceList.map((entry) => (typeof entry === 'string' ? entry : entry?.id)).filter(Boolean), + [sourceList], + ); + const idsKey = useMemo(() => { + const base = ids.join('|'); + return version ? `${base}|v:${version}` : base; + }, [ids, version]); + const idsRef = useRef([]); + const [connectionNonce, setConnectionNonce] = useState(0); + const statsRef = useRef(new Map()); + const debugSnapshots = + typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debugSnapshots'); + + useEffect(() => { + if (!socket) return undefined; + const handleConnect = () => setConnectionNonce((prev) => prev + 1); + socket.on('connect', handleConnect); + return () => socket.off('connect', handleConnect); + }, [socket]); + + useEffect(() => { + idsRef.current = ids; + }, [idsKey, ids]); + + useEffect(() => { + objectUrls.current.forEach((url) => URL.revokeObjectURL(url)); + objectUrls.current.clear(); + setFeeds({}); + }, [idsKey]); + + useEffect(() => { + if (!enabled) { + objectUrls.current.forEach((url) => URL.revokeObjectURL(url)); + objectUrls.current.clear(); + setFeeds({}); + return undefined; + } + if (!idsRef.current.length || !socket) return undefined; + let cancelled = false; + const currentIds = idsRef.current; + + const handleFrame = (meta = {}, buffer) => { + if (cancelled || !meta.id || !buffer) return; + const sizeBytes = buffer.byteLength ?? buffer.length ?? 0; + const now = Date.now(); + const prevStats = statsRef.current.get(meta.id) || { + count: 0, + totalBytes: 0, + lastLogAt: 0, + }; + const nextStats = { + count: prevStats.count + 1, + totalBytes: prevStats.totalBytes + sizeBytes, + lastLogAt: prevStats.lastLogAt, + }; + if (debugSnapshots && (!nextStats.lastLogAt || now - nextStats.lastLogAt >= 10000)) { + const avgBytes = nextStats.count ? nextStats.totalBytes / nextStats.count : 0; + console.log( + '[ptzSnapshot]', + meta.id, + `frame=${sizeBytes}B`, + `avg=${Math.round(avgBytes)}B`, + `count=${nextStats.count}`, + ); + nextStats.lastLogAt = now; + } + statsRef.current.set(meta.id, nextStats); + const url = URL.createObjectURL(new Blob([buffer], { type: 'image/jpeg' })); + const prevUrl = objectUrls.current.get(meta.id); + if (prevUrl) URL.revokeObjectURL(prevUrl); + objectUrls.current.set(meta.id, url); + setFeeds((prev) => ({ + ...prev, + [meta.id]: { + status: 'playing', + ts: meta.ts || Date.now(), + error: null, + objectUrl: url, + }, + })); + }; + + const handleStatus = (meta = {}) => { + if (cancelled || !meta.id) return; + setFeeds((prev) => ({ + ...prev, + [meta.id]: { + ...(prev[meta.id] || {}), + status: meta.error ? 'error' : prev[meta.id]?.status || 'connecting', + error: meta.error || null, + ts: meta.ts || prev[meta.id]?.ts || null, + objectUrl: prev[meta.id]?.objectUrl || null, + }, + })); + }; + + socket.on('ptzCamera:snapshotFrame', handleFrame); + socket.on('ptzCamera:snapshotStatus', handleStatus); + socket.emit('ptzCamera:snapshotSubscribe', { ids: currentIds }, () => {}); + + return () => { + cancelled = true; + socket.emit('ptzCamera:snapshotUnsubscribe', { ids: currentIds }); + socket.off('ptzCamera:snapshotFrame', handleFrame); + socket.off('ptzCamera:snapshotStatus', handleStatus); + objectUrls.current.forEach((url) => URL.revokeObjectURL(url)); + objectUrls.current.clear(); + }; + }, [socket, idsKey, enabled, connectionNonce, debugSnapshots]); + + return feeds; +} + +export function usePtzCameraSnapshot(options = {}) { + /* + This wrapper keeps older callers working while new PTZ UI uses the same + keyed feed shape as useRoverSnapshots(). It should stay tiny so the real + subscription behavior only has one implementation to maintain. + */ + const feeds = usePtzCameraSnapshots([PTZ_CAMERA_ID], options); + return feeds[PTZ_CAMERA_ID] || null; +} diff --git a/webui/src/hooks/useVideoRequests.js b/webui/src/hooks/useVideoRequests.js index fa2f2ac3..cb257dd7 100644 --- a/webui/src/hooks/useVideoRequests.js +++ b/webui/src/hooks/useVideoRequests.js @@ -88,7 +88,12 @@ export function useVideoRequests(sourceList = [], options = {}) { let cancelled = false; function requestEntry(entry) { - const payload = entry.type === 'room' ? { roomCameraId: entry.id } : { roverId: entry.id }; + const payload = + entry.type === 'room' + ? { roomCameraId: entry.id } + : entry.type === 'ptz' + ? { type: 'ptz', id: entry.id } + : { roverId: entry.id }; socket.emit('video:request', payload, (resp = {}) => { if (cancelled) return; if (resp?.error) { diff --git a/webui/src/lib/whepPlayer.js b/webui/src/lib/whepPlayer.js index c45d1943..6e0fc416 100644 --- a/webui/src/lib/whepPlayer.js +++ b/webui/src/lib/whepPlayer.js @@ -29,12 +29,19 @@ function buildAuthHeader(token) { } export class WhepPlayer { - constructor({ url, token, video, onStatus, audioOnly = false, receiveAudio = true }) { + constructor({ url, token, video, onStatus, audioOnly = false, receiveAudio = true, startMuted = null }) { this.url = url; this.token = token; this.video = video; this.audioOnly = audioOnly; this.receiveAudio = receiveAudio; + /* + Browser autoplay usually requires normal video streams to start muted, + while audio-only streams need to start audible. Keep that historical + default, but allow a caller that is already behind a user gesture, such as + the PTZ fullscreen controller, to request audible inline audio. + */ + this.startMuted = startMuted === null ? !audioOnly : Boolean(startMuted); this.pc = null; this.abortController = null; this.onStatus = onStatus; @@ -50,7 +57,7 @@ export class WhepPlayer { if (!this.video) return; this.video.playsInline = true; this.video.autoplay = true; - this.video.muted = this.audioOnly ? false : true; + this.video.muted = this.startMuted; if (typeof this.video.disableRemotePlayback !== 'undefined') { this.video.disableRemotePlayback = true; } @@ -113,7 +120,15 @@ export class WhepPlayer { signal: this.abortController.signal, }); if (!response.ok) { - throw new Error(`WHEP request failed: ${response.status}`); + /* + MediaMTX includes the useful rejection reason in the response body + for many 4xx WHEP failures, such as unsupported codecs or malformed + SDP. Surface that body so camera/video debugging does not stop at a + generic HTTP status code. + */ + const body = await response.text().catch(() => ''); + const detail = body ? `: ${body.slice(0, 180)}` : ''; + throw new Error(`WHEP request failed: ${response.status}${detail}`); } const answerSdp = await response.text(); await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp }); diff --git a/webui/src/spectate/SpectatorApp/components/PtzSpectatorCard.jsx b/webui/src/spectate/SpectatorApp/components/PtzSpectatorCard.jsx new file mode 100644 index 00000000..d1ef7c5b --- /dev/null +++ b/webui/src/spectate/SpectatorApp/components/PtzSpectatorCard.jsx @@ -0,0 +1,127 @@ +// PTZ Spectator Card +// Purpose: Adds the single room PTZ camera to the spectator rover grid. +// Scope: Uses live WHEP only when the server authorizes this socket; otherwise +// falls back to the PTZ snapshot feed that remote spectators are allowed to see. +import PtzLiveVideo from '../../../components/PtzLiveVideo/index.jsx'; +import { useSessionSelector } from '../../../context/SessionContext.jsx'; +import { PTZ_CAMERA_ID, usePtzCameraSnapshots } from '../../../hooks/usePtzCameraSnapshot.js'; + +function formatRemaining(deadline) { + const remaining = Math.max(0, Math.ceil((Number(deadline || 0) - Date.now()) / 1000)); + if (!remaining) return '--'; + const minutes = Math.floor(remaining / 60); + const seconds = remaining % 60; + return `${minutes}:${String(seconds).padStart(2, '0')}`; +} + +function isSpotlightOn(light = {}) { + if (typeof light?.on === 'boolean') return light.on; + const raw = light?.state; + if (typeof raw === 'string') { + const normalized = raw.trim().toLowerCase(); + return !['', '0', 'off', 'false'].includes(normalized); + } + return Boolean(Number(raw)); +} + +function normalizeInfraredMode(mode) { + const normalized = String(mode || '').trim().toLowerCase(); + if (normalized === 'on') return 'On'; + if (normalized === 'off') return 'Off'; + return 'Auto'; +} + +function InfoRow({ label, value, tone = '' }) { + return ( +
    + {label} + {value} +
    + ); +} + +function formatPublisherProgress(progress = null) { + /* + Keep the spectator card dense, but expose enough ffmpeg progress to tell if + the PTZ transcoder is running behind when the live feed looks delayed. + */ + if (!progress) return null; + return [ + progress.fps ? `fps ${progress.fps}` : null, + progress.speed ? `speed ${progress.speed}` : null, + progress.drop_frames ? `drop ${progress.drop_frames}` : null, + ].filter(Boolean).join(' | '); +} + +function PtzSnapshotFallback({ label, source }) { + const snapshotFeeds = usePtzCameraSnapshots([PTZ_CAMERA_ID], { enabled: true }); + const snapshot = snapshotFeeds[PTZ_CAMERA_ID] || null; + return ( +
    + {snapshot?.objectUrl ? ( + {label} + ) : ( +
    + {snapshot?.error || source?.error || 'Waiting for PTZ snapshot...'} +
    + )} + {/* + Keep the PTZ snapshot fallback visually aligned with RoverMediaPlayer: + the video surface owns only playback health, while the card around it + owns identity/context. That prevents a second in-frame camera title and + keeps fallback mode from looking different than live WHEP mode. + */} +
    +
    + Status: {snapshot?.status || 'snapshot'} +
    +
    +
    + ); +} + +function PtzLiveOrSnapshot({ label }) { + return ( + } + /> + ); +} + +export default function PtzSpectatorCard() { + const ptz = useSessionSelector((state) => state.session?.ptzCamera || null); + if (!ptz?.enabled) return null; + + const publisher = ptz?.publisher || {}; + const publisherStatus = publisher.running + ? 'running' + : publisher.restartAt + ? 'restarting' + : publisher.lastEvent || 'stopped'; + const publisherProgress = formatPublisherProgress(publisher.progress); + const queueCount = Array.isArray(ptz?.queue) ? ptz.queue.length : 0; + const label = ptz?.name || 'PTZ Camera'; + + return ( +
    + +
    + + + + + + + {publisherProgress ? : null} + {publisher.lastStderr ? ( +
    + {publisher.lastStderr} +
    + ) : null} +
    +
    + ); +} diff --git a/webui/src/spectate/SpectatorApp/components/RoverRow.jsx b/webui/src/spectate/SpectatorApp/components/RoverRow.jsx index d9da8e49..857835de 100644 --- a/webui/src/spectate/SpectatorApp/components/RoverRow.jsx +++ b/webui/src/spectate/SpectatorApp/components/RoverRow.jsx @@ -2,16 +2,16 @@ // Purpose: Defines the Rover Row module and the local helpers/components used in this file. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. import RoverSpectatorCard from './RoverSpectatorCard.jsx'; +import PtzSpectatorCard from './PtzSpectatorCard.jsx'; export default function RoverRow({ roster }) { - if (roster.length === 0) { - return

    No rovers registered.

    ; - } return (
    + {roster.length === 0 ?

    No rovers registered.

    : null} {roster.map((rover) => ( ))} +
    ); }